GOWS 2026.5.1: document media download fails with 403 while the signed directPath is anonymously downloadable (200)
Engine: GOWS PLUS · Version: 2026.5.1 · Storage: none (we read media via API) · Sessions: multi-tenant, many sessions, production load.
Summary
Inbound document messages (PDFs) intermittently fail to download: GET /api/{session}/chats/{chat}/messages/{id}?downloadMedia=true returns
Error: 13 INTERNAL: failed to download media: download failed with status code 403
This happens only with documents. Images, audio, video, stickers from the very same chats/senders never 403 (measured over several days: 0% loss on image/audio/video, ~2–14% on documents).
The key finding: the media is NOT gone. At the same moment the engine returns 403, the message's own signed directPath (with a still-valid oe expiry) downloads the encrypted blob anonymously with HTTP 200, and it decrypts correctly (valid file, matching fileSHA256).
Reproduction
- Receive a document (PDF) from a contact. Engine
downloadMedia=true returns 403 for that message.
- Fetch the message metadata without downloading:
GET .../messages/{id}?downloadMedia=false and read _data.Message.documentMessage.directPath (note its oe query param is a future unix timestamp, i.e. not expired) and mediaKey.
GET https://mmg.whatsapp.net{directPath} (plain HTTPS, no auth header) → 200, full encrypted blob.
- Decrypt with the standard scheme (HKDF-SHA256 of
mediaKey, info "WhatsApp Document Keys" → iv|cipherKey|macKey; verify last-10-byte HMAC; AES-256-CBC) → the original PDF, with SHA256 matching fileSHA256.
We verified step 3/4 against a failing production message: blob 295930 bytes (HTTP 200), HMAC valid, decrypted 295910 bytes, %PDF- header, fileSHA256 exact match — both from a US datacenter egress and via a BR HTTP proxy (so it is not an egress/IP/geo issue on our side).
Important gotcha for anyone reproducing: do not test using the message's top-level media.url field — its oe is frequently already in the past (expired) and will 403 misleadingly. Test against documentMessage.directPath, whose oe is valid for ~30 days.
Expected vs actual
- Expected: since the signed
directPath is anonymously fetchable (200) and the blob decrypts, the engine should successfully download the document (directly via the signed path, or via media-retry).
- Actual: the engine's authenticated download returns 403 and gives up. Our hypothesis is that whatsmeow's download attaches the
mediaConn Auth token and the CDN rejects the authenticated request for documents in this state, whereas anonymous access via the signed oh/oe succeeds. Documents being
lazy-downloaded (not eagerly cached like image thumbnails) likely exposes this where other types don't.
Notes
Happy to provide engine logs for a specific failing message id on request.
Case 2 (update 2026-06-25): forwarded/old document — downloadMedia=true hangs and the message's directPath is never refreshed after media-retry
A second, distinct failure mode on the same engine version, this time for a forwarded (old) document whose CDN object had already expired by the time the message was delivered to us.
Summary
- Inbound document (PDF) arrives.
downloadMedia=true first returns 403, then on subsequent calls hangs indefinitely (we waited 60s and 150s — the request never returns, vs the instant 403 of Case 1). This strongly suggests the engine did engage media-retry but it never completes/returns.
- The message's own
directPath has an oe in the past (oe=69F24B92 → 2026-04-29), even though the message was delivered to us on 2026-06-25 — i.e. the sender forwarded an old document that references a CDN object uploaded ~2 months earlier. Anonymous GET https://mmg.whatsapp.net{directPath} →
403 (so, unlike Case 1, the blob is genuinely expired on the CDN).
- The
directPath in the message metadata is never refreshed. We re-read ?downloadMedia=false repeatedly over ~80 minutes (and after the hanging downloadMedia=true calls that should have triggered retry); the directPath/oe stays the same expired April value every time. So even a "wait then re-fetch"
strategy cannot recover it — there is no fresh reference to use.
- The same document downloads fine in the official WhatsApp app on the linked phone. So the media is recoverable via the standard media-retry flow (the phone requests the sender to re-upload and gets a fresh reference) — but that fresh reference is delivered only to the requesting device (the phone), and our
GOWS companion session never receives it.
Reproduction
- A contact forwards an old document (one whose CDN object has expired —
directPath.oe in the past).
GET .../messages/{id}?downloadMedia=true → 403, then hangs (>150s, no response) on retry.
GET .../messages/{id}?downloadMedia=false → documentMessage.directPath has oe in the past; anonymous GET {directPath} → 403.
- Re-read step 3 repeatedly over time →
directPath/oe never changes (no refreshed reference from media-retry).
- Open the same message in the WhatsApp app on the linked phone → downloads successfully.
Expected vs actual
- Expected: when media-retry recovers the document (the phone proves it can), the engine should (a) not hang the
downloadMedia request indefinitely, and (b) update the stored message media reference with the fresh directPath from the retry notification, so a subsequent downloadMedia/re-fetch (or a
webhook) can deliver it.
- Actual:
downloadMedia=true hangs; the message's directPath is never refreshed; there is no webhook to signal recovered media; the companion session cannot obtain the file even though the account's phone can.
Asks
- Bound
downloadMedia=true so it does not hang indefinitely during media-retry — return promptly with a clear status (e.g. MEDIA_RETRY_PENDING) instead of blocking the HTTP call.
- Refresh the message's media reference after media-retry completes, so re-fetching the message returns the recovered media. Today the
directPath stays stale forever.
- Optionally: emit a webhook event when previously-unavailable media becomes available (there is currently no event for this — re-fetch is the only option and it never reflects the retry), and/or expose an explicit media-retry endpoint so consumers can trigger and await it.
Engine logs and a specific failing message id available on request.
GOWS 2026.5.1: document media download fails with 403 while the signed directPath is anonymously downloadable (200)
Engine: GOWS PLUS · Version:
2026.5.1· Storage: none (we read media via API) · Sessions: multi-tenant, many sessions, production load.Summary
Inbound document messages (PDFs) intermittently fail to download:
GET /api/{session}/chats/{chat}/messages/{id}?downloadMedia=truereturnsThis happens only with documents. Images, audio, video, stickers from the very same chats/senders never 403 (measured over several days: 0% loss on image/audio/video, ~2–14% on documents).
The key finding: the media is NOT gone. At the same moment the engine returns 403, the message's own signed
directPath(with a still-validoeexpiry) downloads the encrypted blob anonymously with HTTP 200, and it decrypts correctly (valid file, matchingfileSHA256).Reproduction
downloadMedia=truereturns 403 for that message.GET .../messages/{id}?downloadMedia=falseand read_data.Message.documentMessage.directPath(note itsoequery param is a future unix timestamp, i.e. not expired) andmediaKey.GET https://mmg.whatsapp.net{directPath}(plain HTTPS, no auth header) → 200, full encrypted blob.mediaKey, info"WhatsApp Document Keys"→ iv|cipherKey|macKey; verify last-10-byte HMAC; AES-256-CBC) → the original PDF, withSHA256matchingfileSHA256.We verified step 3/4 against a failing production message: blob 295930 bytes (HTTP 200), HMAC valid, decrypted 295910 bytes,
%PDF-header,fileSHA256exact match — both from a US datacenter egress and via a BR HTTP proxy (so it is not an egress/IP/geo issue on our side).Important gotcha for anyone reproducing: do not test using the message's top-level
media.urlfield — itsoeis frequently already in the past (expired) and will 403 misleadingly. Test againstdocumentMessage.directPath, whoseoeis valid for ~30 days.Expected vs actual
directPathis anonymously fetchable (200) and the blob decrypts, the engine should successfully download the document (directly via the signed path, or via media-retry).mediaConnAuthtoken and the CDN rejects the authenticated request for documents in this state, whereas anonymous access via the signedoh/oesucceeds. Documents beinglazy-downloaded (not eagerly cached like image thumbnails) likely exposes this where other types don't.
Notes
2026.5.1, which includes the 403/media-retry fixes ([GOWS] - failed to download media: download failed with status code 403 #2049) and the hash-mismatch retry ([GOWS] Extend MediaRetry to also trigger on ciphertext hash mismatch (not only 403) #2085). This case still fails — the media-retry path does not appear to recover it (the sender re-sending the document does not help either; the same document is downloadable fine from the WhatsApp app onthe linked phone).
directPathis still valid, fall back to fetching the signeddirectPathanonymously (the blob is already encrypted end-to-end; the engine has themediaKeyto decrypt)? That single fallback would recover these.Happy to provide engine logs for a specific failing message id on request.
Case 2 (update 2026-06-25): forwarded/old document —
downloadMedia=truehangs and the message's directPath is never refreshed after media-retryA second, distinct failure mode on the same engine version, this time for a forwarded (old) document whose CDN object had already expired by the time the message was delivered to us.
Summary
downloadMedia=truefirst returns 403, then on subsequent calls hangs indefinitely (we waited 60s and 150s — the request never returns, vs the instant 403 of Case 1). This strongly suggests the engine did engage media-retry but it never completes/returns.directPathhas anoein the past (oe=69F24B92→2026-04-29), even though the message was delivered to us on2026-06-25— i.e. the sender forwarded an old document that references a CDN object uploaded ~2 months earlier. AnonymousGET https://mmg.whatsapp.net{directPath}→403 (so, unlike Case 1, the blob is genuinely expired on the CDN).
directPathin the message metadata is never refreshed. We re-read?downloadMedia=falserepeatedly over ~80 minutes (and after the hangingdownloadMedia=truecalls that should have triggered retry); thedirectPath/oestays the same expired April value every time. So even a "wait then re-fetch"strategy cannot recover it — there is no fresh reference to use.
GOWS companion session never receives it.
Reproduction
directPath.oein the past).GET .../messages/{id}?downloadMedia=true→ 403, then hangs (>150s, no response) on retry.GET .../messages/{id}?downloadMedia=false→documentMessage.directPathhasoein the past; anonymousGET {directPath}→ 403.directPath/oenever changes (no refreshed reference from media-retry).Expected vs actual
downloadMediarequest indefinitely, and (b) update the stored message media reference with the freshdirectPathfrom the retry notification, so a subsequentdownloadMedia/re-fetch (or awebhook) can deliver it.
downloadMedia=truehangs; the message'sdirectPathis never refreshed; there is no webhook to signal recovered media; the companion session cannot obtain the file even though the account's phone can.Asks
downloadMedia=trueso it does not hang indefinitely during media-retry — return promptly with a clear status (e.g.MEDIA_RETRY_PENDING) instead of blocking the HTTP call.directPathstays stale forever.Engine logs and a specific failing message id available on request.