download
DownloadParams implement Downloadable. The client returns Result<Vec<u8>, ClientDownloadError> after applicable session refresh and host failover. Static URLs bypass media IQs and their first reference rejection is final.
download_to_writer
WriterCleanup exposes refusal or cleanup failure. Cancellation or executor loss can prevent return and cleanup; publish a separately retained destination only after success.
Downloadable uses MaybeSendSync; writer methods require DownloadWriter + MaybeSend + 'static. Import the markers from whatsapp_rust::wacore::sync_marker. Native targets retain Send/Sync requirements. On wasm32, local implementations can hold Rc/RefCell state. The writer remains a synchronous Write/Seek/truncate sink; these bounds do not make it a browser stream API.
DownloadWriter trait
A writer must implementWrite, Seek, and truncate. Cursor<Vec<u8>> and std::fs::File implement this contract. A custom host writer must truncate existing output as well as accept writes and rewinds.
ClientDownloadError
Match with a fallback arm because the enum is extensible.
ErrorChainExt recovers typed source information such as HTTP status. Local writer I/O is terminal; HTTP reader failure can still fail over. An existing anyhow::Result function can use ?, or explicitly convert the error at an erased return boundary.
Automatic retry and URL re-derivation
For session-derived routes, the client performs applicable host failover and can refresh the media host list once after HTTP 401. HTTP 403/404/410 are terminal reference rejections. Static URLs bypass session acquisition and their first reference rejection is terminal. InspectClientDownloadError for the final classification; a refreshed empty host list preserves the original rejection status. The standalone MediaDownloader cannot ask a client for refreshed hosts.
Local writer failures do not trigger CDN retry or refresh. A consumed sink may have changed before failure; use the cleanup and cancellation boundaries above rather than publish partial output.
Recovering the CDN status by type
CallErrorChainExt::http_status() on the typed ClientDownloadError. It walks retained sources; absence of a status does not imply an HTTP exchange succeeded. Session acquisition failures retain their IQ cause, while cleanup failures can preserve both the download and cleanup causes. See error chain recovery.
Music artwork
wa::EmbeddedMusic implements Downloadable for encrypted artwork. To derive the mode from its actual chat:
MediaType::MusicArtwork and NewsletterMusicArtwork provide the corresponding routes.
DownloadParams
Store validated, owned CDN metadata without keeping the original message. Fields are private. Both constructors copy the input slices and returnResult<DownloadParams, DownloadPreparationError>.
DownloadParams::encrypted
ProductCatalogImage and NewsletterMusicArtwork, return RequiresPlaintext.
DownloadParams::plaintext
ProductCatalogImage and NewsletterMusicArtwork. MusicArtwork returns RequiresEncryption.
Downloadable to read direct_path(), media_key(), file_sha256(), file_enc_sha256(), file_length(), and app_info(). Construct a new value to change metadata. Replace struct literals with the appropriate constructor and handle its error, usually with ?.
These constructors validate lengths and encryption mode. URL and route validation still happens when preparing the request. DownloadParams has no static-URL field; use the original message or a custom Downloadable to retain a static URL. The container works with download, download_to_writer, and MediaDownloader.
Metadata validation
DownloadUtils::validate_download_metadata(&dyn Downloadable) returns Result<(), DownloadPreparationError>. Client downloads perform this check before requesting a media session, and request preparation checks it before HTTP. Present hashes must contain 32 bytes. Required metadata depends on encryption mode and whether a static URL is available. Optional hashes remain optional where the transport permits them.
The owned container’s 32-byte key requirement does not change the raw Downloadable key contract. Static URLs still bypass the media-session IQ. Missing fields, invalid hashes, and incompatible modes return local preparation errors; use a fallback arm when matching the non-exhaustive DownloadPreparationError.
MediaDownloader
Downloads and decrypts media from the CDN with no connectedClient. Everything a download needs beyond the CDN hosts already lives in the Downloadable itself; MediaDownloader takes the hosts up front instead of asking a live session for them, so a persisted reference (for example a stored DownloadParams) stays downloadable after the client has disconnected.
A live Client still needs a session to fetch its hosts (cached and refreshed automatically, not re-fetched on every call — see Automatic retry and URL re-derivation); MediaDownloader is the path for callers that have no session to ask with at all — a background worker, a queue consumer, or a CLI tool operating on data a paired client saved earlier.
Arc<dyn HttpClient>
required
The same
HttpClient implementation you pass to ClientBuilder. See HTTP Client Trait.Arc<dyn Runtime>
required
Runtime abstraction used to run blocking decrypt/streaming work.
MediaRoute
required
The CDN hosts to try, in order. See
MediaRoute below.download and download_to_writer mirror Client::download and Client::download_to_writer; same streaming/buffered branching, same retry-on-another-host behavior across the route’s hosts, same writer contract for the streaming variant. A Client can refresh its media host list once after HTTP 401. HTTP 403/404/410 are terminal reference rejections. MediaDownloader has no session to refresh, so a reference rejection is terminal on the first attempt.
Example: download after the session is gone
MediaDownloadError
MediaDownloadError is #[non_exhaustive]; match it with a wildcard (_ => ) arm so a future variant doesn’t break your build.
Client::download and Client::download_to_writer return ClientDownloadError, which classifies the final outcome after applicable media-session refresh and host failover. Low-level MediaDownloader uses MediaDownloadError.
MediaRoute and MediaHost
MediaRoute contains CDN hosts in failover order. It carries no authentication token. MediaRoute::from(&media_conn) copies only hosts from the session; upload keeps its separate authentication requirements.
MediaRoute::default_hosts() when you need the built-in DEFAULT_MEDIA_HOSTS. A connected client obtains hosts from its media session. A retained route can be used after disconnect while the media reference remains valid.
DownloadUtils::prepare_download_requests preserves existing query parameters, percent encoding, and fragments while adding the existing media token parameter. Relative media paths remain on the selected HTTPS route origin. Download URLs do not receive session auth. Supply URI-encoded direct paths: malformed percent escapes, raw whitespace, backslashes, and raw non-ASCII characters are rejected. References cannot add credentials or change the selected HTTPS origin. Equivalent IPv6 literal spellings compare as addresses; scheme and port restrictions still apply.
fetch_sticker_pack
Fetch first-party sticker pack metadata (and the per-sticker download handles) from the WhatsApp CDN.&str
required
The first-party sticker pack ID (typically extracted from a received
sticker_pack_message).&str
required
BCP-47 locale tag for localized name / publisher strings. Pass
"en" to match whatsmeow’s default.StickerPack
Pack metadata plus a
Vec<StickerPackItem> of individual stickers. Each StickerPackItem implements Downloadable, so you can pass it straight to client.download(..., whatsapp_rust::download::DownloadLimits::default()).https://static.whatsapp.net/sticker?lottie=1&cat=sticker_pack_data&id={pack_id}&lg={locale}, parses the JSON envelope, and constructs the StickerPack. The endpoint is unauthenticated — the call works whether or not you are paired.
A non-2xx response fails with the same contract as download: the status is recoverable via ErrorChainExt::http_status() (added in PR #1195), not only by parsing the error message.
StickerPack
StickerPackItem
The CDN response is a JSON envelope, not a protobuf message —
StickerPack / StickerPackItem live in wacore::sticker_pack and are independent of waproto::whatsapp::StickerPackMessage (which represents the inline pack-bubble in a chat). The struct mirrors whatsmeow’s FirstPartyStickerPack.Downloadable Trait
TheDownloadable trait provides a generic interface for downloading media from any message type.
fn() -> Option<&str>
WhatsApp CDN path for the media file
fn() -> Option<&[u8]>
32-byte encryption key. Present for E2EE media,
None for plaintext (newsletter/channel) media.fn() -> Option<&[u8]>
SHA-256 hash of the encrypted file. Used for encrypted media validation.
fn() -> Option<&[u8]>
SHA-256 hash of the decrypted file. Used for plaintext media validation.
fn() -> Option<u64>
Original file size in bytes
fn() -> MediaType
Media type for HKDF key derivation (
Image, Video, Audio, Document, etc.)fn() -> Option<&str>
default:"None"
Static CDN URL for direct download. Present on newsletter/channel media, bypasses host construction.
fn() -> bool
default:"media_key().is_some()"
Returns
true if media is encrypted (has media_key), false for plaintext mediaBuilt-in Implementations
TheDownloadable trait is automatically implemented for:
wa::message::ImageMessagewa::message::VideoMessagewa::message::AudioMessagewa::message::DocumentMessagewa::message::StickerMessagewa::ExternalBlobReference(app state)wa::message::HistorySyncNotification
MediaType
Media type enum for encryption/decryption.Image/Sticker→"WhatsApp Image Keys"Video→"WhatsApp Video Keys"Audio→"WhatsApp Audio Keys"Document→"WhatsApp Document Keys"History→"WhatsApp History Keys"GroupHistory→"Group History"MusicArtwork/NewsletterMusicArtwork→"WhatsApp Music Artwork Keys"AppState→"WhatsApp App State Keys"StickerPack→"WhatsApp Sticker Pack Keys"StickerPackThumbnail→"WhatsApp Sticker Pack Thumbnail Keys"LinkThumbnail→"WhatsApp Link Thumbnail Keys"
MediaType methods
Upload paths
ProductCatalogImage is unencrypted — is_encrypted() returns false. This matches WhatsApp Web’s behavior where CreateMediaKeys.js skips encryption for product catalog images. Its upload path is /product/image (not under the /mms/ prefix like other media types).Media Decryption
WhatsApp uses different handling for encrypted (E2EE) and plaintext media:Encrypted Media (E2EE)
- Download encrypted bytes from CDN
- Verify HMAC-SHA256 (last 10 bytes)
- Decrypt using AES-256-CBC with keys derived from
media_keyvia HKDF - Verify the declared
file_enc_sha256matches the ciphertext SHA-256 (when the downloadable carries one), then verify the declaredfile_sha256matches the decrypted plaintext SHA-256. A mismatch fails the attempt withMediaDecryptionError::EncryptedSha256MismatchorPlaintextSha256Mismatchand the client tries the next CDN host. - Return decrypted plaintext
media_key is expanded using HKDF-SHA256 to derive:
- 16-byte IV
- 32-byte cipher key
- 32-byte MAC key
Plaintext media (newsletter/channel)
- Download plaintext bytes from CDN (often via
static_url) - Verify SHA-256 hash matches
file_sha256 - Return plaintext (no decryption needed)
Newsletter and channel media is not encrypted. The library automatically detects this when
media_key is absent and switches to plaintext validation.Example: detect media type
DownloadUtils
Low-level static methods for media decryption and validation. These are re-exported fromwacore::download and useful when you need fine-grained control over the download pipeline.
Key methods
Safe diagnostics
ClientDownloadError and MediaDownloadError omit opaque cause text from both Display and Debug. Library media tracing records safe classifications and typed facts such as HTTP status or I/O kind. Original causes remain accessible through Error::source and downcasting. Explicitly formatting those causes can still expose signed URLs or other backend data.
Download byte limits
EveryClient and MediaDownloader download and download_to_writer call takes a final DownloadLimits argument. Import it from whatsapp_rust::download. DownloadLimits::default() adds no media-layer byte cap. Use DownloadLimits::new(max_wire_bytes, max_plaintext_bytes) for explicit budgets.
Wire accounting includes padding and MAC. Both totals include failed attempts across host failover and session refresh; retrying within the operation does not reset the budget. Limits count media-layer bytes, not HTTP/TLS buffers, allocator capacity or process memory. Declared file sizes and response headers do not replace this accounting.
Bounded downloads require a streaming HTTP adapter. Without one they return DownloadPolicyError::StreamingRequired before HTTP. SDK budget failures are terminal ResourcePolicy errors on ClientDownloadError or MediaDownloadError; they do not trigger another host or refresh. LimitExceeded identifies the MediaByteDomain and limit. Match all these non-exhaustive enums with a fallback. An adapter or writer error containing the same public error type retains its I/O origin.
Dropping a bounded download future requests cancellation at cooperative I/O checkpoints. It cannot interrupt a blocked adapter or writer call; already-scheduled cleanup may continue. Keep the writer publication rules above even with a small byte budget.