fatedier/frp · error · HTTPError

envelope.msg

Error message

envelope.msg

What it means

Business-level error from the frps v2 API: the HTTP response was 200, but the V2Envelope's application code is >= 400. The v2 protocol carries errors inside a successful HTTP transaction, and requestV2 converts envelope.code/msg into an HTTPError so callers see a uniform error object with the server's message.

Source

Thrown at web/frps/src/api/http.ts:78

  const response = await fetch(url, { ...defaultOptions, ...options })
  const envelope = (await response.json().catch(() => null)) as
    | V2Envelope<T>
    | null

  if (!response.ok) {
    throw new HTTPError(
      response.status,
      response.statusText,
      envelope?.msg || `HTTP ${response.status}`,
    )
  }

  if (!envelope || typeof envelope.code !== 'number') {
    throw new Error('Invalid API v2 response')
  }

  if (envelope.code >= 400) {
    throw new HTTPError(envelope.code, envelope.msg, envelope.msg)
  }

  return envelope.data
}

export const buildQueryString = (
  params: Record<string, QueryParamValue>,
): string => {
  const query = new URLSearchParams()
  for (const [key, value] of Object.entries(params)) {
    if (value === null || value === undefined) continue
    query.append(key, String(value))
  }
  const text = query.toString()
  return text ? `?${text}` : ''
}

export const http = {

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Read the error message — it is envelope.msg verbatim from frps and names the exact problem (e.g. 'proxy already exists')
  2. Fix the submitted payload per the message and resubmit
  3. If the code is an auth/permission code, refresh the v2 session and check the operator's permissions
  4. Refresh the dashboard list to reconcile stale UI state before retrying creates

Example fix

// before
await requestV2('/api/v2/proxy/tcp', { method: 'POST', body: cfg }) // throws 'name already exists'

// after
try {
  await requestV2('/api/v2/proxy/tcp', { method: 'POST', body: cfg })
} catch (e: any) {
  if (e?.status === 409 || /already exists/.test(e?.message)) {
    await requestV2(`/api/v2/proxy/tcp/${cfg.name}`, { method: 'PUT', body: cfg })
  } else throw e
}
Defensive patterns

Strategy: try-catch

Type guard

function isV2HTTPError(e: unknown): e is { status: number } & Error {
  return e instanceof Error && typeof (e as any).status === 'number'
}

Try / catch

try {
  await requestV2(url, { method: 'POST', body })
} catch (e) {
  if (isV2HTTPError(e) && e.status === 409) { await updateExisting(); return }
  surfaceServerMessage(e) // envelope.msg is authoritative
  throw e
}

Prevention

When it happens

Trigger: Any /api/v2 operation the server rejects after receiving it: creating a proxy whose name already exists, submitting an invalid config payload, or performing an operation not allowed for the current session — the server replies 200 with {code: 4xx/5xx, msg: '<reason>'}.

Common situations: Duplicate proxy names when adding via the dashboard; validation failures in submitted proxy/visitor configs; permission checks enforced at the application layer rather than HTTP layer; stale UI state resubmitting an already-applied change.

Related errors


AI-assisted analysis of fatedier/frp@6c8a8d0a97 (2026-08-15). Data as JSON: /api/errors/f6893f404eec75c9. Report an issue: GitHub.