{"record":{"id":"414b32026165ec7a","repo":"vercel/next.js","slug":"url-parameter-is-valid-but-upstream-response-is","errorCode":null,"errorMessage":"\"url\" parameter is valid but upstream response is invalid","messagePattern":"\"url\" parameter is valid but upstream response is invalid","errorType":"http","errorClass":"ImageError","httpStatus":508,"severity":"error","filePath":"packages/next/src/server/image-optimizer.ts","lineNumber":910,"sourceCode":"    if (err.name === 'TimeoutError') {\n      Log.error('upstream image response timed out for', href)\n      throw new ImageError(\n        504,\n        '\"url\" parameter is valid but upstream response timed out'\n      )\n    }\n    throw err\n  }\n\n  const locationHeader = res.headers.get('Location')\n  if (\n    isRedirect(res.status) &&\n    locationHeader &&\n    URL.canParse(locationHeader, href)\n  ) {\n    if (count === 0) {\n      Log.error('upstream image response had too many redirects', href)\n      throw new ImageError(\n        508,\n        '\"url\" parameter is valid but upstream response is invalid'\n      )\n    }\n    const redirect = new URL(locationHeader, href).href\n    return fetchExternalImage(\n      redirect,\n      dangerouslyAllowLocalIP,\n      maximumResponseBody,\n      count - 1\n    )\n  }\n\n  if (!res.ok) {\n    Log.error('upstream image response failed for', href, res.status)\n    throw new ImageError(\n      res.status,\n      '\"url\" parameter is valid but upstream response is invalid'","sourceCodeStart":892,"sourceCodeEnd":928,"githubUrl":"https://github.com/vercel/next.js/blob/0ae8c72462952df163f1b1e0726641bc5b40dc93/packages/next/src/server/image-optimizer.ts#L892-L928","documentation":"Thrown by fetchExternalImage (ImageError 508) when the upstream returns a redirect (301/302/303/307/308) with a parseable Location header but the redirect counter has hit zero (default 3 hops exhausted). fetchExternalImage recurses with count-1 on each redirect; when count===0 it stops and reports the loop. The status 508 (Loop Detected) signals too many redirects.","triggerScenarios":"Upstream responds with a chain of redirects longer than 3, or a redirect loop. Each hop decrements count; when it reaches 0 the throw at line 910 fires.","commonSituations":"The image URL is behind a redirect chain (auth gate -> CDN -> origin). A misconfigured upstream returns redirects back to itself (loop). HTTP<->HTTPS redirects that bounce. A URL shortener plus CDN producing multiple hops.","solutions":["Resolve the final image URL (follow redirects manually) and configure that direct URL in remotePatterns.","Fix the upstream redirect configuration to reduce hop count or break the loop.","Ensure the upstream returns the image directly (200) instead of redirecting.","Cache the optimized result so the redirect chain is only walked once."],"exampleFix":"# before: url redirects through many hops until count hits 0\n/_next/image?url=https://host.example/redirect-to-cdn  # 508\n\n# after: use the resolved final URL\n/_next/image?url=https://cdn.example/final-photo.jpg&w=640&q=75","handlingStrategy":"retry","validationCode":"// Resolve final image URL (following redirects) and store it to avoid long chains\nasync function resolveFinalImageUrl(u: string, maxHops = 5) {\n  let cur = u, hops = 0\n  while (hops++ < maxHops) {\n    const r = await fetch(cur, { redirect: 'manual' })\n    const loc = r.headers.get('location')\n    if (loc && r.status >= 300 && r.status < 400) { cur = new URL(loc, cur).href; continue }\n    return cur\n  }\n  throw new Error('too many redirects resolving image URL')\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Configure remotePatterns with the final, direct CDN URL.","Fix upstream redirect loops/multi-hop chains.","Cache optimized results to amortize redirect resolution."],"tags":["image-optimization","network","redirect","upstream"],"analyzedSha":"0ae8c72462952df163f1b1e0726641bc5b40dc93","analyzedAt":"2026-08-06T19:44:29.143Z","schemaVersion":2},"datasetVersion":"2026-08-07T02:17:10.218Z"}