{"record":{"id":"391f81fe54a6df2d","repo":"fatedier/frp","slug":"http-response-status-391f81","errorCode":null,"errorMessage":"HTTP ${response.status}","messagePattern":"HTTP \\$\\{response\\.status\\}","errorType":"http","errorClass":"HTTPError","httpStatus":null,"severity":"error","filePath":"web/frps/src/api/http.ts","lineNumber":37,"sourceCode":"\nexport interface V2Page<T> {\n  total: number\n  page: number\n  pageSize: number\n  items: T[]\n}\n\ntype QueryParamValue = string | number | boolean | null | undefined\n\nasync function request<T>(url: string, options: RequestInit = {}): Promise<T> {\n  const defaultOptions: RequestInit = {\n    credentials: 'include',\n  }\n\n  const response = await fetch(url, { ...defaultOptions, ...options })\n\n  if (!response.ok) {\n    throw new HTTPError(\n      response.status,\n      response.statusText,\n      `HTTP ${response.status}`,\n    )\n  }\n\n  // Handle empty response (e.g. 204 No Content)\n  if (response.status === 204) {\n    return {} as T\n  }\n\n  return response.json()\n}\n\nasync function requestV2<T>(\n  url: string,\n  options: RequestInit = {},\n): Promise<T> {","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/fatedier/frp/blob/6c8a8d0a97d03b44e9528d30b30c70cb9d61b405/web/frps/src/api/http.ts#L19-L55","documentation":"Generic non-2xx failure from the frps dashboard's plain fetch wrapper (request()). Any frps API call that receives a non-OK HTTP status throws an HTTPError with message 'HTTP <status>'. The frps dashboard API sits behind optional basic auth (dashboard.user/dashboard.password) and requires the dashboard port, not the bind port.","triggerScenarios":"Calling frps API endpoints (/api/serverinfo, /api/proxy/tcp, etc.) when the response status is not 2xx: 401 when dashboard basic-auth credentials are wrong or credentials:'include' cookies are missing, 404 for an endpoint that does not exist on that frps version, 500 on server-side handler errors.","commonSituations":"Accessing the API port instead of the dashboard port (bindPort vs webServer.port); dashboard.password set but the request sent no credentials; older frps version that lacks newer API routes; reverse proxy stripping Authorization headers.","solutions":["Confirm the URL uses frps webServer.port (dashboard port), not the proxy bindPort","Provide dashboard basic-auth credentials (the wrapper relies on credentials:'include' cookies from the login flow)","Check err.status in the catch: 401 → auth, 404 → wrong path or old frps, 5xx → server logs","Upgrade frps so its API surface matches the dashboard's expected endpoints"],"exampleFix":"// before\nconst info = await http.get('/api/serverinfo')\n\n// after\nconst info = await http.get('/api/serverinfo', {\n  headers: { Authorization: 'Basic ' + btoa(`${user}:${pass}`) },\n})","handlingStrategy":"try-catch","validationCode":"// Confirm the frps dashboard port responds before the first API call\nconst probe = await fetch(`${base}/api/serverinfo`, { credentials: 'include' })\nif (probe.status === 401) await login() // establish basic-auth/session first","typeGuard":"function isHTTPError(e: unknown): e is { status: number } & Error {\n  return e instanceof Error && typeof (e as any).status === 'number'\n}","tryCatchPattern":"try {\n  return await http.get<T>('/api/serverinfo')\n} catch (e) {\n  if (isHTTPError(e)) {\n    switch (e.status) {\n      case 401: return handleAuthExpired()\n      case 404: return handleWrongEndpoint(e)\n    }\n  }\n  throw e\n}","preventionTips":["Authenticate before the first data fetch and re-auth on 401 rather than letting raw HTTP errors surface","Target webServer.port (dashboard port), never the proxy bindPort","Log e.status alongside the message — 'HTTP 500' alone is diagnosable only with the number"],"tags":["http","typescript","frps","dashboard","authentication"],"backgroundTag":null,"analyzedSha":"6c8a8d0a97d03b44e9528d30b30c70cb9d61b405","analyzedAt":"2026-08-15T06:53:27.215Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}