Skip to content

[templates] parse media headers returned without an example - #231

Open
sebastian-dix-flaerobotics-ai wants to merge 1 commit into
david-lev:devfrom
sebastian-dix-flaerobotics-ai:bugfix-media-header-missing-example
Open

[templates] parse media headers returned without an example#231
sebastian-dix-flaerobotics-ai wants to merge 1 commit into
david-lev:devfrom
sebastian-dix-flaerobotics-ai:bugfix-media-header-missing-example

Conversation

@sebastian-dix-flaerobotics-ai

Copy link
Copy Markdown

The problem

_BaseMediaHeaderComponent.from_dict reads data["example"]["header_handle"][0] unconditionally:

@classmethod
def from_dict(cls, data: dict) -> _BaseMediaHeaderComponent:
    return cls(example=data["example"]["header_handle"][0])

But WhatsApp does not always return the sample media when a template is read back. The sample templates Meta provisions on a new WABA come back with a bare header:

{"type": "HEADER", "format": "IMAGE"}

So _parse_component hits KeyError: 'example', logs it at ERROR with a full traceback, and falls back to the raw dict:

pywa.types.templates ERROR Failed to parse component: {'type': 'HEADER', 'format': 'IMAGE'}.
Defaulting to raw dictionary representation. Please update pywa or report this issue.
KeyError: 'example'

Reproduces on 3.8.0, 3.9.0 and 4.4.0.

Where I hit it

A WhatsApp Business Account still carrying Meta's "Jasper's Market" onboarding samples. Listing its templates parses three such headers — jaspers_market_image_cta_v1 plus two media-carousel cards — so each get_templates() call emitted three ERROR lines with tracebacks. Nothing downstream broke (the dict fallback is fine), but the log noise tripped our production container-error alert.

Verified against a real WABA that this is specific to templates whose sample media WhatsApp did not receive from the business: a template I created myself with an uploaded header_handle gets example.header_handle echoed back as a scontent.whatsapp.net URL, both while PENDING and once APPROVED. So approval is not what strips it.

The change

  • from_dict treats a missing header handle as what it is — absent on read — and still returns the typed component. example may therefore be None, which the annotations and docstrings now say.
  • A header with no example has nothing to upload, so it is skipped when uploading a template's media (sync and async) rather than reaching detect_media_source(None). Submitting such a template still fails in to_dict with the existing "... media example not uploaded yet." error.

A component carrying a malformed example still goes down the normal path, so a genuine parse regression is still reported.

Tests

Three cases added to tests/test_templates.py: a bare header for each of IMAGE/VIDEO/DOCUMENT parses with no log output, a header with an example still carries its handle, and an example-less header cannot be submitted.

uv run pytest — 554 passed. uv run ruff check . and uv run ty check clean.

@yehuda-lev

Copy link
Copy Markdown
Collaborator

Hi, thanks for the report.
Can you provide details how to reduce the bug?
Just call get_templates() to carousel template? Can you share the carousel code? I tried now and there is no error..

`_BaseMediaHeaderComponent.from_dict` reads `data["example"]["header_handle"][0]`
unconditionally, but WhatsApp does not always return the sample media when a
template is read back. The sample templates Meta provisions on a new WABA come
back as bare `{"type": "HEADER", "format": "IMAGE"}`, so every one of them raises
`KeyError: 'example'`, which `_parse_component` logs at ERROR with a traceback
before falling back to the raw dict.

Treat that exact shape — no `example` key at all — as what it is, absent on read,
so the component is still parsed into its type. `example` may therefore be `None`,
which the annotations and docstrings now say. An `example` that is present but not
the documented shape still raises, so a real API change is reported rather than
silently parsed into a component with no media.

A header with no example has nothing to upload, so it is skipped when uploading
a template's media instead of being handed to `detect_media_source(None)`;
submitting such a template still fails in `to_dict` with the existing
"media example not uploaded yet" error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sebastian-dix-flaerobotics-ai
sebastian-dix-flaerobotics-ai force-pushed the bugfix-media-header-missing-example branch from f725564 to 7c5ef56 Compare September 7, 2026 05:54
@sebastian-dix-flaerobotics-ai

Copy link
Copy Markdown
Author

Thanks for looking! The reason you saw no error is the key to the bug: a template you created yourself will never trigger it. When you upload the sample media, WhatsApp echoes example.header_handle straight back on read, so from_dict finds what it expects.

I verified that asymmetry against a real WABA — uploaded a sample image via the Resumable Upload API, created a template with an IMAGE header, then read it back:

{"type": "HEADER", "format": "IMAGE",
 "example": {"header_handle": ["https://scontent.whatsapp.net/v/t61.29466-34/663658189_...png?..."]}}

Same while PENDING and once APPROVED, so template status is not the variable either.

It breaks on templates whose sample media the business never sent — most commonly the sample templates Meta provisions on a new WABA (the "Jasper's Market" set). Those come back bare:

{"type": "HEADER", "format": "IMAGE"}

Repro without any API call

The payload above is all it takes, so this needs no WABA:

from pywa.types.templates import _parse_component

_parse_component({"type": "HEADER", "format": "IMAGE"})
# ERROR pywa.types.templates Failed to parse component: {'type': 'HEADER', 'format': 'IMAGE'}.
# Defaulting to raw dictionary representation. Please update pywa or report this issue.
# KeyError: 'example'

Reproduces on 3.8.0, 3.9.0 and 4.4.0.

Repro through the client

get_templates() on a WABA that still has Meta's provisioned samples. On the one I hit this on, three headers fail per listing — jaspers_market_image_cta_v1 has one at the top level, and jaspers_market_media_carousel_v1 has one per card:

{"type": "CAROUSEL", "cards": [
  {"components": [{"type": "HEADER", "format": "IMAGE"},
                  {"type": "BODY", "text": "Simple and Healthy Sheet Pan Dinner to Feed the Whole Family"},
                  {"type": "BUTTONS", "buttons": [{"type": "URL", "text": "Get this recipe", "url": "https://developers.facebook.com/docs/..."}]}]},
  {"components": [{"type": "HEADER", "format": "IMAGE"},
                  {"type": "BODY", "text": "3 Plant-Powered Salad Bowls to Fuel Your Week"},
                  {"type": "BUTTONS", "buttons": [{"type": "URL", "text": "Get this recipe", "url": "https://developers.facebook.com/docs/..."}]}]}]}

If a fresh test WABA of yours has hello_world and the jaspers_market_* templates, listing it should show it immediately. Nothing downstream actually breaks — the dict fallback is fine — but it is 3 ERROR lines with tracebacks per listing, which is what made it visible to us (it tripped a production container-error alert).

Update just pushed

I narrowed the fix to address review feedback on our side: only a completely absent example key is now treated as "no example". An example that is present but not the documented shape still raises and gets reported, so a real API change on your side is not silently parsed into a component with no media. Added a test for that too.

Happy to reshape any of this if you'd prefer a different approach — e.g. logging at debug for this known-benign case instead of returning example=None.

@yehuda-lev

Copy link
Copy Markdown
Collaborator

The reason you saw no error is the key to the bug: a template you created yourself will never trigger it.

It breaks on templates whose sample media the business never sent — most commonly the sample templates Meta provisions on a new WABA (the "Jasper's Market" set).

Can you explain how can I reduce the bug?
What do I need to Todo to get the same error?

Btw, can you edit the code

return cls(example=data["example"]["header_handle"][0])
and add print(data) before the parsing and share the response here?I want to see how the response looks like.
Thank you!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants