danny-avila/LibreChat · error

Failed to fetch image from URL. Status: ${response.status}

Error message

Failed to fetch image from URL. Status: ${response.status}

What it means

Thrown after the fetch completes when `response.ok` is false — i.e. the remote avatar server returned a non-2xx HTTP status. This is a downstream HTTP error: the URL was reachable, the protocol was allowed, SSRF agents connected, but the origin answered with an error code (404, 403, 500, etc.). The status code is interpolated into the message for diagnostics.

Source

Thrown at api/server/services/Files/images/avatar.js:55

    throw new Error(`Refusing to fetch avatar over ${parsed.protocol}`);
  }

  const { httpAgent, httpsAgent } = createSSRFSafeAgents();
  /**
   * `node-fetch` v2's `timeout` is the total request budget (request initiation
   * through full body receipt), not a TCP-connect-only timeout. That is the
   * stronger of the two for this path — bounds total slow-loris exposure.
   */
  const response = await fetch(parsed.href, {
    headers: fetchOptions.headers,
    agent: (urlObj) => (urlObj.protocol === 'https:' ? httpsAgent : httpAgent),
    redirect: 'error',
    timeout: 5000,
    size: MAX_AVATAR_BYTES,
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch image from URL. Status: ${response.status}`);
  }

  const contentLength = parseInt(response.headers.get('content-length') ?? '0', 10);
  if (contentLength > MAX_AVATAR_BYTES) {
    throw new Error(`Avatar response too large: ${contentLength} bytes`);
  }

  /**
   * Re-check after read in case the server lied about Content-Length or
   * omitted it. `node-fetch` v2 honors the `size` option above and throws on
   * overflow, but Defense-in-depth: assert on the actual buffer length.
   */
  const buffer = await response.buffer();
  if (buffer.length > MAX_AVATAR_BYTES) {
    throw new Error(`Avatar response too large: ${buffer.length} bytes`);
  }
  return buffer;
}

View on GitHub (pinned to 5ff282f900)

Solutions

  1. Verify the URL returns 2xx with a direct `curl -I <url>` from the server host.
  2. If the link is stale, refresh the user's `picture` from the OAuth provider at next login.
  3. For hotlink-protected origins, mirror the avatar to your own storage on first successful fetch instead of hotlinking on every render.
  4. Retry transient 5xx/429 once with backoff in the caller before surfacing failure to the user.

Example fix

// before: no retry, surfaces first 5xx
const buf = await fetchAvatarBuffer(url);

// after: one retry on transient failure
let buf, lastErr;
for (let attempt = 0; attempt < 2; attempt++) {
  try { buf = await fetchAvatarBuffer(url); break; }
  catch (e) {
    lastErr = e;
    if (!/Status: (5\d\d|429)/.test(e.message)) throw e;
    await new Promise(r => setTimeout(r, 500 * (attempt + 1)));
  }
}
if (!buf) throw lastErr;
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight HEAD request (origin must support HEAD)
async function checkAvatarUrl(url) {
  const res = await fetch(url, { method: 'HEAD', timeout: 5000 });
  if (!res.ok) throw new Error(`Avatar endpoint returned ${res.status}`);
  return res;
}

Try / catch

async function fetchWithRetry(url, attempts = 2) {
  let last;
  for (let i = 0; i < attempts; i++) {
    try { return await fetchAvatarBuffer(url); }
    catch (e) {
      last = e;
      const transient = /Status: (5\d\d|429)/.test(e.message);
      if (!transient || i === attempts - 1) throw e;
      await new Promise(r => setTimeout(r, 500 * (i + 1)));
    }
  }
  throw last;
}

Prevention

When it happens

Trigger: An avatar URL that resolves and connects but the origin returns 4xx/5xx: deleted profile photo (404), hotlink-protected image (403), expired signed URL (403/410), or upstream outage (5xx). Also a temporarily rate-limited CDN (429).

Common situations: Social provider avatar links that expire or get deleted after the user changed their profile picture; corporate CDN with Referer/Origin hotlink protection; rate-limited Gravatar/CDN endpoints under load; expired S3 presigned URLs stored as the avatar.

Related errors


AI-assisted analysis of danny-avila/LibreChat@5ff282f900 (2026-08-12). Data as JSON: /api/errors/9471f58485add5d0. Report an issue: GitHub.