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)