fatedier/frp · error · HTTPError

envelope?.msg || `HTTP ${response.status}`

Error message

envelope?.msg || `HTTP ${response.status}`

What it means

Failure from the frps v2 API client (requestV2). It reads the response body as a V2Envelope even when HTTP status is not OK, and prefers the server-supplied envelope.msg as the error message, falling back to 'HTTP <status>' only when the body is not valid JSON. This surfaces the real server error text (e.g. 'token in login doesn't match') instead of a bare status code.

Source

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

  return response.json()
}

async function requestV2<T>(
  url: string,
  options: RequestInit = {},
): Promise<T> {
  const defaultOptions: RequestInit = {
    credentials: 'include',
  }

  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 = (

View on GitHub (pinned to 6c8a8d0a97)

Solutions

  1. Read the thrown error message — it is the server's own explanation (envelope.msg), so act on it directly
  2. For auth failures, re-login / refresh the v2 API session and retry the request
  3. If the message is the bare 'HTTP nnn' fallback, the body was not JSON — check whether a proxy or wrong port is intercepting the request
  4. Check frps logs for the matching request to confirm the server-side reason

Example fix

// before
const res = await requestV2('/api/v2/proxy/tcp') // throws 'session expired'

// after
try {
  const res = await requestV2('/api/v2/proxy/tcp')
} catch (e: any) {
  if (e?.status === 401) {
    await relogin()
    return retry()
  }
  throw e
}
Defensive patterns

Strategy: try-catch

Type guard

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

Try / catch

try {
  await requestV2('/api/v2/proxy/tcp', init)
} catch (e) {
  if (isV2HTTPError(e) && e.status === 401) { await refreshSession(); return retryOnce() }
  showError(e instanceof Error ? e.message : String(e)) // message is envelope.msg from frps
  throw e
}

Prevention

When it happens

Trigger: Calling /api/v2/* endpoints on frps when the server rejects the request with a non-2xx status: expired or missing session/JWT for v2 API auth, requesting a resource ID that does not exist, or an invalid parameter — and the server responds with a JSON envelope whose msg carries the reason.

Common situations: v2 API session expired while the dashboard stayed open; token authentication misconfigured between dashboard and frps; mixing v1 and v2 endpoint paths so the server returns an HTML/empty error body (then the fallback 'HTTP <status>' appears).

Related errors


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