immich-app/immich · error
Failed to fetch picture
Error message
Failed to fetch picture: ${response.statusText} What it means
getProfilePicture fetches the user's avatar URL and throws if the HTTP response status is not ok, embedding the statusText. It indicates the picture URL returned 4xx/5xx (deleted avatar, expired signed URL, auth required, provider outage).
Solutions
- Log response.status and re-fetch the user profile to get a fresh picture URL, then retry
- Fall back to a default avatar when the fetch fails
- Check the URL is reachable (curl -I) and not expired
- Respect rate limits / add caching for avatar responses
Example fix
// before
const pic = await oauthRepo.getProfilePicture(user.pictureUrl);
// after
let pic;
try { pic = await oauthRepo.getProfilePicture(user.pictureUrl); }
catch { pic = DEFAULT_AVATAR; } Defensive patterns
Strategy: fallback
Validate before calling
const probe = await fetch(url, { method: 'HEAD' });
if (!probe.ok) console.warn(`Avatar URL unhealthy (${probe.status}), using fallback`); Try / catch
try {
avatar = await oauthRepo.getProfilePicture(pictureUrl);
} catch (e) {
if (String(e.message).startsWith('Failed to fetch picture')) {
avatar = DEFAULT_AVATAR; // 404/expired/rate-limited URL
} else throw e;
} Prevention
- Cache avatars locally and refresh on a schedule instead of fetching on every render
- Handle 429 with backoff — avatar CDNs rate-limit aggressively
- Re-fetch the profile when the stored picture URL returns 404/403
- Never treat a missing avatar as a login failure
When it happens
Trigger: getProfilePicture(url) is called and response.ok is false — e.g. 404 (avatar removed), 401/403 (signed URL expired or private), 429 (rate limited), 5xx (provider error).
Common situations: Stale cached profile picture URL from an old login; Google/GitHub avatar URL that was deleted or made private; expired signed S3 URLs; provider rate limiting avatar requests.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- await response.text()
- EOFException
- Error backchannel logout: token validation failed
- Error in OAuth discovery
- errors.unable_to_upload_file
AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15).
Data as JSON: /api/errors/5e1c4cd992d7284c.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/repositories/oauth.repository.ts:135
if (error.message.includes('unexpected JWT alg received')) {
this.logger.warn(
[
'Algorithm mismatch. Make sure the signing algorithm is set correctly in the OAuth settings.',
'Or, that you have specified a signing key in your OAuth provider.',
].join(' '),
);
}
this.logger.error('OAuth login failed', error);
throw new Error('OAuth login failed', { cause: error });
}
}
async getProfilePicture(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch picture: ${response.statusText}`);
}
return response.arrayBuffer();
}
private jwksClients: Map<string, JWTVerifyGetKey> = new Map(); // useful for caching and performnce
async validateLogoutToken(config: OAuthConfig, logoutToken: string): Promise<{ sub?: string; sid?: string } | null> {
const client = await this.getClient(config);
const algorithm = client.clientMetadata().id_token_signed_response_alg ?? 'RS256';
let keyOrGetter: Uint8Array | JWTVerifyGetKey;
try {
if (algorithm.startsWith('HS')) {
keyOrGetter = new TextEncoder().encode(config.clientSecret);
} else {
const jwksUri = client.serverMetadata().jwks_uri;
if (!jwksUri) {
throw new Error('Unable to get JWKS URI');View on GitHub (pinned to e55ac299a4)