{"record":{"id":"a2aac7ed01e4948c","repo":"fatedier/frp","slug":"invalid-api-v2-response","errorCode":null,"errorMessage":"Invalid API v2 response","messagePattern":"Invalid API v2 response","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/frps/src/api/http.ts","lineNumber":74,"sourceCode":"  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 = (\n  params: Record<string, QueryParamValue>,\n): string => {\n  const query = new URLSearchParams()\n  for (const [key, value] of Object.entries(params)) {\n    if (value === null || value === undefined) continue\n    query.append(key, String(value))\n  }\n  const text = query.toString()","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/web/frps/src/api/http.ts#L56-L92","documentation":"Thrown by requestV2 in the frps dashboard when the HTTP request succeeded (2xx) but the body cannot be interpreted as a V2Envelope — i.e. it is null, not JSON, or lacks a numeric 'code' field. The v2 client requires every response to be shaped {code, msg, data}; anything else is treated as a protocol violation rather than a server rejection.","triggerScenarios":"Calling a /api/v2/* URL on a server that answers with a non-envelope body: an frps version without v2 API support returning plain JSON or HTML, a reverse proxy serving an HTML error/login page with 200, or an empty body from a misrouted request.","commonSituations":"Dashboard frontend newer than the frps binary it points at (v2 endpoints missing); a gateway/proxy in front of frps rewriting responses; pointing the v2 client at a v1-only endpoint path; CORS middleware returning its own 200 response.","solutions":["Verify the target frps binary actually implements /api/v2 (check version and release notes); upgrade frps or use v1 endpoints","curl the exact /api/v2 URL and confirm the body is {\"code\":...,\"msg\":...,\"data\":...}","Remove intermediate proxies that rewrite the response body, or make them pass it through untouched","Ensure the URL path and port match the frps API surface exactly"],"exampleFix":"// before\nconst data = await requestV2('/api/v2/serverinfo') // Invalid API v2 response\n\n// after: guard the response shape at the call site\nconst resp = await fetch('/api/v2/serverinfo', { credentials: 'include' })\nconst body = await resp.json().catch(() => null)\nif (!body || typeof body.code !== 'number') {\n  throw new Error(`frps has no v2 API at this endpoint (got: ${JSON.stringify(body)?.slice(0, 80)})`)\n}","handlingStrategy":"validation","validationCode":"// Probe v2 API support before using v2 endpoints\nasync function v2Supported(base: string): Promise<boolean> {\n  const r = await fetch(`${base}/api/v2/serverinfo`, { credentials: 'include' })\n  const body = await r.json().catch(() => null)\n  return !!body && typeof body.code === 'number'\n}","typeGuard":"function isV2Envelope(v: unknown): v is { code: number; msg: string; data: unknown } {\n  return typeof v === 'object' && v !== null && typeof (v as any).code === 'number'\n}","tryCatchPattern":"try {\n  return await requestV2<T>(url)\n} catch (e) {\n  if (e instanceof Error && e.message === 'Invalid API v2 response') {\n    throw new Error('frps at this address does not speak the v2 API; upgrade frps or check the URL')\n  }\n  throw e\n}","preventionTips":["Feature-detect the v2 envelope shape once at startup and disable v2 UI when absent","Never front frps with a proxy that rewrites 2xx bodies","Pin dashboard and frps to the same release line"],"tags":["http","typescript","frps","api-v2","schema-mismatch"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}