2026-06-22 OWUI Mobile Image Download

What I set out to do

User report: pressing “download” on a generated image in Open WebUI from an iPhone did nothing. Started as “check the logs and narrow it down”; became a long on-device debugging arc that exposed two unrelated platform bugs and made my first fix obsolete.

What I actually did

  • Confirmed it’s purely client-side — no server request fires for the download, so logs were silent. The preview download (ImagePreview.svelte) saves via file-saver’s saveAs.
  • First fix (commit 21ed4b1): a capture-phase click interceptor → in-gesture same-origin <a download> with ?attachment=true (the /api/v1/files/<id>/content endpoint already sends Content-Disposition: attachment). Verified on desktop with Playwright. Wrong layer — still dead on the phone.
  • Built an on-phone debug harness: injected eruda (CDN mobile console) + progressively richer click / modal-open / element-stack / probe loggers into index.html via docker exec, iterating from the device’s own console output.
  • On-device logs revealed: preview opens, image is a file URL (not data:), but taps on the download button never reach a <button> (land on a bare DIV) — and even a big fixed max-z-index debug button couldn’t be tapped. All preview buttons are dead on touch.
  • Root cause: PanzoomContainer calls preventDefault() on touch pointerdown, which suppresses the browser’s synthesized click. The download button only binds on:click → never fires on touch. (Close button works only because it also binds on:pointerdown.)
  • Reworked fix (commit 393835d): trigger on pointerup/touchend (not suppressed) + click (desktop, deduped); on touch save via the Web Share API using the image pre-fetched when the modal opens (so share() runs in-gesture), anchor fallback on desktop.
  • PWA still served a stale shell: OWUI ships index.html with no Cache-Control, so the iOS PWA heuristically cached it. Added Cache-Control: no-cache in main.py’s SPAStaticFiles.get_response (second patch in the same patcher). Busting the existing stale copy needed Delete App + re-add — an installed iOS PWA has isolated storage (not in Safari’s Website Data) and no pull-to-refresh.
  • Confirmed working in Safari and the installed PWA.

What was striking

  • My committed first fix was the right mechanism at the wrong layer. On touch the click never fires, so nothing downstream ever ran. Only on-device instrumentation surfaced this — desktop Playwright passed every check.
  • “Buttons in the preview don’t work on touch” was the unlock. A guaranteed-tappable green debug button that still couldn’t be tapped proved the problem was systemic (panzoom eating touch), not a mis-tap on a tiny target.
  • iOS PWA caching is its own trap: close+reopen doesn’t reload, the site isn’t in Safari’s Website Data, and only delete+re-add clears the shell.

Lessons worth keeping

  • Inspect the live system (on the actual device) before theorizing — I shipped a desktop-verified fix that ignored the touch reality. Injecting eruda on the phone was the turning point.
  • For SPA shells, no Cache-Control = stale PWAs; ship no-cache so injected shell patches actually propagate. (Captured in memory: reference_owui_mobile_image_download.)

Top 3 next

  1. User opted not to file upstream — but the panzoom touch-click suppression and the missing shell cache headers are both genuine OWUI bugs worth a report if it recurs.
  2. After OWUI :main rebuilds, check the patcher log for SKIP (anchor not unique/found) — the main.py SPAStaticFiles.get_response anchor could drift and silently drop the no-cache patch.
  3. Web Share path is unverified for data:-URL inline images (current images are all file URLs) — revisit if a chat model starts returning inline base64.

Open WebUI · LiteLLM · SigNoz