Skip to content

fix(compose): pasted image upload can render permanently broken with no working retry #419

Description

@dmnyc

Problem

Some images pasted into the composer (clipboard paste-to-upload, not the Photos picker) upload successfully but then render as a permanently broken "Tap to retry" placeholder in the live preview — and tapping retry never recovers it. This isn't every pasted image, which points at something environment/timing-dependent rather than a hard client bug in the upload call itself.

Likely cause

RetryingAsyncImage (RetryingAsyncImage.swift) automatically retries a failed load 3 times with exponential backoff (0.5s / 1s / 2s, ~3.5s total) before giving up and showing the "Tap to retry" placeholder. Tapping it resets attempt to 0 and re-fires the fetch through InlineMediaLoader.staticImage(...), which goes through HttpClientFactory.imageClient — a URLSession configured with requestCachePolicy = .useProtocolCachePolicy and a real 256 MB disk-backed URLCache (sharedImageURLCache).

Two ways that combination produces a permanently-broken image after a successful upload:

  1. Propagation delay vs. retry window. BlossomClient.upload returns the public URL directly from the server's PUT /media//upload JSON response (BUD-02) the moment the server acknowledges the upload — but some Blossom servers/CDNs return that URL before the blob is actually fetchable everywhere (mirrors, edge caches). If that lag exceeds ~3.5s, every automatic retry 404s before the resource is ready.
  2. A 404 during that window can get cached. Because imageClient uses the default protocol cache policy with a persistent URLCache, if the server's 404 response (or an intermediary CDN's) is sent with cacheable headers, URLCache can store it. Every subsequent load of that same URL — including the manual "Tap to retry" — is a plain session.data(from: url) call, which respects the cache and can keep replaying the stale 404 forever, even long after the blob becomes available. This is the "no remedy" part: the image may be fetchable again within seconds, but the app never asks the network again for that URL.

Fix direction

Have the manual "Tap to retry" path bypass the cache — issue that one fetch with URLRequest(url:, cachePolicy: .reloadIgnoringLocalCacheData) instead of session.data(from: url), so a stale cached failure can't block a user-initiated retry indefinitely. Threading this through means InlineMediaLoader.staticImage needs a way to force a cache-bypassing fetch (e.g. a bypassCache: Bool param plumbed from RetryingAsyncImage's tap handler), since it's currently a bare URL-based call with no per-request override.

Automatic retries can likely stay as-is (or grow slightly), but the manual retry is the one that must not be defeated by a cached error.

Repro

Hard to repro on demand since it depends on server-side timing, but the shape reported: paste an image into the composer, it uploads and shows a thumbnail/preview, and at some point the preview flips to "Tap to retry" and stays there no matter how many times it's tapped.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions