{"record":{"id":"dcc91d8c0cf296a7","repo":"koala73/worldmonitor","slug":"too-many-redirects","errorCode":null,"errorMessage":"Too many redirects","messagePattern":"Too many redirects","errorType":"http","errorClass":"RssProxyPolicyError","httpStatus":502,"severity":"error","filePath":"api/rss-proxy.js","lineNumber":169,"sourceCode":"      let currentUrl = parsedUrl;\n\n      for (let redirectCount = 0; redirectCount <= MAX_DIRECT_REDIRECTS; redirectCount += 1) {\n        const response = await fetchWithTimeout(currentUrl.href, {\n          headers: DIRECT_FETCH_HEADERS,\n          redirect: 'manual',\n        }, timeout);\n\n        if (!DIRECT_REDIRECT_STATUSES.has(response.status)) {\n          return response;\n        }\n\n        const location = response.headers.get('location');\n        if (!location) {\n          return response;\n        }\n\n        if (redirectCount === MAX_DIRECT_REDIRECTS) {\n          throw new RssProxyPolicyError('Too many redirects', 502);\n        }\n\n        const redirectUrl = new URL(location, currentUrl.href);\n        assertAllowedRedirect(redirectUrl);\n        currentUrl = redirectUrl;\n      }\n    };\n\n    let response;\n    let usedRelay = false;\n\n    if (isRelayOnly) {\n      // Skip direct fetch entirely — these domains block Vercel IPs\n      response = await fetchViaRailway(feedUrl, timeout);\n      usedRelay = !!response;\n      if (!response) throw new Error(`Railway relay unavailable for relay-only domain: ${hostname}`);\n    } else {\n      try {","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/api/rss-proxy.js#L151-L187","documentation":"Thrown by the direct-fetch redirect loop in the RSS proxy when the redirect chain exceeds MAX_DIRECT_REDIRECTS (3). The proxy manually follows redirects (to apply protocol and domain checks on each hop) and after 3 redirects it stops and throws RssProxyPolicyError with HTTP status 502. This prevents infinite redirect loops and bounds latency.","triggerScenarios":"An RSS feed URL that redirects more than 3 times before reaching a non-redirect (200/4xx/5xx) response — e.g. a feed that bounces between http and https, between apex and www, through a CDN chain, or a misconfigured redirect loop (A->B->A). Only 301/302/303/307/308 statuses count as redirects (DIRECT_REDIRECT_STATUSES).","commonSituations":"A feed behind multiple CDN layers (origin -> CDN -> www); a feed with an http-to-https redirect combined with an apex-to-www redirect (2 hops) plus one more; a genuine redirect loop from a misconfigured server; a feed that moved and chains through several old URLs.","solutions":["Find the final canonical feed URL (follow the redirects manually in a browser or with curl -L) and use that URL directly to avoid the chain.","If the domain consistently requires many redirects, add it to RELAY_ONLY_DOMAINS so the proxy uses the Railway relay (which handles redirects differently) instead of the bounded direct path.","If the feed has a redirect loop (same URLs cycling), report it to the feed provider — this is a server misconfiguration.","Increase MAX_DIRECT_REDIRECTS only if you understand the latency and loop-risk tradeoffs (not recommended for an edge function)."],"exampleFix":"// before — long redirect chain triggers the limit\nGET /api/rss-proxy?url=http://old.feeds.example.com/news.xml\n// after — use the final canonical URL directly\nGET /api/rss-proxy?url=https://cdn.feeds.example.com/news.xml","handlingStrategy":"try-catch","validationCode":"// Pre-resolve the feed URL to find its final canonical form\nasync function resolveCanonicalFeedUrl(feedUrl) {\n  let url = feedUrl;\n  for (let i = 0; i < 5; i++) { // one more than MAX_DIRECT_REDIRECTS to detect the issue\n    const res = await fetch(url, { method: 'HEAD', redirect: 'manual' });\n    const location = res.headers.get('location');\n    if (!location || ![301,302,303,307,308].includes(res.status)) return url;\n    url = new URL(location, url).href;\n  }\n  throw new Error(`Feed URL exceeds redirect limit; canonical URL: ${url}`);\n}","typeGuard":"function isTooManyRedirectsError(e: unknown): e is { status: number } & Error {\n  return e instanceof Error && (e as any).name === 'RssProxyPolicyError' && e.message === 'Too many redirects';\n}","tryCatchPattern":"try {\n  const feed = await fetch('/api/rss-proxy?url=' + encodeURIComponent(feedUrl));\n} catch (e) {\n  if (isTooManyRedirectsError(e)) {\n    // Resolve the canonical URL and use it directly\n    const canonical = await resolveCanonicalFeedUrl(feedUrl);\n    const feed = await fetch('/api/rss-proxy?url=' + encodeURIComponent(canonical));\n  } else throw e;\n}","preventionTips":["Find and store the final canonical feed URL instead of relying on the proxy to follow long chains.","Add feeds with persistent redirect issues to RELAY_ONLY_DOMAINS to use the Railway relay path.","Monitor for redirect loops — they indicate server misconfiguration on the feed origin."],"tags":["rss-proxy","redirect","edge-function","loop-protection"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}