neondatabase/neon · warning · ApiError

missing request body

Error message

missing request body

What it means

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.

Source

Thrown at libs/http-utils/src/json.rs:18

use anyhow::Context;
use bytes::Buf;
use hyper::{Body, Request, Response, StatusCode, header};
use serde::{Deserialize, Serialize};

use super::error::ApiError;

/// Parse a json request body and deserialize it to the type `T`.
pub async fn json_request<T: for<'de> Deserialize<'de>>(
    request: &mut Request<Body>,
) -> Result<T, ApiError> {
    let body = hyper::body::aggregate(request.body_mut())
        .await
        .context("Failed to read request body")
        .map_err(ApiError::BadRequest)?;

    if body.remaining() == 0 {
        return Err(ApiError::BadRequest(anyhow::anyhow!(
            "missing request body"
        )));
    }

    let mut deser = serde_json::de::Deserializer::from_reader(body.reader());

    serde_path_to_error::deserialize(&mut deser)
        // intentionally stringify because the debug version is not helpful in python logs
        .map_err(|e| anyhow::anyhow!("Failed to parse json request: {e}"))
        .map_err(ApiError::BadRequest)
}

/// Parse a json request body and deserialize it to the type `T`. If the body is empty, return `T::default`.
pub async fn json_request_maybe<T: for<'de> Deserialize<'de> + Default>(
    request: &mut Request<Body>,
) -> Result<T, ApiError> {
    let body = hyper::body::aggregate(request.body_mut())
        .await

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Send an actual JSON body, even if empty object {} where the endpoint accepts it: curl -d '{}' or the equivalent in your client
  2. Check for hops (proxies, redirects) that drop the body, and confirm Content-Type: application/json is accompanied by a non-zero body
  3. If the endpoint legitimately allows no body, use its variant that calls json_request_maybe (or pick the API route documented as bodyless)

Example fix

# before
curl -X POST http://localhost:9898/v1/tenant/aaa/timeline   # no -d -> 400 missing request body

# after
curl -X POST http://localhost:9898/v1/tenant/aaa/timeline \
  -H 'Content-Type: application/json' -d '{"new_timeline_id":"..."}'
Defensive patterns

Strategy: validation

Validate before calling

// Never issue a body-less JSON POST:
const body = JSON.stringify(payload ?? {});
await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body });

Type guard

function hasJsonBody(body) { return body != null && body.length > 0; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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