{"record":{"id":"aad3a20f4830b480","repo":"alan2207/bulletproof-react","slug":"message","errorCode":null,"errorMessage":"${message}","messagePattern":"\\$\\{message\\}","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/nextjs-app/src/lib/api-client.ts","lineNumber":95,"sourceCode":"      ...headers,\n      ...(cookieHeader ? { Cookie: cookieHeader } : {}),\n    },\n    body: body ? JSON.stringify(body) : undefined,\n    credentials: 'include',\n    cache,\n    next,\n  });\n\n  if (!response.ok) {\n    const message = (await response.json()).message || response.statusText;\n    if (typeof window !== 'undefined') {\n      useNotifications.getState().addNotification({\n        type: 'error',\n        title: 'Error',\n        message,\n      });\n    }\n    throw new Error(message);\n  }\n\n  return response.json();\n}\n\nexport const api = {\n  get<T>(url: string, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'GET' });\n  },\n  post<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'POST', body });\n  },\n  put<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'PUT', body });\n  },\n  patch<T>(url: string, body?: any, options?: RequestOptions): Promise<T> {\n    return fetchApi<T>(url, { ...options, method: 'PATCH', body });\n  },","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/alan2207/bulletproof-react/blob/9506629ed003a561c6627735480cce4994244bb4/apps/nextjs-app/src/lib/api-client.ts#L77-L113","documentation":"This is the api client's fetchApi wrapper in apps/nextjs-app/src/lib/api-client.ts:95 firing on any non-2xx response. It first surfaces the server's message via the notifications store (browser only), then throws a plain Error whose message is `response.message` from the JSON body or `response.statusText` as fallback. All calls through `api.get/post/put/patch/delete` reject this way, so callers must catch it to handle API failures.","triggerScenarios":"Any request to `${env.API_URL}<url>` returning a non-ok status: 401 from an expired/missing auth cookie, 400 from Zod validation on the server, 404 for a missing resource, or 500 from a thrown server error. Also occurs when API_URL points to the wrong host/backend so every request 404s/502s, or when the mock API (MSW) is disabled but no real backend is running.","commonSituations":"Sitting idle until the auth cookie/JWT expires and then every mutation 401s; the API server not running locally while ENABLE_API_MOCKING=false; a misconfigured API_URL in .env.local pointing at a stale deployment; server-side Zod schema stricter than client form validation causing 400s.","solutions":["Read error.message — it mirrors the server's response message — and fix the underlying request (auth, payload, URL) that produced the non-2xx status.","If it's a 401, your session expired: refresh/re-authenticate and retry; the API client relies on HttpOnly cookies, so check that credentials:'include' requests actually carry them.","Verify API_URL in apps/nextjs-app/.env.local points to a running backend (or set NEXT_PUBLIC_ENABLE_API_MOCKING=true to use MSW mocks).","In React Query consumers, handle the rejection via the mutation/query error callback or an ErrorBoundary instead of letting it bubble uncaught."],"exampleFix":"// before\nawait api.post('/discussions', { body: payload }); // uncaught Error on 4xx/5xx\n\n// after\ntry {\n  const discussion = await api.post('/discussions', { body: payload });\n} catch (e) {\n  // message is the server-provided message or statusText\n  console.error((e as Error).message);\n}","handlingStrategy":"try-catch","validationCode":"// Validate payload with Zod before calling the API to avoid 400s\nimport { Schema } from './schema';\nconst parsed = Schema.safeParse(payload);\nif (!parsed.success) {\n  // surface form errors instead of hitting the API\n  console.error(parsed.error.flatten().fieldErrors);\n} else {\n  await api.post('/discussions', { body: parsed.data });\n}","typeGuard":"import { ZodError } from 'zod';\nconst isApiError = (e: unknown): e is Error & { message: string } =>\n  e instanceof Error && e.message.length > 0;","tryCatchPattern":"try {\n  const data = await api.get('/discussions');\n} catch (error) {\n  // message mirrors the server response message (or statusText)\n  showToast((error as Error).message);\n  // optionally inspect status if you extend fetchApi to attach it\n}","preventionTips":["Wrap every api.* call in React Query mutations/queries and handle errors via onError callbacks, not bare awaits.","Keep client-side Zod schemas in sync with server schemas to prevent 400 validation responses.","Verify API_URL and backend health before debugging deep call stacks — most blanket failures are a wrong base URL or dead server.","Attach response.status to the thrown Error in fetchApi so callers can branch on 401 vs 404 vs 500."],"tags":["http","api-client","fetch","error-handling"],"backgroundTag":"http-request-failed","analyzedSha":"9506629ed003a561c6627735480cce4994244bb4","analyzedAt":"2026-08-27T06:11:26.302Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}