FastAPI 0.161 Unions Forms. Starlette 1.9 Streams Tests

PyPI Oct 22: FastAPI 0.161.0, unions of form models. 0.160.0 forbids extra siblings. Starlette 1.9.0 Oct 18 streams TestClient. The pin is still >=0.46.

PyPI’s FastAPI 0.161.0 page shows the wheel fastapi-0.161.0-py3-none-any.whl uploaded October 22, 2026, 18:30:41 UTC. The GitHub release is the same minute, tag 0.161.0, commit 80c8742, signed github-actions. The feature line: support for combining unions of form models, PR #16596 by tiangolo. That is a form parser. It is not your Starlette pin.

Starlette 1.9.0 landed October 18. The Kludex/starlette release at 15:49:28 UTC streams TestClient response bodies so client.stream() returns before the application finishes. FastAPI 0.161.0’s requires_dist still says starlette>=0.46.0. Three facts. Mix them and you will bump FastAPI because tests hang, then never get the TestClient change.

We already filed FastAPI Conf on October 28 and Starlette’s BadHost CVE against a FastAPI box. This week the named versions are 0.161.0 and 1.9.0. The named trap is extra="forbid".

0.161.0 is unions of form models. 0.160.0 is the break.

0.161.0’s release body is one feature. Combining unions of form models. If your endpoint never takes a union of form models, 0.161.0 is a no-op you will still install because it is Latest. That is allowed. It is not a reason to skip 0.160.0’s notes.

0.160.0 published October 22 at 16:54:31 UTC, about ninety minutes before 0.161.0. Query, header, cookie, and form parameter models now apply their extra configuration to all submitted fields in the same parameter location, consistent with Pydantic. Fields declared in other models, individual parameters, or dependencies, which were previously excluded, are in scope.

Breaking, PR #16594: with extra="forbid", fields outside the model are rejected even if another parameter declares them. With extra="allow", those fields show up as model extras. Default extra="ignore" ignores them. If your tests started 422-ing on Tuesday afternoon, read 0.160.0 before you bisect 0.161.0. The union-of-forms feature did not invent the 422. The sibling-field rule did.

0.160.0 also bumped certifi from 2026.1.4 to 2026.10.19 in PR #16571. That is FastAPI’s own CI extra, not your app’s bundle, unless you were installing FastAPI’s test deps and calling it a pin. We already lived through pip 26.2.2 dropping emSign while the OS store lagged. This certifi bump is a cousin, not a duplicate. Do not skip 0.160.0 because you upgraded pip.

The two days before were a burst. GitHub: 0.156.0 through 0.156.4 on October 20 with AliasChoices fixes for path, named JSON body, form models, query, headers, cookies. October 21: 0.157.0 combining query, header, and cookie parameter models (#16573); 0.158.0 combining form models (#16577); 0.158.1 preserving form media types in OpenAPI (#16579); 0.158.2 optional form models (#16582); 0.159.0 optional query, header, and cookie parameter models (#16584); 0.159.1 optional unions of form models (#16586). If your lockfile still says 0.155, you are not one bump away from unions. You are a changelog away.

PyPI’s 0.161.0 history table also lists 0.158.1 and 0.158.0 on October 21 with “No provenance” in the extract we have. 0.161.0’s wheel SHA-256 prefix from the JSON is 843d71746b18a334. Verify the full digest on the files tab before you install in an air-gapped job. A prefix is not a pin.

starlette>=0.46.0 will take 1.8.0. It will not take the stream.

Starlette 1.9.0, October 18, two release files, full provenance on PyPI. 1.8.0 was October 13. FastAPI 0.161.0 requires starlette>=0.46.0, plus pydantic>=2.9.0, typing-extensions>=4.8.0, typing-inspection>=0.4.2, annotated-doc>=0.0.2, opentelemetry-api>=1.44.0. There is no starlette>=1.9. pip install fastapi==0.161.0 can leave you on 1.8.0 if 1.8.0 already satisfied the range.

What 1.9.0 actually changed, from the release body:

Changed: stream TestClient response bodies so client.stream() returns before the application finishes. Closing an unfinished streamed response now cancels the application (#3491).

Fixed: percent-encode ?, #, \, and a leading // in the request path when building request.url; encode \ and a leading // in url_path_for (#3671). Use byte length for Content-Length when the body is a typed or multidimensional memoryview (#3622). Include HEAD in the Allow header of HTTPEndpoint 405 responses when GET is defined (#3585). Match host names case-insensitively in Host routing and TrustedHostMiddleware (#3542). Preserve every existing Vary header in MutableHeaders.add_vary_header(), used by GZipMiddleware and SessionMiddleware (#3675).

If your tests call client.stream() and then assert on a side effect that used to happen before the call returned, 1.9.0 will fail you. That failure is the feature. Pin starlette>=1.9.0 in the app if you want it. Do not assume FastAPI did.

The host-matching fix is the one that bites people who wrote TrustedHostMiddleware tests with mixed case. API.Example.COM and api.example.com now match. If you were using case as a negative test, the negative test is wrong.

HEAD on 405 is the one that bites HTTP-oddity scanners. If GET is defined and someone POSTs, Allow should already have listed GET; it now also lists HEAD. Update the assertion. Do not “fix” the framework.

We already spent Django 6.1.2 on four CVEs and a UUID formset. This week is not a CVE file. It is a pin file. Treat it that way.

Conf is still October 28. The wheel is not a ticket.

FastAPI Conf ‘26 remains October 28 in Amsterdam on the project site. We wrote that on October 12. A 0.161.0 drop on October 22 does not change the badge. If your talk is OpenAPI form media types, 0.158.1 is the commit to cite. If your talk is unions of form models, 0.161.0. If your talk is TestClient streaming, you are on Starlette’s stage even if the conference logo says FastAPI.

Lock 0.160.0’s extra behavior in the same PR as the bump. Add a test that sends a sibling field with extra="forbid" on one model and a declared field on another, in the same location. If that test 422s after the bump, you read the note. If you only bump to 0.161.0 and look at unions, you will ship the break with a feature bullet.

Starlette 1.9.0 is optional until you write it down. FastAPI will not write it down for you. >=0.46.0 is a floor from another era. Floors do not stream.

AliasChoices ate October 20. Do not skip the patch train.

If you jump from 0.155 to 0.161 because Latest looks clean, you swallow a day of AliasChoices fixes without a test that uses aliases. 0.156.0 through 0.156.4 on October 20, per GitHub: path parameters, named JSON body parameters, form models, query parameters, headers, cookies. Those PRs are #16560, #16559, #16557, #16555 in the notes we pulled. If OpenAPI or form parsing of aliased fields looked wrong on 0.155, the fix may already be in 0.156.4. 0.161.0 will include it. Your changelog email should still name 0.156.x so the next person does not re-open a ticket.

Optional models landed October 21: forms in 0.158.2, query/header/cookie in 0.159.0, optional unions of form models in 0.159.1, then combining unions in 0.161.0. That is a sequence, not four ways to say the same thing. Optional is None. Combining is more than one model in one location. Unions are A | B. If your endpoint is Form() on a single model, none of this is your incident. If your endpoint is a union of two form models, 0.161.0 is the first release that claims that shape. Write a test that posts both variants before you trust the bullet.

OpenAPI form media types in 0.158.1 is the one that bites generated clients. If a client stopped sending multipart/form-data after a FastAPI bump last week, read #16579 before you blame Starlette 1.9.0. Starlette did not touch your OpenAPI document.

Content-Length on a memoryview body is a Starlette 1.9.0 fix. FastAPI streaming responses that wrap memoryviews can send a wrong length on 1.8.0. If you serve numpy buffers or bytes memoryviews, pin 1.9.0 even if FastAPI is happy with 0.46. Percent-encoding of ? and # in request.url is the other silent break: a test that compared raw paths with query characters in the path will change. That is the security-shaped fix. Do not revert it to make a golden string pass.

python-multipart>=0.0.18 is still an extra on FastAPI’s standard extra, not a hard require of 0.161.0’s base install. Base 0.161.0 is Starlette, Pydantic, typing-extensions, typing-inspection, annotated-doc, opentelemetry-api. If your Docker image installs fastapi with no extras, you did not pick up python-multipart and you did not pick up uvicorn. Form unions on a base install still need the multipart extra you were already supposed to have.

sdist SHA-256 prefix from the JSON for the wheel was 843d71746b18a334. The tarball is a different digest. Do not run sha256sum on fastapi-0.161.0.tar.gz with the wheel hash. PyPI’s files tab has both. Conf Amsterdam does not sign your lockfile. The lockfile is the article.

Docs PRs in 0.157.0 the same Wednesday clarified boolean query parameter parsing (#16569) and nullable query parameters versus path parameter defaults (#16568). Those are docs. They still change what a junior copies from the site. If your boolean query tests flipped when you bumped past 0.157, read the docs PR before you blame 0.160.0’s extra rule.

httpx2>=2.0.0 appears on FastAPI’s standard extra next to httpx<1.0.0,>=0.23.0. Two httpx names. TestClient in Starlette 1.9.0 is the streaming change; the client library extra is a different pin. Do not upgrade httpx because TestClient hangs. Hang is #3491. Closing the unfinished stream cancels the app. If a test used to leak a background task after client.stream(), 1.9.0 may start cancelling it. That is the intended close. Fix the test to consume or close the stream.

GZip and Session middleware go through add_vary_header(). 1.9.0 keeps every existing Vary value instead of replacing them. If a CDN test expected a single Accept-Encoding token and you also set Vary: Cookie, the header is now both. Update the assertion. Do not strip Cookie to make GZip pretty.

1.8.0 on October 13 is still a legal install under FastAPI’s range. Write starlette==1.9.0 if you want the stream, the memoryview length, and the case-insensitive host match. Write nothing if you only wanted form unions. Latest on two packages is two decisions. Pin both in the same PR or pin neither and wait for the 422s. The 422s will be 0.160.0. The hung stream will be 1.9.0. Name which one you meant. Then run the test suite twice: once on forms, once on client.stream(). A green chat window is not a lockfile. Neither is a conference badge. Ship the pin.

Spread The Article

Share this guide

Send this article to your network or keep a copy of the direct link.

X Facebook LinkedIn Reddit Telegram

Discussion

Leave a comment

No comments yet

Be the first to start the conversation.