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

  1. Remove the body from the request: drop -d/--data/body fields, or use the correct POST endpoint if you must send data
  2. Move any data you need to transmit into query parameters for GET routes
  3. In your client, delete the body AND the Content-Length/Transfer-Encoding headers so no chunk is sent
  4. 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

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


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/53de1fdde52961aa. Report an issue: GitHub.