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.

DimensionWebViewHost-owned runtime
Trigger<a download> is a no-opUniform runtime.download()
Android pathHost must wire DownloadListenerNative DownloadManager
iOS pathNo download managerNative WKDownloadDelegate
Blob savesNot from the pageOne API
PermissionsManual per appNative bridge
ConsistencyPer-engineUniform

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