Skip to content

[callback] treat missing message context as no reply_to_message in template quick-reply#216

Merged
david-lev merged 2 commits into
david-lev:devfrom
WouterDurnez:fix/callback-missing-context
Jul 13, 2026
Merged

[callback] treat missing message context as no reply_to_message in template quick-reply#216
david-lev merged 2 commits into
david-lev:devfrom
WouterDurnez:fix/callback-missing-context

Conversation

@WouterDurnez

Copy link
Copy Markdown
Contributor

The bug

WhatsApp Cloud API does not always include the context field on incoming type: "button" messages (template quick-reply presses). We hit this in production (EU WABA, 2026-07-13) with a payload that carried the newer user_id / from_user_id fields but no context:

{
  "from": "32400000001",
  "from_user_id": "BE.0000000000000000",
  "id": "wamid.HBg...",
  "timestamp": "1783932397",
  "type": "button",
  "button": {"payload": "...", "text": "..."}
}

CallbackButton.from_update (and CallbackSelection.from_update) index msg["context"] unconditionally:

File "pywa_async/server.py", line 231, in _call_handlers
    ].from_update(client=self, update=raw_update)
File "pywa/types/callback.py", line 298, in from_update
    reply_to_message=ReplyToMessage.from_dict(msg["context"]),
KeyError: 'context'

The server catches this, logs Failed to construct update, and still returns 200 — so Meta never redelivers and the user's button press is silently lost (the registered handler never fires).

The fix

Parse context the same way Message.from_update already does (message.py: content.get("context", {})ReplyToMessage.from_dict(context) if context.get("id") else None): treat it as optional and set reply_to_message=None when absent. Annotations and docstrings updated to ReplyToMessage | None accordingly (matching Message.reply_to_message).

Tests

Added quick_reply_without_context / callback_without_context update fixtures plus reply_to_message assertions in test_updates.py. Without the fix, the new fixtures reproduce the production crash immediately (fixture construction in tests/common.py raises KeyError: 'context'); with it, the full suite passes with no regressions.

Thanks for the great library! 🙏

WhatsApp does not always include the context field on incoming
template quick-reply button presses (observed in production on Cloud
API webhooks that also carry the new user_id/from_user_id fields).
CallbackButton.from_update and CallbackSelection.from_update index
msg["context"] unconditionally, so such updates raise KeyError:
'context' and are dropped by the server's update constructor — the
handler never fires and, since 200 is returned, Meta never redelivers.

Parse context the same way Message.from_update already does: treat it
as optional and set reply_to_message=None when absent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@yehuda-lev yehuda-lev added the bug Something isn't working label Jul 13, 2026
@yehuda-lev yehuda-lev changed the title [callback] treat missing message context as no reply_to_message [callback] treat missing message context as no reply_to_message in template quick-reply Jul 13, 2026
@david-lev

Copy link
Copy Markdown
Owner

Hi @WouterDurnez, thanks for the contribution and for adding the test cases!

Just to help me understand the root cause better, could you share which version of the messages webhook field you are currently using where you ran into this?

You can find this information in your app settings > WhatsApp > Configuration:
image

Also, did you notice if there's a specific scenario that triggers CallbackSelection to drop the context field as well?

Thanks again!

@WouterDurnez

Copy link
Copy Markdown
Contributor Author

Hi David, the messages webhook field is using v25.0.

The observed case was a template quick-reply button (type: "button"). We’ve only seen one occurrence and haven’t observed CallbackSelection/list_reply dropping context.

I included CallbackSelection defensively because its constructor makes the same unconditional msg["context"] assumption; that test is synthetic rather than based on a production payload. Happy to narrow the change if you prefer.

Thanks!

@david-lev

Copy link
Copy Markdown
Owner

Thanks for the clarification! That makes total sense.

The reason this only happens for CallbackButton is because it handles two distinct payload types: interactive (for regular button_reply presses) and button (for template QuickReplyButton clicks). Meta omits the context field specifically on that button template type.

pywa/pywa/types/callback.py

Lines 280 to 288 in 6ebd161

match msg_type := msg["type"]:
case MessageType.INTERACTIVE:
title = msg["interactive"]["button_reply"]["title"]
data = msg["interactive"]["button_reply"]["id"]
case MessageType.BUTTON:
title = msg["button"]["text"]
data = msg["button"]["payload"]
case _:
raise ValueError(f"Invalid message type {msg_type}")

Since CallbackSelection only ever handles interactive payloads (list_reply), it shouldn't actually be at risk of dropping the context field.

Let's go ahead and narrow the change down. Could you please:

  • Revert the defensive changes and synthetic tests made to CallbackSelection.
  • Update the docstrings for reply_to_message in CallbackButton (for both the synchronous and asynchronous files) to explicitly mention that it can be None specifically when a template quick-reply button is clicked.

Appreciate the thoroughness! Let me know when you've updated it.

CallbackSelection only ever parses interactive list_reply payloads,
which always carry the context field — only the button payload type
(template QuickReplyButton clicks) omits it. Revert the defensive
context parsing and synthetic fixture for CallbackSelection, and
document on CallbackButton.reply_to_message (sync and async) that it
is None specifically for template quick-reply clicks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@WouterDurnez

Copy link
Copy Markdown
Contributor Author

Done in fbaadb6 👍

  • Reverted all CallbackSelection changes (defensive context parsing, | None annotation, docstring, and the synthetic callback_without_context fixture/assertions) — it's back to exactly what's on master.
  • Updated the reply_to_message docstring on CallbackButton in both pywa/types/callback.py and pywa_async/types/callback.py to state it's None specifically when a template QuickReplyButton is clicked (WhatsApp omits context on button payloads).

Full suite passes (158 passed). Thanks for the review!

@david-lev
david-lev merged commit 116c642 into david-lev:dev Jul 13, 2026
2 checks passed
@david-lev

Copy link
Copy Markdown
Owner

Awesome, thank you for the quick update and for cleaning that up! Everything looks perfectly dialed in now.
Merging this right away. Thanks again for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants