{"record":{"id":"5e1c4cd992d7284c","repo":"immich-app/immich","slug":"failed-to-fetch-picture-response-statustext","errorCode":null,"errorMessage":"Failed to fetch picture: ${response.statusText}","messagePattern":"Failed to fetch picture: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/repositories/oauth.repository.ts","lineNumber":135,"sourceCode":"      if (error.message.includes('unexpected JWT alg received')) {\n        this.logger.warn(\n          [\n            'Algorithm mismatch. Make sure the signing algorithm is set correctly in the OAuth settings.',\n            'Or, that you have specified a signing key in your OAuth provider.',\n          ].join(' '),\n        );\n      }\n\n      this.logger.error('OAuth login failed', error);\n\n      throw new Error('OAuth login failed', { cause: error });\n    }\n  }\n\n  async getProfilePicture(url: string) {\n    const response = await fetch(url);\n    if (!response.ok) {\n      throw new Error(`Failed to fetch picture: ${response.statusText}`);\n    }\n\n    return response.arrayBuffer();\n  }\n\n  private jwksClients: Map<string, JWTVerifyGetKey> = new Map(); // useful for caching and performnce\n  async validateLogoutToken(config: OAuthConfig, logoutToken: string): Promise<{ sub?: string; sid?: string } | null> {\n    const client = await this.getClient(config);\n    const algorithm = client.clientMetadata().id_token_signed_response_alg ?? 'RS256';\n    let keyOrGetter: Uint8Array | JWTVerifyGetKey;\n\n    try {\n      if (algorithm.startsWith('HS')) {\n        keyOrGetter = new TextEncoder().encode(config.clientSecret);\n      } else {\n        const jwksUri = client.serverMetadata().jwks_uri;\n        if (!jwksUri) {\n          throw new Error('Unable to get JWKS URI');","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/server/src/repositories/oauth.repository.ts#L117-L153","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nconst pic = await oauthRepo.getProfilePicture(user.pictureUrl);\n// after\nlet pic;\ntry { pic = await oauthRepo.getProfilePicture(user.pictureUrl); }\ncatch { pic = DEFAULT_AVATAR; }","handlingStrategy":"fallback","validationCode":"const probe = await fetch(url, { method: 'HEAD' });\nif (!probe.ok) console.warn(`Avatar URL unhealthy (${probe.status}), using fallback`);","typeGuard":null,"tryCatchPattern":"try {\n  avatar = await oauthRepo.getProfilePicture(pictureUrl);\n} catch (e) {\n  if (String(e.message).startsWith('Failed to fetch picture')) {\n    avatar = DEFAULT_AVATAR; // 404/expired/rate-limited URL\n  } else throw e;\n}","preventionTips":["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"],"tags":["http","oauth","profile-picture"],"backgroundTag":"http-error-response","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}