Skip to content

[Bug]: iOS re-downloads every attachment image on each reconnect because signed asset URLs change and expo-image has no stable cache key聽#18812

Closed

Description

@Nelglor

Note

馃 Claude Fable 5.1 responding on behalf of Nick

Before submitting

Area

apps/mobile

Steps to reproduce

  1. From the iOS app, start a thread on a remote environment (Tailscale or T3 Connect) and attach a dozen photos across a few messages.
  2. Use the phone the way people use it while an agent runs: lock it, unlock to check, lock again. Every unlock reconnects the environment socket.
  3. Watch the server trace for GET /api/assets/... from the phone's user agent.

Expected behavior

Each attachment image is downloaded once and served from expo-image's disk cache afterwards. A reconnect, or a new signed URL for the same attachment, should not cost another download.

Actual behavior

Every reconnect re-downloads every visible attachment image in full. In one thread over one afternoon, one 0.5 MB photo (IMG_7302.jpg) was fetched 33 times by the same phone under about 25 different signed URLs. Across the thread: 176 image GETs, 124 distinct URLs, 13 distinct images, all 200, roughly 95 MB over Tailscale for images the phone had already shown.

Three things combine:

  1. Asset URLs are signed per mint with a one hour expiry (ASSET_TOKEN_TTL_MS, apps/server/src/assets/AssetAccess.ts), so every mint is a new URL.
  2. The client's createUrl query re-executes when the environment connection changes (createEnvironmentQueryAtomFamily reads the connection atom, packages/client-runtime/src/state/runtime.ts), so each reconnect mints fresh URLs for every mounted image, well inside the 5 min stale / 30 min refresh / 60 min idle windows in packages/client-runtime/src/state/assets.ts.
  3. MessageAttachmentImage in apps/mobile/src/features/threads/ThreadFeed.tsx passes only source={{ uri }} to expo-image with no cacheKey, so a new URL is a disk-cache miss. The server sends cache-control: private, max-age=3600 but no ETag on bytes assets (apps/server/src/http.ts), so the request is always a full transfer.

Trace excerpt from the host (phone user agent T3Code/119), one three minute window. Each WS-TICKET / SESSION / SHELL group is an unlock; the image fetches that follow are the same photos each time:

19:24:16 WS-TICKET            19:25:01 WS-TICKET            19:26:27 WS-TICKET
19:24:17 SESSION              19:25:01 SESSION              19:26:28 SESSION
19:24:24 SHELL                19:25:07 SHELL                19:26:32 SHELL
19:24:27 IMG_7302.jpg RFs_as  19:25:07 IMG_7302.jpg GMWvJs  19:26:32 IMG_7302.jpg BxhWkU
19:24:27 IMG_7302.jpg GMWvJs  19:25:07 IMG_7302.jpg RFs_as  19:26:32 IMG_7302.jpg mjjzag
19:24:27 IMG_7303.jpg H8vA98  19:25:09 IMG_7302.jpg mjjzag  19:26:34 IMG_7302.jpg YQRung
19:24:27 IMG_7311.jpg XznlSk  19:25:09 IMG_7311.jpg Yab9TE  19:26:33 IMG_7311.jpg pMTNFA
...                           ...                           ...

(The suffix is the tail of the signed token. The same image also appears twice in the same second under two tokens, so two subscribers mint independently.)

Impact

Minor bug or occasional failure

It is a data and battery cost rather than a functional failure. On a photo-heavy thread the phone moved ~95 MB of already-seen images in one afternoon, decoded on screen each time. On cellular that is metered data.

Version or commit

Mobile TestFlight 2.0.0 (120). Server 0.0.46-nightly.20261012.3010. Code references checked against main at 511d20c6f2.

Environment

iPhone, iOS 26, Claude Agent provider. Host is Linux (Ubuntu) reached over Tailscale, not T3 Connect. Phone was on home wifi and cellular during the window.

Logs or stack traces

Counts above come from server.trace.ndjson on the host, filtered to the phone's user agent and url.path containing /api/assets/. Happy to share the raw span lines for the thread.

Screenshots, recordings, or supporting files

None needed; the trace is the evidence.

Workaround

None in the app. Not reconnecting is not an option on iOS, since the socket drops on lock.

Fix candidates

  • Smallest: give expo-image a stable cacheKey derived from the attachment id (and a version if the file can change) in MessageAttachmentImage, so URL churn stops defeating the disk cache. One component.
  • Also worth doing: keep an unexpired signed URL across a reconnect instead of re-minting it, and send an ETag on bytes assets so a conditional request can return 304.

I can send a PR for the cacheKey change if the diagnosis holds.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions