{"record":{"id":"a597859090d46b0b","repo":"neondatabase/neon","slug":"failed-to-parse-json-request-e","errorCode":null,"errorMessage":"Failed to parse json request: {e}","messagePattern":"Failed to parse json request: (.+?)","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"warning","filePath":"libs/http-utils/src/json.rs","lineNumber":27,"sourceCode":"pub async fn json_request<T: for<'de> Deserialize<'de>>(\n    request: &mut Request<Body>,\n) -> Result<T, ApiError> {\n    let body = hyper::body::aggregate(request.body_mut())\n        .await\n        .context(\"Failed to read request body\")\n        .map_err(ApiError::BadRequest)?;\n\n    if body.remaining() == 0 {\n        return Err(ApiError::BadRequest(anyhow::anyhow!(\n            \"missing request body\"\n        )));\n    }\n\n    let mut deser = serde_json::de::Deserializer::from_reader(body.reader());\n\n    serde_path_to_error::deserialize(&mut deser)\n        // intentionally stringify because the debug version is not helpful in python logs\n        .map_err(|e| anyhow::anyhow!(\"Failed to parse json request: {e}\"))\n        .map_err(ApiError::BadRequest)\n}\n\n/// Parse a json request body and deserialize it to the type `T`. If the body is empty, return `T::default`.\npub async fn json_request_maybe<T: for<'de> Deserialize<'de> + Default>(\n    request: &mut Request<Body>,\n) -> Result<T, ApiError> {\n    let body = hyper::body::aggregate(request.body_mut())\n        .await\n        .context(\"Failed to read request body\")\n        .map_err(ApiError::BadRequest)?;\n\n    if body.remaining() == 0 {\n        return Ok(T::default());\n    }\n\n    let mut deser = serde_json::de::Deserializer::from_reader(body.reader());\n","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/http-utils/src/json.rs#L9-L45","documentation":"Returned as HTTP 400 BadRequest by json_request in neon's http-utils when the non-empty request body fails JSON deserialization into the endpoint's typed request struct. The error is produced by serde_path_to_error, so the message includes both the serde error and the JSON path of the offending field, and it is intentionally stringified (Display) so python-side logs stay readable. It fires for syntax errors, type mismatches, and missing required fields alike.","triggerScenarios":"POSTing {\"tenant_id\": 123} where the handler's struct wants TenantId (a stringified UUID), sending invalid JSON like a trailing comma, or omitting a required field — serde_path_to_error names the path (e.g. .tenant_id) in the message.","commonSituations":"Sending UUIDs without quotes (JSON number instead of string); null for a non-Option field; snake_case/camelCase mixups after a client library change; hand-written JSON with comments or trailing commas; version skew where the API added a required field.","solutions":["Read the message: the part after 'Failed to parse json request:' gives the serde reason plus the JSON path of the bad field","Fix that field's type/shape — quote IDs, supply required fields, remove trailing commas/comments","Compare your payload against the endpoint's request struct in the neon source (the handler's json_request::<T> names T)","If the API version changed, update the client to the current schema"],"exampleFix":"# before\ncurl -X POST .../failpoints -d '{ name: \"fp1\", actions: \"return\" }'   # unquoted keys -> 400\n\n# after\ncurl -X POST .../failpoints -H 'Content-Type: application/json' \\\n  -d '[{\"name\":\"fp1\",\"actions\":\"return\"}]'","handlingStrategy":"validation","validationCode":"// Validate the payload against the endpoint's schema client-side first:\nconst req = { tenant_id: tenantId }; // must be string UUID if the API type is TenantId\nif (!/^[0-9a-fA-F-]{36}$/.test(req.tenant_id)) throw new Error(\"tenant_id must be a UUID string\");\nawait fetch(url, { method: \"POST\", body: JSON.stringify(req) });","typeGuard":"function isTimelineCreateRequest(b) {\n  return typeof b === \"object\" && b !== null &&\n    (b.new_timeline_id === undefined || /^[0-9a-f-]{36}$/.test(b.new_timeline_id));\n}","tryCatchPattern":"// Parse the 400 body to surface the serde path to the operator:\nconst resp = await fetch(url, req);\nif (resp.status === 400) {\n  const msg = (await resp.json()).error ?? await resp.text();\n  throw new Error(`request rejected: ${msg}`); // message contains the offending JSON path\n}","preventionTips":["Stringify IDs explicitly; never let numbers become JSON numbers for UUID-typed fields","Run payloads through a JSON schema or typed client generated from the API structs","The error message names the exact JSON path — read it before changing anything else"],"tags":["neon","http-utils","json","serde","request-body","http-400"],"backgroundTag":"json-deserialization-failed","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}