Skip to content

GOWS 2026.5.1: document media — 403 on live media + media-retry never completes/refreshes directPath #2131

Description

@Adonix-Digital

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

  1. Receive a document (PDF) from a contact. Engine downloadMedia=true returns 403 for that message.
  2. 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.
  3. GET https://mmg.whatsapp.net{directPath} (plain HTTPS, no auth header) → 200, full encrypted blob.
  4. 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=69F24B922026-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

  1. A contact forwards an old document (one whose CDN object has expired — directPath.oe in the past).
  2. GET .../messages/{id}?downloadMedia=true → 403, then hangs (>150s, no response) on retry.
  3. GET .../messages/{id}?downloadMedia=falsedocumentMessage.directPath has oe in the past; anonymous GET {directPath} → 403.
  4. Re-read step 3 repeatedly over time → directPath/oe never changes (no refreshed reference from media-retry).
  5. 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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions