{"record":{"id":"d269cb32c2dbcffc","repo":"neondatabase/neon","slug":"missing-request-body","errorCode":null,"errorMessage":"missing request body","messagePattern":"missing request body","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"warning","filePath":"libs/http-utils/src/json.rs","lineNumber":18,"sourceCode":"use anyhow::Context;\nuse bytes::Buf;\nuse hyper::{Body, Request, Response, StatusCode, header};\nuse serde::{Deserialize, Serialize};\n\nuse super::error::ApiError;\n\n/// Parse a json request body and deserialize it to the type `T`.\npub 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","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/http-utils/src/json.rs#L1-L36","documentation":"Returned as HTTP 400 BadRequest by json_request in neon's http-utils when the request body aggregated to zero bytes. json_request is the standard body parser for every JSON POST/PUT endpoint (configure, timeline create, failpoints, etc.), and it deliberately treats an empty body as a client error instead of attempting deserialization. Note that endpoints that accept an empty body use json_request_maybe instead.","triggerScenarios":"POST /configure or POST /failpoints with Content-Length: 0 (or a fully drained/chunked-empty body); curl invoked without -d; a client that serializes an empty object to nothing; a proxy stripping the body.","commonSituations":"Forgetting the -d/--data flag in curl; HTTP client configured with a body but a redirect (301/302) caused it to be dropped and re-sent as GET-with-no-body; JSON.stringify of undefined producing nothing; misconfigured gateway between client and service.","solutions":["Send an actual JSON body, even if empty object {} where the endpoint accepts it: curl -d '{}' or the equivalent in your client","Check for hops (proxies, redirects) that drop the body, and confirm Content-Type: application/json is accompanied by a non-zero body","If the endpoint legitimately allows no body, use its variant that calls json_request_maybe (or pick the API route documented as bodyless)"],"exampleFix":"# before\ncurl -X POST http://localhost:9898/v1/tenant/aaa/timeline   # no -d -> 400 missing request body\n\n# after\ncurl -X POST http://localhost:9898/v1/tenant/aaa/timeline \\\n  -H 'Content-Type: application/json' -d '{\"new_timeline_id\":\"...\"}'","handlingStrategy":"validation","validationCode":"// Never issue a body-less JSON POST:\nconst body = JSON.stringify(payload ?? {});\nawait fetch(url, { method: \"POST\", headers: { \"Content-Type\": \"application/json\" }, body });","typeGuard":"function hasJsonBody(body) { return body != null && body.length > 0; }","tryCatchPattern":null,"preventionTips":["Always pass -d / a body with JSON POSTs, even if just {}","Beware redirects: re-issue POSTs with the body after following redirects manually","Log request sizes client-side to catch proxies stripping bodies"],"tags":["neon","http-utils","json","request-body","http-400"],"backgroundTag":"empty-request-body","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}