neondatabase/neon · error · ApiError
Unexpected request body
Error message
Unexpected request body
What it means
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).
Source
Thrown at libs/http-utils/src/request.rs:109
v.parse().map_err(|e| {
ApiError::BadRequest(anyhow!("cannot parse query param {param_name}: {e}"))
})
})
.transpose()
}
pub fn must_parse_query_param<E: fmt::Display, T: FromStr<Err = E>>(
request: &Request<Body>,
param_name: &str,
) -> Result<T, ApiError> {
parse_query_param(request, param_name)?.ok_or_else(|| {
ApiError::BadRequest(anyhow!("no {param_name} specified in query parameters"))
})
}
pub async fn ensure_no_body(request: &mut Request<Body>) -> Result<(), ApiError> {
match request.body_mut().data().await {
Some(_) => Err(ApiError::BadRequest(anyhow!("Unexpected request body"))),
None => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_query_param_duplicate() {
let req = Request::builder()
.uri("http://localhost:12345/testuri?testparam=1")
.body(hyper::Body::empty())
.unwrap();
let value = get_query_param(&req, "testparam").unwrap();
assert_eq!(value.unwrap(), "1");
let req = Request::builder()View on GitHub (pinned to 8f60b04da4)
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
Example fix
# before
curl -X GET 'http://localhost:9898/v1/status' -d '{"verbose":true}'
# -> 400 Unexpected request body
# after
curl -X GET 'http://localhost:9898/v1/status?verbose=true'
# -> 200 Defensive patterns
Strategy: validation
Validate before calling
use hyper::header::{CONTENT_LENGTH, TRANSFER_ENCODING};
/// Cheap header-level pre-check before calling ensure_no_body (which does the
/// authoritative body-chunk check).
pub fn announces_body(request: &Request<Body>) -> bool {
let has_len = request
.headers()
.get(CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.is_some_and(|n| n > 0);
has_len || request.headers().contains_key(TRANSFER_ENCODING)
} Try / catch
if let Err(ApiError::BadRequest(e)) = ensure_no_body(request).await {
// 400; also log method+URI to identify which client sends bodies to a GET route
tracing::warn!(method = %request.method(), uri = %request.uri(), "body on bodyless route");
return Err(ApiError::BadRequest(e));
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- param {param_name} specified more than once
- no {param_name} specified in query parameters
- cannot parse query param {param_name}: {e}
- error downloading extension {:?}: {:?}
- Error parsing {remote_endpoint}, expected host:port, got {er
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/53de1fdde52961aa.
Report an issue: GitHub.