On the open web, an <a download> link or a PDF/CSV endpoint saves a file to the user's device. Inside a WebView that same tap is a silent no-op. Android exposes downloads only through a host-side DownloadListener, and iOS WKWebView ships with no download manager. A host-owned runtime bridges the download layer — one API that saves a real file on every engine.
Android: downloads need a host listener
Android's own docs and the wider community agree: a stock WebView does not download anything by itself. When a user taps a download link, the engine hands the request parameters — url, userAgent, contentDisposition, mimeType, contentLength — to the DownloadListener you register via setDownloadListener, or nothing happens at all. Without that listener, a checkout page's "Download invoice PDF" button that works in Chrome does absolutely nothing inside the app's WebView. Wiring it means owning the WebView instance, parsing the filename from Content-Disposition, requesting WRITE_EXTERNAL_STORAGE (or handing the job to DownloadManager), and presenting progress to the user. Blob downloads and <a download> are the same story: the page cannot initiate the file write; only the host can.
iOS: WKWebView has no download manager
Apple's WebView has no built-in download pipeline. Write-ups of WKWebView limitations put it bluntly: "No download manager, so file downloads fail or silently do nothing." Before iOS 15 the only path was to intercept the request and re-implement the save yourself. iOS 15 added WKDownloadDelegate, but the host must still implement it — the <a download> attribute only signals intent, and the actual execution lives in the app. The default behavior is worse than a no-op: the file often opens inside the web frame and breaks navigation history, or the tap is swallowed entirely with no feedback.
What breaks when download dies
- Invoices and statements never land. A "Download PDF" button that works in Safari does nothing in the WebView.
- CSV and report exports vanish. SaaS and finance dashboards that export data lose the only path out.
- App updates and APKs stall. Update flows that push an APK or a config bundle fail inside the embedded browser.
- Embedded browsers diverge further. WeChat, Douyin, and similar in-app WebViews rarely expose any download surface, so the break hides during desktop QA.
The runtime fix
A host-owned runtime bridges the download layer. The page calls one uniform API — runtime.download(url, options) or runtime.saveFile(blob) — and the host saves through DownloadManager on Android and WKDownloadDelegate on iOS. Filename, destination, progress, and storage permission are handled natively and consistently. One integration, every engine, one saved file.
| Dimension | WebView | Host-owned runtime |
|---|---|---|
| Trigger | <a download> is a no-op | Uniform runtime.download() |
| Android path | Host must wire DownloadListener | Native DownloadManager |
| iOS path | No download manager | Native WKDownloadDelegate |
| Blob saves | Not from the page | One API |
| Permissions | Manual per app | Native bridge |
| Consistency | Per-engine | Uniform |
A native app saves a file on every device. A stock WebView saves on almost none. A runtime gives the page one save action — and a real file at the end of it.
Sources
- dev.to — WebView's download listener on Android: how it works — without
setDownloadListener, tapping a download link does nothing; the listener handsurl/mimeType/filenameto the host. - DevGex — Enabling file download in Android WebView —
setDownloadListenerplusDownloadManager; storage permissions required. - gotoapp.store — WKWebView limitations — "No download manager, so file downloads fail or silently do nothing."
- Medium — Downloading files from a WKWebView since iOS 15 —
WKDownloadDelegate;<a download>signals intent but execution still needs the host;canShowMimeType. - GitHub MAUI discussion — WebView on iOS can't download file — default iOS WebView shows the file in-page or breaks navigation; you must add
WKDownloadDelegatehandlers.