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

  1. Log response.status and re-fetch the user profile to get a fresh picture URL, then retry
  2. Fall back to a default avatar when the fetch fails
  3. Check the URL is reachable (curl -I) and not expired
  4. 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

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


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)