Skip to content

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

How we use AISend a correction

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
Why these scores

Claim ledger

Ranked by verification strength, evidence, and original report placement.

  1. [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.

    ReportedSupportedView cited source
  2. [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.

    ReportedSupportedView cited source
  3. [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.

    ReportedSupportedView cited source

Sources

1 independent publisher whose own reporting we read for this story.

  1. dev.to

    1 article · August 23, 2026

    Your canvas.toBlob might be silently handing you a PNG

Share your take

Let Clarity write the post for you.

Signed-in readers get a short post drafted on this story in the register they choose — narrative, analytical, or a direct position — editable to the last word before it goes anywhere. The share buttons at the top of this story work without an account.

Topics and entities

Follow any of these and your For You feed starts watching them — no settings page required.

Topics

Entities

Loading related stories