Build1 distinct publisher3 min readUpdated
The HTML download attribute is a hint browsers may ignore. On Android WebView and iOS Safari it was ignored silently, and DevTools emulation never noticed.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
For roughly a year, the save button in a vanilla JS collage editor did nothing on phones: no error, no corrupt file, just a tap, an active state, and silence [1]. According to the developer's writeup on dev.to, nobody filed a bug in that time, because a button that crashes gets reported and a button that does nothing gets read as user error [2][22].
The export was the textbook version. Draw to a canvas, call `canvas.toDataURL('image/png')`, create an anchor, set `href` and `download`, click it [4]. The editor is entirely client-side, images never leaving the device, which is the product's whole premise [3]. That code works in every desktop browser and it works in Chrome DevTools device emulation, which is precisely why it survived so long [5]. It does not work in an Android WebView, and it does not work in iOS Safari [6].
The mechanism is unglamorous: `download` is a hint, and both environments are free to ignore it [7]. An Android WebView has no download manager attached unless the host app wires one up, so the navigation to a `data:` URL is dropped [8]. iOS Safari has historically refused `download` on `data:` and `blob:` URLs from a synthetic click [9]. Neither throws, neither logs, and there is no rejected promise to catch or event to listen for [10]. There is no `if (downloadWorked)` to write, so the bug is not detectable from the JS side at all; you find it by holding a phone [11].
The fixes are platform-specific because the capability is. The mobile builds are the same editor wrapped in Capacitor, so on Android the developer handed the base64 payload to a plugin and wrote the bytes with the platform's own file APIs, via `Filesystem.writeFile` into `Directory.Documents` [12][13]. On iOS there was no equivalent escape hatch worth taking, so export goes through the Web Share API instead [14].
That is where the second silent failure lives. The feature detect has to be `navigator.canShare({ files: [file] })`, not a bare check for `navigator.share`, because sharing text is widely supported and sharing files is a separate capability; a detect without the payload lies [15]. Worse, `navigator.share()` requires transient user activation and must be called during the gesture that triggered it, and that activation is consumed by `await` [16]. The natural-looking version, awaiting `canvas.toBlob` before sharing, produces a share sheet that never appears or flashes and dies, with no error and no console output [17]. Same signature as the original bug.
So the data-URL-to-`File` conversion has to be synchronous: `toDataURL()`, `atob()`, and the `File` constructor all are, so the chain can run inside the handler with no yield point [18]. The ugly `charCodeAt` loop over a `Uint8Array` exists only to stay inside the gesture [19]. The prettier `fetch(dataUrl).then(r => r.blob())` is unusable because it is async, as is `canvas.toBlob()`; the loop costs a handful of milliseconds for a few megapixels [20]. Finding that cost the developer an evening [21].
Worth watching in your own stack: any success path that has no observable signal. Emulation reproduces viewport, not download policy or activation rules, so if the only proof that an export worked is a user telling you, assume it does not.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
Nobody reported the bug. The author's reading: a button that crashes gets a bug report; a button that does nothing gets interpreted as user error, so people assumed they had tapped wrong, tapped again, then left.
That approach works in every desktop browser and works in Chrome DevTools device emulation, which is how the bug survived so long, and it works in a mobile browser often enough not to raise immediate suspicion.
navigator.share() requires transient user activation and must be called during the gesture that triggered it, and that activation is consumed by await.
In the broken iOS version, the click handler awaits canvas.toBlob before calling navigator.share, and the share sheet either never appears or flashes and dies, with no error and no console output, because the await yielded to the event loop and closed the activation window.
The base64-to-File conversion therefore has to be synchronous: canvas.toDataURL(), atob(), and the File constructor are all synchronous, so the whole chain can run inside the handler with no yield point.
The dataUrlToFile helper splits the data URL, extracts the MIME type by regex, decodes with atob, copies into a Uint8Array with a charCodeAt loop, and returns a new File; the loop exists specifically to stay inside the gesture.
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.
Detailed and internally consistent, but single-source and unversioned
The account supplies verbatim before/after code, named APIs and explicit causal mechanisms (download as a hint, WebView without a download manager, transient activation consumed by await), which is unusually specific for a single post. Against that, it is one first-person item from one publisher with no browser/OS version matrix, no reproduction artefacts, no external spec or vendor references, and no data behind the 'zero bug reports' and 'works often enough' statements.
One personal app shipped both fixes; no wider usage data
Concrete deployment evidence exists but is narrow: the author shipped the Android native-write path and the iOS Web Share path in his own Capacitor-wrapped collage editor. The underlying building blocks (Capacitor Filesystem, Web Share API with files) are described as ordinary practice, yet the supplied material gives no user counts, install numbers, third-party adopters or downstream references, so adoption is measurable only at single-project scale.
Modestly understated relative to what the account shows
The framing is self-critical rather than promotional: the iOS outcome is called a compromise with an 'acceptable' ceiling, the caveat that a resolved share promise does not prove a file was saved is stated plainly, and no product, launch or performance boast is made. That pulls the gap negative. It does not go further negative because two assertions do reach beyond the evidence shown - the unversioned claim about iOS Safari refusing download on data:/blob: URLs, and the categorical 'does not work' framing supported only by manual single-device testing.
Personal developer post about the author's own tool; no vendor stake disclosed
The writer has a mild interest in presenting his own privacy-first collage editor and in earning developer-community credibility on a community publishing platform, and he names Capacitor as the dependency that made the Android fix possible. There is no evidence in the supplied material of sponsorship, vendor affiliation, paid placement or product being sold, and the post argues against its own iOS solution, which limits promotional pressure.
Plausible and specific, unverified beyond the author
Confidence is limited by structure rather than by internal quality: one publisher, one item, one developer's device testing, and no corroboration for either the platform behaviours or the zero-report outcome. The code-level specificity, the consistency of the two independently described failure modes (dropped data: navigation and consumed activation), and the author's own stated caveats support moderate confidence in the engineering account, while impact scale and current-version browser behaviour remain unestablished.
build
The NestJS default path puts the query inside the business rule, and nothing fails when it moves1 distinct publisher
build
Ornith-1.0's benchmarks are fine. Ollama can't parse its tool calls.1 distinct publisher
build
The capture returned HTTP 200. The file was a Cloudflare block page.1 distinct publisher
build
The check that never fires: why every agent-built detector needs a negative control1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 15, 2026