Build1 publisher3 min readPublished
The HTML spec tells a browser to fall back to image/png when it cannot encode the type you asked for, and Safari cannot encode WebP, so the only thing between your users and a mislabeled download is a check on blob.type.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The callback signature cannot tell you which of two things you received. toBlob returns a Blob, and on the post's reading of the HTML spec an unsupported type is swapped for image/png [5], with neither a throw nor a null to mark the substitution [4]. The pixels are fine and the file is valid; the only field carrying the truth is blob.type [7]. Safari has decoded WebP since version 14 [3]. Encoding one through canvas has never been available there [2].
The guard in the post awaits convertToBlob, compares the returned type against the requested MIME, and throws a message that names the missing capability and points at the WebAssembly encoder [7]. That single comparison turns a silent wrong-file bug into an unsupported-browser error, which is a condition you can branch on.
The same shape shows up one layer down. libwebp will hit a byte budget directly: ask for 200 KB and the encoder converges on it, instead of re-encoding at a dozen qualities and bisecting [17]. Convergence happens through repeated entropy analysis, so at the default of a single pass there is nothing to converge across and target_size is dropped with no indication [18]. The post sets pass to 6 only when a budget is actually present, because passes cost real time [19]. Both bugs have one signature: a parameter is accepted, something else happens, and verification means inspecting the output rather than trusting the input [21].
The survey behind the opening claim is "every browser-based WebP converter I looked at" [1], which is a sample, not a census. Two conditions decide whether it reaches your code: you request a non-PNG type from canvas, and you derive the download filename from the format you asked for rather than from the blob you got back. If both hold and any of your traffic is Safari, the mislabeling is already shipping.
Leaving canvas costs a codec bundle, and the bundler configuration is where the post spends most of its six documented problems [8] [22]. The exclude and include lists in one Vite config read as contradictory until you have the test. jSquash's Emscripten glue gets rewritten by the dependency pre-bundler and then cannot resolve its sibling .wasm file, so it goes in exclude [10]. utif2 and libheif-js are CommonJS, and pre-bundling is the step that makes them importable at all, so they go in include [11]. What decides the side is whether the package loads a separate .wasm at runtime [12] [20]. Matching is per specifier, so @jsquash/webp does not cover @jsquash/webp/encode, and every subpath needs its own line [13].
The dev-server failure is the more expensive one to diagnose. Vite's startup scan walks the source graph and not dynamic imports inside workers [15], so the lazy HEIC load that keeps a JPEG user from pulling 2 MB [14] stays undeclared until someone converts one, at which point Vite re-optimizes and the page reload takes the queue with it [16]. The console message for that, 504 (Outdated Optimize Dep), is accurate and useless [16].
The assert is the cheap part. What it exposes is the price of an encoder you actually control: a WASM codec, plus a config that has to be told twice about the same dependency graph [9].
Ranked by verification strength, evidence, and original report placement.
canvas.toBlob does not throw and does not return null when the requested type is unsupported.
The HTML spec says that if the requested type is not supported, the browser falls back to image/png.
On Safari the WebP request hands back a perfectly valid PNG, so a download named photo.webp ships a PNG with the wrong extension, and the user finds out only when something downstream rejects it.
The only way to detect the substitution is to check the returned blob: the post awaits canvas.convertToBlob({ type: mime, quality: quality / 100 }) and throws when blob.type !== mime, with an error stating that the browser cannot encode that format via canvas, that Safari does not support it, and that the WebAssembly encoder is required.
Follow any of these and your For You feed starts watching them — no settings page required.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
First-hand and checkable, but singular
The mechanism is the strongest part: dev.to's author does not merely report that Safari returns a PNG, he attributes it to the HTML specification's instruction to fall back to image/png, which makes the behaviour predictable rather than anecdotal. Every failure comes with the code that produces it and the error string it emits, down to the 504 console message. Against that, one developer is the entire record — no WebKit bug reference, and nobody else confirming the Vite re-optimization behaviour or the Safari version detail.
No usage visible
There is nothing to count. The converter the author built is never named, no traffic or install figures appear, and no other team is described adopting the blob.type guard or the WASM codec path. The claim that surveyed converters all use the canvas one-liner comes without a list of which ones were examined.
Filed smaller than its reach
The Safari finding is treated as a warm-up to a six-item debugging list, yet it is the one item with consequences outside the author's own repository: any site offering client-side WebP export is handing Safari visitors a file whose name lies about its contents. No superlatives, no product to sell, and an admission that the bug took him an embarrassingly long time to spot. We keep the reading close to aligned because the breadth of the exposure is inferred from the spec's behaviour, not counted.
Reputation, not revenue
The payoff for this kind of dev.to post is a byline and credibility among people solving the same problem, and the author spends part of it admitting he shipped a bug. Nothing is being sold: the app goes unnamed, no sponsor appears, and the critical material falls on Safari, Vite and libwebp defaults, none of which are his to promote. The mild self-interest is in the framing of having done the harder thing by compiling the codec himself.
Solid mechanics, single witness
We would act on the blob.type guard tomorrow, because the reasoning behind it is specification-level and self-consistent, and the same signature explains the second bug: a setting accepted, ignored, and never reported. Confidence stops short of high because version numbers, the exact Vite behaviour, and the scope of affected converters all rest on one person's recollection, and our coverage contains no independent check of any of them.
build
canvas.toBlob hands you a PNG and calls it WebP: check blob.type, not the user agent1 publisher
build
FFmpeg.wasm ships, but budget for a watchdog: one in ten heavy jobs deadlocked in silence1 publisher
build
Vellum redacts PDFs by rasterizing pages, trading away selectable text1 publisher
build
Pin `formats` before you take the next/image v4 bump1 publisher
Publishers with included, body-backed reporting in this cluster.
1 article · September 6, 2026