{"record":{"id":"50373c03d59be9fe","repo":"fatedier/frp","slug":"envelope-msg-http-response-status","errorCode":null,"errorMessage":"envelope?.msg || `HTTP ${response.status}`","messagePattern":"envelope\\?\\.msg \\|\\| `HTTP (.+?)`","errorType":"http","errorClass":"HTTPError","httpStatus":null,"severity":"error","filePath":"web/frps/src/api/http.ts","lineNumber":66,"sourceCode":"\n  return response.json()\n}\n\nasync function requestV2<T>(\n  url: string,\n  options: RequestInit = {},\n): Promise<T> {\n  const defaultOptions: RequestInit = {\n    credentials: 'include',\n  }\n\n  const response = await fetch(url, { ...defaultOptions, ...options })\n  const envelope = (await response.json().catch(() => null)) as\n    | V2Envelope<T>\n    | null\n\n  if (!response.ok) {\n    throw new HTTPError(\n      response.status,\n      response.statusText,\n      envelope?.msg || `HTTP ${response.status}`,\n    )\n  }\n\n  if (!envelope || typeof envelope.code !== 'number') {\n    throw new Error('Invalid API v2 response')\n  }\n\n  if (envelope.code >= 400) {\n    throw new HTTPError(envelope.code, envelope.msg, envelope.msg)\n  }\n\n  return envelope.data\n}\n\nexport const buildQueryString = (","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/web/frps/src/api/http.ts#L48-L84","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Read the thrown error message — it is the server's own explanation (envelope.msg), so act on it directly","For auth failures, re-login / refresh the v2 API session and retry the request","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","Check frps logs for the matching request to confirm the server-side reason"],"exampleFix":"// before\nconst res = await requestV2('/api/v2/proxy/tcp') // throws 'session expired'\n\n// after\ntry {\n  const res = await requestV2('/api/v2/proxy/tcp')\n} catch (e: any) {\n  if (e?.status === 401) {\n    await relogin()\n    return retry()\n  }\n  throw e\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isV2HTTPError(e: unknown): e is { status: number; msg?: string } & Error {\n  return e instanceof Error && typeof (e as any).status === 'number'\n}","tryCatchPattern":"try {\n  await requestV2('/api/v2/proxy/tcp', init)\n} catch (e) {\n  if (isV2HTTPError(e) && e.status === 401) { await refreshSession(); return retryOnce() }\n  showError(e instanceof Error ? e.message : String(e)) // message is envelope.msg from frps\n  throw e\n}","preventionTips":["Wrap requestV2 calls in one place and standardize on the thrown message for user-facing errors","Refresh the v2 session proactively on a timer instead of waiting for 401s","Treat bare 'HTTP nnn' (envelope fallback) as a routing/proxy misconfiguration signal"],"tags":["http","typescript","frps","api-v2","envelope"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}