immich-app/immich · error · Error

Failed to fetch picture: ${response.statusText}

Error message

Failed to fetch picture: ${response.statusText}

What it means

`getProfilePicture` fetches an avatar URL (usually from the OAuth provider) and throws `Failed to fetch picture: <statusText>` whenever the HTTP response is not ok (status outside 200-299). It is a plain fetch with no retry or auth, so any 403/404/5xx from the image host surfaces as this error.

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 5666d57f15)

Solutions

  1. Log the URL and statusText/status in the error to see why the fetch failed (403 vs 404 vs DNS).
  2. Re-fetch the picture URL from a fresh profile response — signed URLs may have expired.
  3. Check that the image host is reachable from the server (curl the URL from inside the container); fix DNS/proxy if self-hosted.
  4. Add headers the CDN may require (User-Agent) or fetch the picture via the provider's API instead of the raw URL.
  5. Treat as non-fatal: catch the error and fall back to a default avatar instead of failing the whole login.

Example fix

// before
const pic = await getProfilePicture(profile.avatarUrl); // throws on 403
// after
try {
  pic = await getProfilePicture(profile.avatarUrl);
} catch {
  pic = DEFAULT_AVATAR;
}
Defensive patterns

Strategy: fallback

Try / catch

try {
  const buf = await getProfilePicture(url);
} catch (e) {
  // e.message contains the statusText
  console.warn(`avatar fetch failed (${e.message}), using default`);
  const buf = DEFAULT_AVATAR;
}

Prevention

When it happens

Trigger: The profile image URL returned by the provider is expired (many providers' avatar URLs are signed and short-lived), returns 403 when fetched without credentials/UA, 404 after the user removed the picture, rate-limits (429), or the host is unreachable/DNS fails.

Common situations: Provider avatar URLs expiring between profile fetch and image download; hotlink protection on the image CDN blocking server-side fetches; self-hosted provider on an internal hostname the server can't resolve; large images hitting timeouts.

Related errors


AI-assisted analysis of immich-app/immich@5666d57f15 (2026-09-01). Data as JSON: /api/errors/5e1c4cd992d7284c. Report an issue: GitHub.