koala73/worldmonitor · error · RssProxyPolicyError
Redirect to disallowed domain
Error message
Redirect to disallowed domain
What it means
The proxy follows redirects manually (redirect: 'manual') and re-applies the domain allowlist to every Location header, with the same www-prefix normalization as the initial check. This RssProxyPolicyError (403) means the redirect chain left the allowlisted feed domains — the guard that prevents the proxy from being used as an open relay / SSRF vector.
Solutions
- Trace the chain: curl -sIL '<feed-url>' | grep -i '^location'
- Request the final destination URL directly through the proxy instead of the redirecting one
- If the target is a domain you intentionally serve, add it to the allowlist in api/_rss-allowed-domain-match.js (shared with the CI validator so both stay in sync)
- If the chain looks unstable or suspicious, drop the feed from the client list
Example fix
# before $ curl -sIL https://feeds.example.com/rss | grep -i '^location' Location: https://cdn.example-cdn.com/feed.xml # not allowlisted -> 403 Redirect to disallowed domain # after — add the redirect target to the shared allowlist in api/_rss-allowed-domain-match.js # (e.g. 'cdn.example-cdn.com'), or call the proxy with the final URL: GET /api/rss-proxy?url=https://cdn.example-cdn.com/feed.xml
Defensive patterns
Strategy: validation
Validate before calling
// Resolve the redirect chain client-side and confirm every hop stays allowlisted
async function finalAllowedUrl(feedUrl, isAllowed) {
let u = new URL(feedUrl);
for (let i = 0; i < 3; i++) {
const res = await fetch(u, { redirect: 'manual' });
const loc = res.headers.get('location');
if (!loc) return u.href;
u = new URL(loc, u);
if (!isAllowed(u.hostname)) throw new Error(`Chain leaves allowlist at ${u.hostname}`);
}
throw new Error('Too many redirects');
} Try / catch
try {
const xml = await fetchFeedViaProxy(feedUrl);
} catch (err) {
if (/Redirect to disallowed domain/.test(err.message)) {
// policy stop: resolve the chain, then either pin the final URL or extend the allowlist
return pinFinalUrl(feedUrl);
}
throw err;
} Prevention
- Store the final redirect target in feed configs instead of legacy redirecting URLs
- Extend the shared allowlist (api/_rss-allowed-domain-match.js) whenever adding a feed whose chain crosses domains
- Periodically re-run the CI feed validator to catch chains that drift off the allowlist
When it happens
Trigger: An allowlisted host 301/302s to a different domain (redirect service, publisher migration, unrelated CDN); region-based mirrors chosen per request; the allowlist containing only one of the domains the chain passes through.
Common situations: A publisher moves its feed and the legacy URL redirects forever; chains that combine http→https and host changes; adding feeds by their old feedburner-style URLs.
Related errors
- callbackUrl hostname is a blocked metadata endpoint
- callbackUrl resolves to a private/reserved address
- callbackUrl resolves to a private/reserved address
- Invalid returnUrl: must use a trusted worldmonitor.app…
- serverUrl hostname is blocked
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-08-21).
Data as JSON: /api/errors/a68f5a2d0f830383.
Report an issue: GitHub.
Appendix: source
Thrown at api/rss-proxy.js:237
return new URL(feedUrl).hostname === 'news.google.com';
} catch {
return false;
}
}
function assertHttpProtocol(url, message = 'URL protocol not allowed', status = 400) {
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new RssProxyPolicyError(message, status);
}
}
function assertAllowedRedirect(url) {
assertHttpProtocol(url, 'Redirect protocol not allowed', 403);
// Apply the same www-normalization as the initial domain check so that
// canonical redirects (e.g. apex -> www) are not incorrectly rejected when
// only one form is in the allowlist.
if (!isAllowedDomain(url.hostname)) {
throw new RssProxyPolicyError('Redirect to disallowed domain');
}
}
export default async function handler(req, ctx) {
const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');
if (isDisallowedOrigin(req)) {
return jsonResponse({ error: 'Origin not allowed' }, 403, corsHeaders);
}
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
if (req.method !== 'GET') {
return jsonResponse({ error: 'Method not allowed' }, 405, corsHeaders);
}
View on GitHub (pinned to e586b8b4b8)