{"record":{"id":"9cf9daaea48fdd42","repo":"koala73/worldmonitor","slug":"railway-relay-unavailable-for-relay-only-domain","errorCode":null,"errorMessage":"Railway relay unavailable for relay-only domain: ${hostname}","messagePattern":"Railway relay unavailable for relay-only domain: (.+?)","errorType":"http","errorClass":null,"httpStatus":502,"severity":"error","filePath":"api/rss-proxy.js","lineNumber":337,"sourceCode":"\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 {\n        response = await fetchDirect();\n      } catch (directError) {\n        if (directError instanceof RssProxyPolicyError) throw directError;\n        // A throwing relay leg here must not replace directError — a null or\n        // non-ok relay response already falls through to it below, so a thrown\n        // relay error should too, rather than becoming the reported failure.\n        let relayResponse = null;\n        try {\n          relayResponse = await fetchViaRailway(feedUrl, timeout);\n        } catch (relayError) {\n          console.error('RSS proxy relay fallback error:', feedUrl, relayError instanceof Error ? relayError.message : String(relayError));\n        }\n        response = relayResponse;\n        usedRelay = !!response;\n        if (!response) throw directError;\n      }","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/api/rss-proxy.js#L319-L355","documentation":"Hosts in RELAY_ONLY_DOMAINS (known to block Vercel edge IPs, e.g. www.who.int, kyivindependent.com) skip the direct fetch entirely and must be fetched through the Railway relay. This error means fetchViaRailway returned null — getRelayBaseUrl() is unset (it returns null with no env value) or the relay leg produced no response — so for these hosts no fetch path remains.","triggerScenarios":"The Edge deployment lacks the relay base URL env var, so getRelayBaseUrl() returns null and the throw is immediate; the Railway relay service is down, sleeping, or undeployed; the relay request for that feed failed before returning a response.","commonSituations":"Deploying Edge functions without the relay env var; renamed relay configuration keys; Railway service crashed or sleeping; local development where the private relay is unreachable.","solutions":["Set the relay base URL env var on the Vercel project so getRelayBaseUrl() returns a value","Verify the relay is alive: curl -fsS \"$RELAY_BASE_URL/health\" or curl its /rss?url=... endpoint with a known feed","Check Railway logs for the failing /rss request and redeploy the relay if it is down","Only if the host no longer blocks Vercel IPs, remove it from RELAY_ONLY_DOMAINS so the direct path with relay fallback is used"],"exampleFix":"# before — relay env unset, request to a relay-only host fails fast\nGET /api/rss-proxy?url=https://www.who.int/rss-feeds/news-xml.xml\n# 500: Railway relay unavailable for relay-only domain: www.who.int\n\n# after — configure and verify the relay, then retry\n$ vercel env set RELAY_BASE_URL https://<railway-service>.up.railway.app   # then redeploy\n$ curl -fsS \"$RELAY_BASE_URL/health\"\n$ curl 'https://<edge>/api/rss-proxy?url=https%3A%2F%2Fwww.who.int%2Frss-feeds%2Fnews-xml.xml'","handlingStrategy":"validation","validationCode":"// Before requesting a relay-only feed, confirm the relay is configured and alive\nconst RELAY_ONLY = new Set(['www.who.int', 'kyivindependent.com', /* ... */]);\nasync function relayReady() {\n  const base = process.env.RELAY_BASE_URL;   // whatever getRelayBaseUrl() reads\n  if (!base) return false;\n  try { return (await fetch(`${base}/health`)).ok; } catch { return false; }\n}\nif (RELAY_ONLY.has(host) && !(await relayReady())) {\n  return serviceUnavailable('rss relay down');   // fail fast with a clear signal\n}","typeGuard":null,"tryCatchPattern":"try { xml = await fetchFeedViaProxy(feedUrl); }\ncatch (err) {\n  if (/Railway relay unavailable/.test(err.message)) {\n    // infrastructure outage: surface 503-style degradation, queue retry — same params will keep failing\n    return markDegraded('rss-relay');\n  }\n  throw err;\n}","preventionTips":["Monitor the Railway relay with a health check and alert before it matters","Include the relay env var in deployment checklists and preview-environment templates","Track per-host success rates: relay-only hosts failing while direct hosts succeed points at the relay, not the origins"],"tags":["relay","railway","infrastructure","rss-proxy","availability"],"backgroundTag":"upstream-proxy-unavailable","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}