tiangolo/fastapi · error · HTTPException

{"errors": exc.errors(), "body": body.decode()}

Error message

{"errors": exc.errors(), "body": body.decode()}

What it means

This is not a string detail but an HTTPException raised inside a custom APIRoute (ValidationErrorLoggingRoute) that intercepts RequestValidationError. When the request body fails Pydantic validation, the handler reads the raw body, builds a detail object {'errors': [...], 'body': '<raw>'}, and raises HTTPException(422) with that object as detail. It is the Annotated-style variant of the custom-request-and-route tutorial. The 'message' here is the detail-construction expression, not a fixed string.

Solutions

  1. Send a JSON array of integers: POST / with body [1,2,3] and Content-Type: application/json.
  2. Validate the body shape on the client before sending.
  3. If you do not want the raw body echoed, revert to FastAPI's default route class.

Example fix

// before
curl -X POST -H 'Content-Type: application/json' -d '5' http://localhost:8000/
// after
curl -X POST -H 'Content-Type: application/json' -d '[1,2,3]' http://localhost:8000/
Defensive patterns

Strategy: validation

Validate before calling

import json, httpx
def is_int_list(body: str) -> bool:
    try:
        parsed = json.loads(body)
    except json.JSONDecodeError:
        return False
    return isinstance(parsed, list) and all(isinstance(x, int) for x in parsed)
payload = '[1,2,3]'
if is_int_list(payload):
    resp = httpx.post('http://localhost:8000/', content=payload, headers={'Content-Type': 'application/json'})

Type guard

def is_list_of_int(value: object) -> bool:
    return isinstance(value, list) and all(isinstance(x, int) and not isinstance(x, bool) for x in value)

Try / catch

try:
    resp = httpx.post(url, content=payload, headers={'Content-Type': 'application/json'})
    resp.raise_for_status()
except httpx.HTTPStatusError as e:
    if e.response.status_code == 422:
        detail = e.response.json()
        # detail has 'errors' and 'body' keys
        ...

Prevention

When it happens

Trigger: POST / with a body that is not a valid list[int], e.g. '[1, "a"]', '{}', or a non-JSON body. The custom route handler catches the RequestValidationError and re-raises a 422 carrying both the structured errors and the original body bytes.

Common situations: Clients sending a single int instead of a list, malformed JSON, or wrong content-type. Because the route overrides the default 422 formatter, the error shape differs from FastAPI's built-in response (it echoes the raw body), which can surprise clients expecting the default schema.

Related errors


AI-assisted analysis of tiangolo/fastapi@3e8d1526d8 (2026-08-11). Data as JSON: /api/errors/621b2a18720e8250. Report an issue: GitHub.

Appendix: source

Thrown at docs_src/custom_request_and_route/tutorial002_an_py310.py:19

from collections.abc import Callable
from typing import Annotated

from fastapi import Body, FastAPI, HTTPException, Request, Response
from fastapi.exceptions import RequestValidationError
from fastapi.routing import APIRoute


class ValidationErrorLoggingRoute(APIRoute):
    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            try:
                return await original_route_handler(request)
            except RequestValidationError as exc:
                body = await request.body()
                detail = {"errors": exc.errors(), "body": body.decode()}
                raise HTTPException(status_code=422, detail=detail)

        return custom_route_handler


app = FastAPI()
app.router.route_class = ValidationErrorLoggingRoute


@app.post("/")
async def sum_numbers(numbers: Annotated[list[int], Body()]):
    return sum(numbers)

View on GitHub (pinned to 3e8d1526d8)