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 viafile-saver’ssaveAs. - First fix (commit
21ed4b1): a capture-phaseclickinterceptor → in-gesture same-origin<a download>with?attachment=true(the/api/v1/files/<id>/contentendpoint already sendsContent-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.htmlviadocker 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 bareDIV) — and even a big fixed max-z-index debug button couldn’t be tapped. All preview buttons are dead on touch. - Root cause:
PanzoomContainercallspreventDefault()on touch pointerdown, which suppresses the browser’s synthesizedclick. The download button only bindson:click→ never fires on touch. (Close button works only because it also bindson:pointerdown.) - Reworked fix (commit
393835d): trigger onpointerup/touchend(not suppressed) +click(desktop, deduped); on touch save via the Web Share API using the image pre-fetched when the modal opens (soshare()runs in-gesture), anchor fallback on desktop. - PWA still served a stale shell: OWUI ships
index.htmlwith noCache-Control, so the iOS PWA heuristically cached it. AddedCache-Control: no-cacheinmain.py’sSPAStaticFiles.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
clicknever 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; shipno-cacheso injected shell patches actually propagate. (Captured in memory: reference_owui_mobile_image_download.)
Top 3 next
- 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.
- After OWUI
:mainrebuilds, check the patcher log forSKIP (anchor not unique/found)— themain.pySPAStaticFiles.get_responseanchor could drift and silently drop theno-cachepatch. - 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.
Related
Open WebUI · LiteLLM · SigNoz