mihomo-party-org/clash-party · error · GatewayError

transient

transient

Error message

endpoint escaped gateway origin

What it means

urlOf() builds the request URL by resolving a gateway endpoint against the gateway base URL and then verifies the resulting origin still matches the gateway's origin (the 'second line of defense'). If an endpoint value — often supplied by the server or a redirect — escapes to a different host (via backslash tricks, '@' userinfo, or encoding), this GatewayError('transient', ...) is thrown to block SSRF-style endpoint escape.

Source

Thrown at src/main/resolve/plugin/gateway.ts:43

  constructor(kind: GatewayErrorKind, message: string, status?: number) {
    super(message)
    this.name = 'GatewayError'
    this.kind = kind
    this.status = status
  }
}

interface RawResult {
  status: number
  json: Record<string, unknown> | undefined
  text: string
}

function urlOf(t: GatewayTarget, ep: keyof IGatewayEndpoints): string {
  const u = new URL(t.endpoints[ep], t.gateway)
  // 第二道防线:拼出的 URL 必须仍落在网关 origin 上(防端点逃逸到其它 host,如反斜杠/编码技巧)。
  if (u.origin !== new URL(t.gateway).origin) {
    throw new GatewayError('transient', 'endpoint escaped gateway origin')
  }
  return u.toString()
}

function lookupFor(net: GatewayNet): LookupFunction | undefined {
  return net.proxy ? undefined : (net.lookup ?? createGuardedLookup())
}

// DNS 解析失败 / 连接拒绝 / TLS 失败 → 缓存网关“不可达/已退役”信号(spec §5),交由编排层重新发现。
// 超时('Request timed out' / ETIMEDOUT)、5xx、429、SSRF/重定向/大小拦截仍按瞬时失败退避,不在此列。
const UNREACHABLE_CODES = new Set([
  'ENOTFOUND',
  'EAI_AGAIN',
  'ECONNREFUSED',
  'ECONNRESET',
  'EHOSTUNREACH',
  'ENETUNREACH',
  'EHOSTDOWN',

View on GitHub (pinned to 911e090537)

Solutions

  1. Fix the endpoint entry in your gateway target config to be a relative path (e.g. '/v1/config').
  2. If the gateway itself supplies endpoints, report/verify the gateway — a different origin means it is misbehaving or compromised.
  3. This is classified 'transient'; retrying may help only if the gateway has been fixed, otherwise the endpoint config must change.

Example fix

// before
endpoints: { config: 'https://other-host.example/config' }
// after
endpoints: { config: '/config' } // relative, stays on gateway origin
Defensive patterns

Strategy: try-catch

Validate before calling

function endpointsStayOnOrigin(t: GatewayTarget): boolean {
  const origin = new URL(t.gateway).origin
  return Object.values(t.endpoints).every(
    (ep) => { try { return new URL(ep, t.gateway).origin === origin } catch { return false } }
  )
}

Try / catch

try {
  const url = urlOf(target, 'config')
} catch (e) {
  if (e instanceof GatewayError && e.code === 'transient' && e.message === 'endpoint escaped gateway origin') {
    // reject the target config / quarantine the gateway — do not follow the escaped URL
  } else throw e
}

Prevention

When it happens

Trigger: A GatewayTarget.endpoints entry contains a value that, when fed to new URL(ep, gateway), resolves to a different origin — e.g. an absolute URL like 'https://evil.example/x', a protocol-relative '//evil.example/', or 'https://gw.example\@evil.example'.

Common situations: A misconfigured or compromised gateway returning hostile endpoint URLs, or a local config with a typo'd absolute endpoint string instead of a relative path.

Related errors


AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30). Data as JSON: /api/errors/b206423124d4a681. Report an issue: GitHub.