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:
- 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.
- 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.
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 resetsattemptto 0 and re-fires the fetch throughInlineMediaLoader.staticImage(...), which goes throughHttpClientFactory.imageClient— aURLSessionconfigured withrequestCachePolicy = .useProtocolCachePolicyand a real 256 MB disk-backedURLCache(sharedImageURLCache).Two ways that combination produces a permanently-broken image after a successful upload:
BlossomClient.uploadreturns the public URL directly from the server'sPUT /media//uploadJSON 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.imageClientuses the default protocol cache policy with a persistentURLCache, if the server's 404 response (or an intermediary CDN's) is sent with cacheable headers,URLCachecan store it. Every subsequent load of that same URL — including the manual "Tap to retry" — is a plainsession.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 ofsession.data(from: url), so a stale cached failure can't block a user-initiated retry indefinitely. Threading this through meansInlineMediaLoader.staticImageneeds a way to force a cache-bypassing fetch (e.g. abypassCache: Boolparam plumbed fromRetryingAsyncImage's tap handler), since it's currently a bareURL-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.