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
- Send a JSON array of integers: POST / with body [1,2,3] and Content-Type: application/json.
- Validate the body shape on the client before sending.
- 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
- Always send Content-Type: application/json with a JSON array body.
- Validate the payload is list[int] before sending.
- Expect the non-default 422 shape (errors + raw body) from this custom route.
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
- {"errors": exc.errors(), "body": body.decode()}
- Expected UploadFile, received
- Invalid YAML
- validation error(s)
- Cannot set both 'data' and 'raw_data' on the same…
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)