{"record":{"id":"f6893f404eec75c9","repo":"fatedier/frp","slug":"envelope-msg","errorCode":null,"errorMessage":"envelope.msg","messagePattern":"envelope\\.msg","errorType":"http","errorClass":"HTTPError","httpStatus":null,"severity":"error","filePath":"web/frps/src/api/http.ts","lineNumber":78,"sourceCode":"  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()\n  return text ? `?${text}` : ''\n}\n\nexport const http = {","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/web/frps/src/api/http.ts#L60-L96","documentation":"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.","triggerScenarios":"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>'}.","commonSituations":"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.","solutions":["Read the error message — it is envelope.msg verbatim from frps and names the exact problem (e.g. 'proxy already exists')","Fix the submitted payload per the message and resubmit","If the code is an auth/permission code, refresh the v2 session and check the operator's permissions","Refresh the dashboard list to reconcile stale UI state before retrying creates"],"exampleFix":"// before\nawait requestV2('/api/v2/proxy/tcp', { method: 'POST', body: cfg }) // throws 'name already exists'\n\n// after\ntry {\n  await requestV2('/api/v2/proxy/tcp', { method: 'POST', body: cfg })\n} catch (e: any) {\n  if (e?.status === 409 || /already exists/.test(e?.message)) {\n    await requestV2(`/api/v2/proxy/tcp/${cfg.name}`, { method: 'PUT', body: cfg })\n  } else throw e\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isV2HTTPError(e: unknown): e is { status: number } & Error {\n  return e instanceof Error && typeof (e as any).status === 'number'\n}","tryCatchPattern":"try {\n  await requestV2(url, { method: 'POST', body })\n} catch (e) {\n  if (isV2HTTPError(e) && e.status === 409) { await updateExisting(); return }\n  surfaceServerMessage(e) // envelope.msg is authoritative\n  throw e\n}","preventionTips":["GET-before-POST to distinguish create from update","Show the server msg verbatim in UI toasts — it names the offending field","Reconcile list state after every failed mutation"],"tags":["http","typescript","frps","api-v2","validation"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}