{"record":{"id":"9ccaee16f9c43974","repo":"decolua/9router","slug":"invalid-json-body-9ccaee","errorCode":null,"errorMessage":"Invalid JSON body","messagePattern":"Invalid JSON body","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"src/sse/handlers/fetch.js","lineNumber":29,"sourceCode":"import { 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\";\nimport { assertPublicUrl } from \"@/shared/utils/ssrfGuard.js\";\n\n/**\n * Handle web fetch (URL extraction) request for the SSE/Next.js server.\n * Provider IS the model. Mirrors handleEmbeddings auth + fallback flow.\n *\n * @param {Request} request\n */\nexport async function handleFetch(request) {\n  let body;\n  try {\n    body = await request.json();\n  } catch {\n    log.warn(\"FETCH\", \"Invalid JSON body\");\n    return errorResponse(HTTP_STATUS.BAD_REQUEST, \"Invalid JSON body\");\n  }\n\n  const reqUrl = new URL(request.url);\n  // Accept either `provider` or `model` (UI sends `model` since provider IS the model for webFetch)\n  const providerInput = body.provider || body.model;\n  const targetUrl = body.url;\n  const format = body.format;\n  const maxCharacters = body.max_characters;\n\n  log.request(\"POST\", `${reqUrl.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)\");","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/sse/handlers/fetch.js#L11-L47","documentation":"HTTP 400 returned by handleFetch when request.json() throws — the body is not valid JSON (empty body, malformed JSON, or a non-JSON content type). The handler parses the body first and fails fast before any auth or validation.","triggerScenarios":"POST to the fetch endpoint with an empty body, trailing commas, unquoted keys, form-encoded or multipart bodies, or a body stringified incorrectly client-side.","commonSituations":"Using curl without --data or with unquoted JSON on the shell; forgetting Content-Type: application/json; SDK sending FormData; proxy stripping the body on redirects.","solutions":["Send a valid JSON body with Content-Type: application/json","Validate the payload with JSON.stringify before sending","If using curl, quote the JSON: -d '{\"provider\":\"x\",\"url\":\"https://...\"}'","Confirm no middleware/proxy is transforming or dropping the body"],"exampleFix":"// before\ncurl -X POST http://localhost:20128/v1/fetch -d provider=jina url=https://example.com\n// after\ncurl -X POST http://localhost:20128/v1/fetch -H 'Content-Type: application/json' -d '{\"provider\":\"jina\",\"url\":\"https://example.com\"}'","handlingStrategy":"validation","validationCode":"const payload = JSON.stringify({ provider, url });\nJSON.parse(payload); // fail fast locally before sending\nawait post('/v1/fetch', payload, { headers: { 'Content-Type': 'application/json' } });","typeGuard":"function isSerializable(x) {\n  try { JSON.stringify(x); return true; } catch { return false; }\n}","tryCatchPattern":"const res = await post('/v1/fetch', body);\nif (res.status === 400 && (await res.text()).includes('Invalid JSON body')) {\n  console.error('Request body was not parseable JSON — log the raw body and Content-Type');\n}","preventionTips":["Always JSON.stringify the body and set Content-Type: application/json","Never send FormData/form-encoded bodies to this endpoint","Test requests with curl + quoted JSON first"],"tags":["bad-request","json","validation"],"backgroundTag":"invalid-json-body","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}