koala73/worldmonitor · error · RssProxyPolicyError
URL protocol not allowed
Error message
URL protocol not allowed
What it means
api/rss-proxy.js is a feed-fetching endpoint with an SSRF guard: before any network call it parses the `url` query parameter and assertHttpProtocol rejects every scheme except http: and https: with HTTP 400. A scheme-less value like `example.com/feed.xml` also lands here because WHATWG URL parsing yields the pseudo-protocol `example.com:`.
Solutions
- Always pass a fully-qualified URL: url=https://example.com/rss.xml
- URL-encode the feed URL with encodeURIComponent when it carries its own query params
- Fix the upstream caller that strips or double-encodes the scheme
Example fix
# before GET /api/rss-proxy?url=example.com/feed.xml # protocol becomes 'example.com:' -> 400 GET /api/rss-proxy?url=ftp://example.com/feed.xml # non-http scheme -> 400 # after GET /api/rss-proxy?url=https%3A%2F%2Fexample.com%2Ffeed.xml
Defensive patterns
Strategy: validation
Validate before calling
// Client-side guard before calling the RSS proxy
function validFeedParam(raw) {
let u;
try { u = new URL(raw); } catch { return false; }
return u.protocol === 'http:' || u.protocol === 'https:';
}
if (!validFeedParam(feedUrl)) throw new Error(`Feed URL must be absolute http(s): ${feedUrl}`); Try / catch
try {
const res = await fetch(`/api/rss-proxy?url=${encodeURIComponent(feedUrl)}`);
if (res.status === 400) {/* fix the caller's URL construction; do not retry unchanged */}
} catch (e) { /* network-level handling */ } Prevention
- Construct proxy URLs only from stored, canonical https:// feed addresses
- Always encodeURIComponent the url parameter
- Run feed configs through a validator (the repo's scripts/validate-rss-feeds.mjs shares the same rules)
When it happens
Trigger: Passing a URL without a scheme (url=example.com/rss.xml); passing feed://, ftp://, data: or file: URLs; a caller double-encoding the parameter so the protocol portion is mangled.
Common situations: Feed URLs copied from reader apps that use feed://; hand-built query strings that omit https://; test fixtures passing bare hostnames.
Related errors
- Redirect to disallowed domain
- Too many redirects
- UNSAFE_SOURCE_URL
- callbackUrl DNS resolution returned no addresses
- callbackUrl hostname is a blocked metadata endpoint
AI-assisted analysis of koala73/worldmonitor@e586b8b4b8 (2026-08-21).
Data as JSON: /api/errors/29002455166492ae.
Report an issue: GitHub.
Appendix: source
Thrown at api/rss-proxy.js:227
}),
}, timeoutMs);
}
// Allowlist + match predicate live in api/_rss-allowed-domain-match.js
// (shared with scripts/validate-rss-feeds.mjs --ci so the SSRF guard runs
// identically in the Edge handler and the build-time validator).
function isGoogleNewsFeedUrl(feedUrl) {
try {
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);View on GitHub (pinned to e586b8b4b8)