From bde6be91ca5ca6992d864ece6a8bfc731abd7fd7 Mon Sep 17 00:00:00 2001 From: psavarmattas Date: Sat, 15 Aug 2026 16:37:44 +0100 Subject: [PATCH] fix(avatar): retry the redirect fetch instead of giving up on one failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You confirmed the whole upload chain works — the new avatar shows instantly on Outline's web app — but this app kept showing the old/ placeholder picture in both Settings and the sidebar, which read the same underlying session.userAvatarURL. Since that state is provably correct (the same URL that works on web), the bug has to be in how AvatarBadge loads it, not in the update itself. Leading theory: a freshly-uploaded attachment's redirect URL can fail on the very first request right after upload — self-hosted storage behind a reverse proxy isn't necessarily instantly consistent — and AvatarBadge's .task(id:) only ever fires once per URL with no retry, so a single transient failure right after uploading would leave the placeholder showing forever even though the exact same URL works fine moments later (which lines up with it looking fine on a fresh web load). Now retries twice with a short delay and explicitly checks the HTTP status before treating the body as image data, instead of silently accepting whatever came back. Also drops any URLCache involvement (.reloadIgnoringLocalCacheData) as a second, independent possible cause, cheap to rule out at the same time. Couldn't confirm this is the actual root cause without being able to run the app — worth retesting. --- Outpost/Support/AvatarBadge.swift | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Outpost/Support/AvatarBadge.swift b/Outpost/Support/AvatarBadge.swift index bf35d89..a5920a7 100644 --- a/Outpost/Support/AvatarBadge.swift +++ b/Outpost/Support/AvatarBadge.swift @@ -41,11 +41,35 @@ struct AvatarBadge: View { .task(id: avatarURL) { loadedImage = nil guard let avatarURL else { return } - guard let (data, _) = try? await URLSession.shared.data(from: avatarURL) else { return } - loadedImage = PlatformImage(data: data) + loadedImage = await Self.loadImage(from: avatarURL) } } + /// A freshly-uploaded attachment's redirect URL can 404/fail on the very + /// first request right after upload (self-hosted reverse-proxied + /// storage, not necessarily instantly consistent) — since `.task(id:)` + /// only ever runs once per URL and never retries on its own, a single + /// transient failure right after uploading a new avatar would leave the + /// placeholder showing forever, even though the exact same URL works + /// fine moments later. Two short-delayed retries before giving up. + private static func loadImage(from url: URL) async -> PlatformImage? { + var request = URLRequest(url: url) + request.cachePolicy = .reloadIgnoringLocalCacheData + + for attempt in 0..<3 { + if attempt > 0 { + try? await Task.sleep(for: .milliseconds(400)) + } + if let (data, response) = try? await URLSession.shared.data(for: request), + let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode), + let image = PlatformImage(data: data) { + return image + } + } + return nil + } + private func platformImage(_ image: PlatformImage) -> Image { #if os(macOS) Image(nsImage: image)