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

  1. Always pass a fully-qualified URL: url=https://example.com/rss.xml
  2. URL-encode the feed URL with encodeURIComponent when it carries its own query params
  3. 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

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


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)