{"record":{"id":"f1ab8f1c426f257a","repo":"decolua/9router","slug":"invalid-json-body-f1ab8f","errorCode":null,"errorMessage":"Invalid JSON body","messagePattern":"Invalid JSON body","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"src/sse/handlers/search.js","lineNumber":28,"sourceCode":"import { handleSearchCore } from \"open-sse/handlers/search/index.js\";\nimport { errorResponse, unavailableResponse } from \"open-sse/utils/error.js\";\nimport { HTTP_STATUS } from \"open-sse/config/runtimeConfig.js\";\nimport * as log from \"../utils/logger.js\";\nimport { updateProviderCredentials, checkAndRefreshToken } from \"../services/tokenRefresh.js\";\nimport { handleComboChat, getComboModelsFromData } from \"open-sse/services/combo.js\";\n\n/**\n * Handle web search request for the SSE/Next.js server.\n * Provider IS the model (no model field). Mirrors handleEmbeddings auth + fallback flow.\n *\n * @param {Request} request\n */\nexport async function handleSearch(request) {\n  let body;\n  try {\n    body = await request.json();\n  } catch {\n    log.warn(\"SEARCH\", \"Invalid JSON body\");\n    return errorResponse(HTTP_STATUS.BAD_REQUEST, \"Invalid JSON body\");\n  }\n\n  const url = new URL(request.url);\n  // Accept either `provider` or `model` (UI sends `model` since provider IS the model for webSearch)\n  const providerInput = body.provider || body.model;\n  const query = body.query;\n\n  log.request(\"POST\", `${url.pathname} | ${providerInput}`);\n\n  // Log API key (masked)\n  const apiKey = extractApiKey(request);\n  if (apiKey) {\n    log.debug(\"AUTH\", `API Key: ${log.maskKey(apiKey)}`);\n  } else {\n    log.debug(\"AUTH\", \"No API key provided (local mode)\");\n  }\n","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/sse/handlers/search.js#L10-L46","documentation":"handleSearch (src/sse/handlers/search.js:28) parses the request body with request.json() inside a try/catch; any parse failure returns HTTP 400 'Invalid JSON body'. The endpoint requires a JSON document with provider/model and query fields, so a malformed body is rejected before any auth or routing happens. This is a client-side request formatting problem, not a server fault.","triggerScenarios":"POST to the /v1 search endpoint with a body that request.json() cannot parse: empty body, HTML/plain-text content, truncated JSON, or an unparseable content-type/body combination (e.g. form-encoded or raw bytes).","commonSituations":"A script sending JSON.stringify on an already-stringified string or concatenating objects; a proxy/gateway mangling or truncating the payload; forgetting 'Content-Type: application/json' while a client library then encodes the body differently; curl with -d instead of --data and no proper quoting; a file uploaded raw instead of read-and-parsed.","solutions":["Send a valid JSON object body, e.g. {\"provider\":\"exa\",\"query\":\"...\"} or {\"model\":\"...\",\"query\":\"...\"}.","Set the Content-Type: application/json header on the request.","Validate the JSON with JSON.parse (or a linter) on the client before sending.","Check for double-serialization: if your payload looks like '{\\\"provider\\\"...' when logged, un-nest the string.","Inspect the raw body actually received server-side (proxy logs) to catch middleware or gateway corruption."],"exampleFix":"// before: body is double-stringified / wrong type\nfetch(base + '/v1/search', { method: 'POST', body: JSON.stringify(JSON.stringify(payload)) });\n// after\nfetch(base + '/v1/search', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ provider: 'exa', query: 'langchain error handling' })\n});","handlingStrategy":"validation","validationCode":"const payload = { provider: 'exa', query: 'test' };\nconst bodyText = JSON.stringify(payload);\nJSON.parse(bodyText); // throws locally if you accidentally serialized twice\nif (!payload.provider || !payload.query) throw new Error('provider and query are required');","typeGuard":"function isSearchPayload(v) {\n  return typeof v === 'object' && v !== null\n    && typeof (v.provider ?? v.model) === 'string'\n    && typeof v.query === 'string' && v.query.trim().length > 0;\n}","tryCatchPattern":"let body;\ntry { body = await res.json(); } catch { throw new Error('Non-JSON response — check request encoding and endpoint'); }","preventionTips":["Always set Content-Type: application/json on POSTs to /v1 endpoints.","Log the exact body string once during integration to catch double-stringification early.","Validate payloads with a schema (zod/ajv) before sending.","Beware proxies/middleware that rewrite or compress request bodies."],"tags":["json","bad-request","http-400","request-validation"],"backgroundTag":"invalid-json-body","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}