{"record":{"id":"53de1fdde52961aa","repo":"neondatabase/neon","slug":"unexpected-request-body","errorCode":null,"errorMessage":"Unexpected request body","messagePattern":"Unexpected request body","errorType":"http","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"libs/http-utils/src/request.rs","lineNumber":109,"sourceCode":"            v.parse().map_err(|e| {\n                ApiError::BadRequest(anyhow!(\"cannot parse query param {param_name}: {e}\"))\n            })\n        })\n        .transpose()\n}\n\npub fn must_parse_query_param<E: fmt::Display, T: FromStr<Err = E>>(\n    request: &Request<Body>,\n    param_name: &str,\n) -> Result<T, ApiError> {\n    parse_query_param(request, param_name)?.ok_or_else(|| {\n        ApiError::BadRequest(anyhow!(\"no {param_name} specified in query parameters\"))\n    })\n}\n\npub async fn ensure_no_body(request: &mut Request<Body>) -> Result<(), ApiError> {\n    match request.body_mut().data().await {\n        Some(_) => Err(ApiError::BadRequest(anyhow!(\"Unexpected request body\"))),\n        None => Ok(()),\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_get_query_param_duplicate() {\n        let req = Request::builder()\n            .uri(\"http://localhost:12345/testuri?testparam=1\")\n            .body(hyper::Body::empty())\n            .unwrap();\n        let value = get_query_param(&req, \"testparam\").unwrap();\n        assert_eq!(value.unwrap(), \"1\");\n\n        let req = Request::builder()","sourceCodeStart":91,"sourceCodeEnd":127,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/http-utils/src/request.rs#L91-L127","documentation":"Raised by ensure_no_body when an endpoint that must not carry a request body receives at least one body chunk. Handlers of bodyless endpoints (typically GET/DELETE routes) call this guard right after routing; if request.body_mut().data().await yields Some, the call fails with ApiError::BadRequest (HTTP 400).","triggerScenarios":"Sending a body to a bodyless endpoint: 'curl -X GET -d \"{...}\" /v1/status', POSTing JSON to a GET-only route, or clients/proxies that emit a chunked transfer body (even an effectively empty one) on every request.","commonSituations":"curl's -d/--data flags implying a body; HTTP client libraries that attach a default JSON body to all requests; proxies or service meshes injecting payloads; testing tools reusing a POST template against a GET endpoint.","solutions":["Remove the body from the request: drop -d/--data/body fields, or use the correct POST endpoint if you must send data","Move any data you need to transmit into query parameters for GET routes","In your client, delete the body AND the Content-Length/Transfer-Encoding headers so no chunk is sent","If you operate the server, document which routes are bodyless to prevent SDK misuse"],"exampleFix":"# before\ncurl -X GET 'http://localhost:9898/v1/status' -d '{\"verbose\":true}'\n# -> 400 Unexpected request body\n\n# after\ncurl -X GET 'http://localhost:9898/v1/status?verbose=true'\n# -> 200","handlingStrategy":"validation","validationCode":"use hyper::header::{CONTENT_LENGTH, TRANSFER_ENCODING};\n\n/// Cheap header-level pre-check before calling ensure_no_body (which does the\n/// authoritative body-chunk check).\npub fn announces_body(request: &Request<Body>) -> bool {\n    let has_len = request\n        .headers()\n        .get(CONTENT_LENGTH)\n        .and_then(|v| v.to_str().ok())\n        .and_then(|v| v.parse::<u64>().ok())\n        .is_some_and(|n| n > 0);\n    has_len || request.headers().contains_key(TRANSFER_ENCODING)\n}","typeGuard":null,"tryCatchPattern":"if let Err(ApiError::BadRequest(e)) = ensure_no_body(request).await {\n    // 400; also log method+URI to identify which client sends bodies to a GET route\n    tracing::warn!(method = %request.method(), uri = %request.uri(), \"body on bodyless route\");\n    return Err(ApiError::BadRequest(e));\n}","preventionTips":["Configure HTTP clients with an explicit 'no body' request construction for GET/DELETE rather than reusing POST templates","In curl invocations, never leave -d on commands changed to GET (-G moves data to query params)","Have proxies reject or strip bodies on bodyless methods at the edge","Assert in route tests that each GET handler calls ensure_no_body so the contract is enforced uniformly"],"tags":["rust","http","request-body","bad-request","validation"],"backgroundTag":"unexpected-request-body","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}