agalwood/Motrix · error · HttpError

plugin.http.scheme_not_allowed

plugin.http.scheme_not_allowed

Error message

URL scheme '${parsed.protocol}' is not allowed; use http: or https:

What it means

The HTTP capability only permits http: and https: URLs. parseUrl() accepts any syntactically valid URL (including file:, ftp:, data:), then checkScheme() rejects anything not in ALLOWED_SCHEMES. This is an SSRF / local-file-access guard around plugin-initiated network calls, and it is re-applied on every redirect hop so a Location response cannot escape the allowlist.

Source

Thrown at src/core/plugin/capabilities/http.ts:142

// Shared dispatcher for non-proxied requests; per-call ProxyAgent is built
// fresh when `opts.proxy` is provided.
const sharedAgent = new Agent()

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function parseUrl(raw: string): URL {
  try {
    return new URL(raw)
  } catch {
    throw new HttpError('plugin.http.invalid_url', `Invalid URL: ${raw}`)
  }
}

function checkScheme(parsed: URL): void {
  if (!ALLOWED_SCHEMES.has(parsed.protocol)) {
    throw new HttpError(
      'plugin.http.scheme_not_allowed',
      `URL scheme '${parsed.protocol}' is not allowed; use http: or https:`
    )
  }
}

function clampTimeout(ms: number | undefined, defaultMs: number): number {
  // Reject non-finite values (NaN/Infinity) — http.get/post reach this with
  // unvalidated opts, and Math.min(MAX, NaN) is NaN, disabling the timeout.
  if (ms === undefined || !Number.isFinite(ms)) return defaultMs
  return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, ms))
}

function clampMaxBody(
  requested: number | undefined,
  defaultBytes: number
): number {
  // Only accept a finite, positive request. A NaN (Math.min(NaN, HARD) = NaN)

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Normalize URLs to http:// or https:// before calling the http capability.
  2. For local files, use the fs/storage capability instead of file:// over http.
  3. Sanitize untrusted URL input (strip/replace non-http protocols) at the plugin boundary.
  4. If a redirect target is the cause, set redirect:'manual' and inspect each Location yourself.

Example fix

// before
await http.request({ url: 'file:///etc/passwd', responseType: 'text' })

// after
await http.request({ url: 'https://example.com/data', responseType: 'text' })
Defensive patterns

Strategy: validation

Validate before calling

import { ALLOWED_SCHEMES } from '...'
function assertHttpUrl(raw: string): URL {
  const u = new URL(raw)
  if (u.protocol !== 'http:' && u.protocol !== 'https:') {
    throw new Error(`refusing non-http(s) URL: ${u.protocol}`)
  }
  return u
}
// at call site:
assertHttpUrl(url)
await http.request({ url, responseType: 'json' })

Type guard

function isHttpUrl(raw: string): raw is `${'http'|'https'}://${string}` {
  try {
    const u = new URL(raw)
    return u.protocol === 'http:' || u.protocol === 'https:'
  } catch { return false }
}

Try / catch

try {
  await http.request({ url, responseType: 'json' })
} catch (e) {
  if (e instanceof HttpError && e.code === 'plugin.http.scheme_not_allowed') {
    // log and reject the untrusted input; do not retry as-is
  } else throw e
}

Prevention

When it happens

Trigger: Calling http.get/post/request with a URL whose protocol is not http:/https: (e.g. 'file:///etc/passwd', 'ftp://host', 'data:text/plain,x'). Also triggered when a 3xx response's Location header points to a non-allowed scheme, because checkScheme() runs on every hop at http.ts:412.

Common situations: Plugin builds a URL from user/config input without normalizing; plugin reads a local-resource path formatted as file://; CI/runtime upgrades tighten the allowlist; a server or short-link service redirects to a non-http scheme.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/f2efba36178d3f32. Report an issue: GitHub.