BuildNot yet confirmed elsewhere1 publisher3 min readPublished
canvas.toBlob hands you a PNG and calls it WebP: check blob.type, not the user agent
The HTML spec mandates the silent substitution, blob.type is the only record that it happened, and a user-agent lookup table cannot stand in for the check.
The Engineer · Build desk
What happened
- A user could not open the .webp files a browser-based tool produced; a hex editor showed PNG magic bytes under the .webp extension.
- That is specified behaviour: when the requested MIME type is unsupported, toBlob must encode PNG instead, without raising an error.
- Canvas exposes no capability query for image encoding, unlike MediaRecorder.isTypeSupported and Media Capabilities encodingInfo.
Why it matters
- exposure The mislabelled file leaves the browser and fails in somebody else's desktop application, and the support ticket arrives at your tool rather than at the engine that made the substitution.
- decision Teams reaching for a user-agent version table are choosing a lookup that reports the wrong answer the moment a phone user asks for the desktop site.
- constraint With no query available, a format menu cannot be assembled from a static support matrix; it has to be built from probe results after the browser has been made to encode something.
- cost Verification is cheap enough to remove the excuse: one 2x2 encode per format, cached, plus a kilobyte or two of inlined base64 if decode support also needs testing.
The substitution survives every guard a careful person already writes. The callback fires, the blob is not null, and its size looks plausible [5]. Nothing in the API reports the swap, because the specification requires the fallback and says nothing about disclosing it: no exception, no warning, no second argument [2]. The single witness is `blob.type`, which on iOS below 16.4 reads `image/png` after you asked for `image/webp` [3].
There is an accident of history worth noting here. `toDataURL` falls back identically, but the string it returns opens with `data:image/png;base64,`, so the mismatch is readable without tooling [4]. The binary path is the quiet one.
The measured numbers are where this stops being theoretical. On desktop Chromium, six requested types produced two real encodes: WebP at 564 bytes and JPEG at 796 [9]. AVIF, HEIC, TIFF and GIF each came back as `image/png` at 95 bytes, and those four blobs were byte-for-byte identical, being the same 2x2 PNG four times over [9][10]. Two thirds of the requested list was mislabelled [14]. A pipeline whose only test is "did I get a blob" scores six successes out of six on that run while writing four wrong files [16]. Even the size heuristic is no help at real dimensions: the fallback is 17 percent of the WebP at the probe's 2x2 size [15], a gap that says nothing about a 4000-pixel export.
The AVIF row is the one that reorders priorities. Chrome decodes AVIF perfectly well in an `img` element and will not produce one from canvas [11], because encode and decode are separate code paths shipped on separate schedules [17]. Safari 16 has the same asymmetry, and nothing mainstream encodes HEIC at all [17]. An export menu offering HEIC is offering PNG with a different suffix.
Which is why the version table is the wrong instrument. Every browser on iOS is WebKit underneath, so "is this Safari" answers nothing useful, and in-app webviews report system versions that do not line up with the standalone browser. A user who enables Request Desktop Website will hand a phone's capabilities over a macOS user agent [6]. The string answers who the client claims to be; the question is whether it can encode WebP at this moment, and engine version, OS version, host app and build flags all sit in between [7]. Canvas has no `isTypeSupported` and no `encodingInfo`, though MediaRecorder and Media Capabilities both do [8], so the information has to be produced rather than looked up: a 2x2 canvas, one semi-transparent pixel so alpha is probed at the same time, one `toBlob` per MIME type, and the comparison `blob.type === mime`, cached [12].
The deeper defect in the original code is naming. If `blob.type` is the only field that tells the truth [3], then it, not the requested MIME, should decide the extension. Code that writes `output.webp` because it asked for WebP is asserting a fact it never verified, and the person who discovers the error is downstream with a hex editor [1].
What to watch
- Whether any vendor or spec proposal adds a capability query for canvas encoding, which would retire runtime probing.
- Whether Chromium ships AVIF encoding in canvas, which would change the four-of-six fallback count on desktop.
- Whether export libraries start deriving the file extension from blob.type rather than from the requested MIME type.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence62
- Adoption
- Insufficient
- Hype gap0
- Incentives28
- Confidence58
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A user reported that the .webp files produced by the author's tool would not open on their desktop; opened in a hex editor, the first four bytes were 89 50 4E 47, a PNG carrying a .webp extension.
- [2]
The HTML spec explicitly requires that if the user agent does not support the requested type, canvas.toBlob must create the file using the PNG format instead, with no exception, no warning, and no second argument reporting what happened.
- [3]
blob.type is the only place the substitution is recorded; on iOS below 16.4 a request for image/webp returns a blob whose type is image/png.
- [4]
toDataURL performs the same fallback, but there it is visible to the naked eye because the returned data URL starts with data:image/png;base64,.
- [5]
When the fallback occurs the callback still fires, the blob is not null, and its size looks reasonable.
- [6]
User-agent detection fails on iOS: every browser there is WebKit underneath, embedded webviews inside apps track the system version in ways that do not always match the standalone browser, and a user can enable Request Desktop Website and present a macOS user agent from an iPhone.
- [7]
The user agent string answers who the client is, not whether it can encode WebP at that moment; engine version, OS version, host app and build flags sit between the two questions, and any mismatch makes a lookup table lie.
- [8]
MediaRecorder.isTypeSupported and navigator.mediaCapabilities.encodingInfo provide capability queries for media, but canvas toBlob and toDataURL expose no capability query at all; the spec defines the fallback without providing a way to ask about it up front.
- [9]
On desktop Chromium the author's probe returned: image/webp as image/webp at 564 B; image/jpeg as image/jpeg at 796 B; and image/avif, image/heic, image/tiff and image/gif each as image/png at 95 B.
- [10]
The four fallback results were byte-for-byte identical at 95 bytes because they are literally the same 2x2 PNG.
- [11]
Desktop Chrome decodes AVIF correctly in an img element but canvas will not produce one; the author had assumed AVIF encoding failure was a mobile-only problem.
- [12]
The author's capability probe creates a 2x2 canvas, fills one pixel with a semi-transparent colour that also probes alpha, calls toBlob with the requested MIME, and tests blob.type === mime, caching the result per MIME type in a Map.
- [13]
Probing decode support needs a sample file in that format, so the author inlines base64 probes: a 1x1 AVIF runs a couple of hundred bytes and a handful of formats together costs a kilobyte or two.
- [14]
Four of the six formats requested on desktop Chromium came back as PNG, two thirds of the list.
- [15]
At the probe's 2x2 size the 95-byte fallback PNG is about 17 percent of the 564-byte real WebP encode.
- [16]
A pipeline that only checks whether the blob is non-null records six successes out of six on that Chromium run while four of the files are mislabelled.
- [17]
Encoding and decoding are separate code paths shipped independently: Safari 16 decodes AVIF but cannot encode it, and nothing mainstream encodes HEIC at all.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toYour canvas.toBlob might be silently handing you a PNG
1 article · August 23, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.