ruvnet/ruflo · error · HttpFetchValidationError

FORBIDDEN_PROTOCOL

FORBIDDEN_PROTOCOL

Error message

protocol ${parsed.protocol} not allowed (only http: and https:)

What it means

HttpFetchValidationError with code FORBIDDEN_PROTOCOL, thrown by validateUrl() (http-fetch-tools.ts:55) when the URL parses but its scheme is anything other than http: or https:. The http_fetch tool is an internet fetcher, not a local file or arbitrary-scheme reader, so file://, ftp://, data:, ws:, chrome:, and every other protocol are refused before any connection is attempted.

Solutions

  1. Serve the resource over http(s) — for local files, run a local HTTP server and fetch http://127.0.0.1:PORT (that then hits the PRIVATE_ADDRESS guard, so set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 if intended)
  2. Read local files with your own code (fs) instead of http_fetch — it is not a file reader
  3. Replace ftp:// sources with an https mirror

Example fix

// before
await callTool('http_fetch', { url: 'file:///home/me/notes.md' });
// throws: protocol file: not allowed (FORBIDDEN_PROTOCOL)

// after: read local files outside the fetch tool
import { readFile } from 'node:fs/promises';
const content = await readFile('/home/me/notes.md', 'utf8');
Defensive patterns

Strategy: validation

Validate before calling

function isHttpUrl(raw: string): boolean {
  try {
    const proto = new URL(raw).protocol.toLowerCase();
    return proto === 'http:' || proto === 'https:';
  } catch { return false; }
}
if (!isHttpUrl(url)) throw new TypeError('only absolute http/https URLs are fetchable');

Try / catch

try {
  await callTool('http_fetch', { url });
} catch (e) {
  if (e instanceof HttpFetchValidationError && e.code === 'FORBIDDEN_PROTOCOL' && url.startsWith('file:')) {
    // fall back to local file reading outside the fetch tool
    return readFile(fileUrlToPath(url), 'utf8');
  }
  throw e;
}

Prevention

When it happens

Trigger: http_fetch with 'file:///etc/hostname', 'file:///home/user/report.html', 'ftp://legacy-host/pub/data.csv', or 'data:text/html,...'. The protocol comparison is lowercased, so casing is not the issue — only http and https pass.

Common situations: Trying to read a local file through the fetch tool; legacy ftp links from old documentation; expecting fetch()-like tolerance (browser fetch also rejects file://, but users try anyway); attempting data: URLs for inline payloads.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/523b891fab58a0bd. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/http-fetch-tools.ts:55

    this.name = 'HttpFetchValidationError';
  }
}

/**
 * Decide whether the URL is permitted under the default secure-by-default
 * allowlist. Block file://, ftp://, RFC-1918 private addresses, loopback,
 * link-local — unless CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 is set.
 */
export function validateUrl(rawUrl: string): URL {
  let parsed: URL;
  try {
    parsed = new URL(rawUrl);
  } catch {
    throw new HttpFetchValidationError(`invalid URL: ${rawUrl}`, 'INVALID_URL');
  }
  const proto = parsed.protocol.toLowerCase();
  if (proto !== 'http:' && proto !== 'https:') {
    throw new HttpFetchValidationError(
      `protocol ${parsed.protocol} not allowed (only http: and https:)`,
      'FORBIDDEN_PROTOCOL',
    );
  }
  const host = parsed.hostname.toLowerCase();
  const allowPrivate = process.env.CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE === '1';
  if (!allowPrivate && isPrivateOrLoopback(host)) {
    throw new HttpFetchValidationError(
      `host ${host} is loopback/private/link-local; set CLAUDE_FLOW_HTTP_FETCH_ALLOW_PRIVATE=1 to override`,
      'PRIVATE_ADDRESS',
    );
  }
  return parsed;
}

function isPrivateOrLoopback(host: string): boolean {
  if (host === 'localhost' || host === 'localhost.localdomain') return true;
  // IPv6 loopback

View on GitHub (pinned to fa13ee4ad6)