Compare commits

...

19 Commits

Author SHA1 Message Date
Yurii Motov
01c0e11700 Rename docs_src/websockets to docs_src/websockets_ 2026-02-23 21:52:01 +01:00
github-actions[bot]
2f9c914d44 📝 Update release notes
[skip ci]
2026-02-23 18:48:43 +00:00
Sebastián Ramírez
0cf27ecf88 👥 Update FastAPI People - Experts (#14972)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com>
2026-02-23 19:47:59 +01:00
github-actions[bot]
3f30ca1a5e 📝 Update release notes
[skip ci]
2026-02-23 18:32:32 +00:00
Motov Yurii
6af3832126 👷 Allow skipping benchmark job in test workflow (#14974) 2026-02-23 19:31:54 +01:00
Sebastián Ramírez
acdf52e0c8 📝 Update release notes 2026-02-23 18:54:18 +01:00
Sebastián Ramírez
5c863d0718 🔖 Release version 0.132.0 2026-02-23 18:49:58 +01:00
github-actions[bot]
ac8621a76e 📝 Update release notes
[skip ci]
2026-02-23 17:46:11 +00:00
Sebastián Ramírez
22354a2530 🔒️ Add strict_content_type checking for JSON requests (#14978) 2026-02-23 18:45:20 +01:00
github-actions[bot]
94a1ee749e 📝 Update release notes
[skip ci]
2026-02-23 16:50:41 +00:00
dependabot[bot]
248d7fb9f5 ⬆ Bump flask from 3.1.2 to 3.1.3 (#14949)
Bumps [flask](https://github.com/pallets/flask) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/3.1.2...3.1.3)

---
updated-dependencies:
- dependency-name: flask
  dependency-version: 3.1.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 17:49:53 +01:00
github-actions[bot]
da1937443d 📝 Update release notes
[skip ci]
2026-02-23 15:04:55 +00:00
Sofie Van Landeghem
5161f7b42b ⬆ Update all dependencies to use griffelib instead of griffe (#14973)
* update to griffelib

* also update pydantic-ai

* move griffelib to get better GH diff

* restore accidental edit
2026-02-23 16:04:24 +01:00
github-actions[bot]
fef2ce70d9 📝 Update release notes
[skip ci]
2026-02-23 11:45:11 +00:00
Motov Yurii
a3c8c37272 🔨 Fix FastAPI People workflow (#14951) 2026-02-23 12:44:47 +01:00
github-actions[bot]
2826124378 📝 Update release notes
[skip ci]
2026-02-22 18:22:03 +00:00
Sebastián Ramírez
4da264f0f3 👷 Do not run codspeed with coverage as it's not tracked (#14966) 2026-02-22 19:21:38 +01:00
github-actions[bot]
c5559a66dd 📝 Update release notes
[skip ci]
2026-02-22 18:14:11 +00:00
Sebastián Ramírez
1cea8f659c 👷 Do not include benchmark tests in coverage to speed up coverage processing (#14965) 2026-02-22 19:13:49 +01:00
39 changed files with 1104 additions and 590 deletions

View File

@@ -68,10 +68,8 @@ jobs:
python-version: "3.13"
coverage: coverage
uv-resolution: highest
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
- os: ubuntu-latest
python-version: "3.13"
coverage: coverage
uv-resolution: highest
codspeed: codspeed
- os: ubuntu-latest
@@ -109,20 +107,10 @@ jobs:
run: uv pip install "git+https://github.com/Kludex/starlette@main"
- run: mkdir coverage
- name: Test
if: matrix.codspeed != 'codspeed'
run: uv run --no-sync bash scripts/test.sh
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
- name: CodSpeed benchmarks
if: matrix.codspeed == 'codspeed'
uses: CodSpeedHQ/action@v4
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
with:
mode: simulation
run: uv run --no-sync coverage run -m pytest tests/ --codspeed
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
- name: Store coverage files
if: matrix.coverage == 'coverage'
@@ -132,6 +120,39 @@ jobs:
path: coverage
include-hidden-files: true
benchmark:
needs:
- changes
if: needs.changes.outputs.src == 'true' || github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
env:
UV_PYTHON: "3.13"
UV_RESOLUTION: highest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Setup uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Install Dependencies
run: uv sync --no-dev --group tests --extra all
- name: CodSpeed benchmarks
uses: CodSpeedHQ/action@v4
with:
mode: simulation
run: uv run --no-sync pytest tests/benchmarks --codspeed
coverage-combine:
needs:
- test
@@ -176,6 +197,7 @@ jobs:
if: always()
needs:
- coverage-combine
- benchmark
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
@@ -186,4 +208,4 @@ jobs:
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: coverage-combine,test
allowed-skips: coverage-combine,test,benchmark

View File

@@ -38,13 +38,13 @@ In der Produktion hätten Sie eine der oben genannten Optionen.
Aber es ist der einfachste Weg, sich auf die Serverseite von WebSockets zu konzentrieren und ein funktionierendes Beispiel zu haben:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Einen `websocket` erstellen { #create-a-websocket }
Erstellen Sie in Ihrer **FastAPI**-Anwendung einen `websocket`:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Technische Details
@@ -58,7 +58,7 @@ Sie könnten auch `from starlette.websockets import WebSocket` verwenden.
In Ihrer WebSocket-Route können Sie Nachrichten `await`en und Nachrichten senden.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
Sie können Binär-, Text- und JSON-Daten empfangen und senden.
@@ -109,7 +109,7 @@ In WebSocket-Endpunkten können Sie Folgendes aus `fastapi` importieren und verw
Diese funktionieren auf die gleiche Weise wie für andere FastAPI-Endpunkte/*Pfadoperationen*:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info | Info
@@ -154,7 +154,7 @@ Damit können Sie den WebSocket verbinden und dann Nachrichten senden und empfan
Wenn eine WebSocket-Verbindung geschlossen wird, löst `await websocket.receive_text()` eine `WebSocketDisconnect`-Exception aus, die Sie dann wie in folgendem Beispiel abfangen und behandeln können.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
Zum Ausprobieren:

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,88 @@
# Strict Content-Type Checking { #strict-content-type-checking }
By default, **FastAPI** uses strict `Content-Type` header checking for JSON request bodies, this means that JSON requests **must** include a valid `Content-Type` header (e.g. `application/json`) in order for the body to be parsed as JSON.
## CSRF Risk { #csrf-risk }
This default behavior provides protection against a class of **Cross-Site Request Forgery (CSRF)** attacks in a very specific scenario.
These attacks exploit the fact that browsers allow scripts to send requests without doing any CORS preflight check when they:
* don't have a `Content-Type` header (e.g. using `fetch()` with a `Blob` body)
* and don't send any authentication credentials.
This type of attack is mainly relevant when:
* the application is running locally (e.g. on `localhost`) or in an internal network
* and the application doesn't have any authentication, it expects that any request from the same network can be trusted.
## Example Attack { #example-attack }
Imagine you build a way to run a local AI agent.
It provides an API at
```
http://localhost:8000/v1/agents/multivac
```
There's also a frontend at
```
http://localhost:8000
```
/// tip
Note that both have the same host.
///
Then using the frontend you can make the AI agent do things on your behalf.
As it's running **locally**, and not in the open internet, you decide to **not have any authentication** set up, just trusting the access to the local network.
Then one of your users could install it and run it locally.
Then they could open a malicious website, e.g. something like
```
https://evilhackers.example.com
```
And that malicious website sends requests using `fetch()` with a `Blob` body to the local API at
```
http://localhost:8000/v1/agents/multivac
```
Even though the host of the malicious website and the local app is different, the browser won't trigger a CORS preflight request because:
* It's running without any authentication, it doesn't have to send any credentials.
* The browser thinks it's not sending JSON (because of the missing `Content-Type` header).
Then the malicious website could make the local AI agent send angry messages to the user's ex-boss... or worse. 😅
## Open Internet { #open-internet }
If your app is in the open internet, you wouldn't "trust the network" and let anyone send privileged requests without authentication.
Attackers could simply run a script to send requests to your API, no need for browser interaction, so you are probably already securing any privileged endpoints.
In that case **this attack / risk doesn't apply to you**.
This risk and attack is mainly relevant when the app runs on the **local network** and that is the **only assumed protection**.
## Allowing Requests Without Content-Type { #allowing-requests-without-content-type }
If you need to support clients that don't send a `Content-Type` header, you can disable strict checking by setting `strict_content_type=False`:
{* ../../docs_src/strict_content_type/tutorial001_py310.py hl[4] *}
With this setting, requests without a `Content-Type` header will have their body parsed as JSON, which is the same behavior as older versions of FastAPI.
/// info
This behavior and configuration was added in FastAPI 0.132.0.
///

View File

@@ -38,13 +38,13 @@ In production you would have one of the options above.
But it's the simplest way to focus on the server-side of WebSockets and have a working example:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Create a `websocket` { #create-a-websocket }
In your **FastAPI** application, create a `websocket`:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Technical Details
@@ -58,7 +58,7 @@ You could also use `from starlette.websockets import WebSocket`.
In your WebSocket route you can `await` for messages and send messages.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
You can receive and send binary, text, and JSON data.
@@ -109,7 +109,7 @@ In WebSocket endpoints you can import from `fastapi` and use:
They work the same way as for other FastAPI endpoints/*path operations*:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info
@@ -154,7 +154,7 @@ With that you can connect the WebSocket and then send and receive messages:
When a WebSocket connection is closed, the `await websocket.receive_text()` will raise a `WebSocketDisconnect` exception, which you can then catch and handle like in this example.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
To try it out:

View File

@@ -7,6 +7,28 @@ hide:
## Latest Changes
### Internal
* 👥 Update FastAPI People - Experts. PR [#14972](https://github.com/fastapi/fastapi/pull/14972) by [@tiangolo](https://github.com/tiangolo).
* 👷 Allow skipping `benchmark` job in `test` workflow. PR [#14974](https://github.com/fastapi/fastapi/pull/14974) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.132.0
### Breaking Changes
* 🔒️ Add `strict_content_type` checking for JSON requests. PR [#14978](https://github.com/fastapi/fastapi/pull/14978) by [@tiangolo](https://github.com/tiangolo).
* Now FastAPI checks, by default, that JSON requests have a `Content-Type` header with a valid JSON value, like `application/json`, and rejects requests that don't.
* If the clients for your app don't send a valid `Content-Type` header you can disable this with `strict_content_type=False`.
* Check the new docs: [Strict Content-Type Checking](https://fastapi.tiangolo.com/advanced/strict-content-type/).
### Internal
* ⬆ Bump flask from 3.1.2 to 3.1.3. PR [#14949](https://github.com/fastapi/fastapi/pull/14949) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Update all dependencies to use `griffelib` instead of `griffe`. PR [#14973](https://github.com/fastapi/fastapi/pull/14973) by [@svlandeg](https://github.com/svlandeg).
* 🔨 Fix `FastAPI People` workflow. PR [#14951](https://github.com/fastapi/fastapi/pull/14951) by [@YuriiMotov](https://github.com/YuriiMotov).
* 👷 Do not run codspeed with coverage as it's not tracked. PR [#14966](https://github.com/fastapi/fastapi/pull/14966) by [@tiangolo](https://github.com/tiangolo).
* 👷 Do not include benchmark tests in coverage to speed up coverage processing. PR [#14965](https://github.com/fastapi/fastapi/pull/14965) by [@tiangolo](https://github.com/tiangolo).
## 0.131.0
### Breaking Changes

View File

@@ -193,6 +193,7 @@ nav:
- advanced/generate-clients.md
- advanced/advanced-python-types.md
- advanced/json-base64-bytes.md
- advanced/strict-content-type.md
- fastapi-cli.md
- Deployment:
- deployment/index.md

View File

@@ -38,13 +38,13 @@ En producción tendrías una de las opciones anteriores.
Pero es la forma más sencilla de enfocarse en el lado del servidor de WebSockets y tener un ejemplo funcional:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Crear un `websocket` { #create-a-websocket }
En tu aplicación de **FastAPI**, crea un `websocket`:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Detalles Técnicos
@@ -58,7 +58,7 @@ También podrías usar `from starlette.websockets import WebSocket`.
En tu ruta de WebSocket puedes `await` para recibir mensajes y enviar mensajes.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
Puedes recibir y enviar datos binarios, de texto y JSON.
@@ -109,7 +109,7 @@ En endpoints de WebSocket puedes importar desde `fastapi` y usar:
Funcionan de la misma manera que para otros endpoints de FastAPI/*path operations*:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info | Información
@@ -154,7 +154,7 @@ Con eso puedes conectar el WebSocket y luego enviar y recibir mensajes:
Cuando una conexión de WebSocket se cierra, el `await websocket.receive_text()` lanzará una excepción `WebSocketDisconnect`, que puedes capturar y manejar como en este ejemplo.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
Para probarlo:

View File

@@ -38,13 +38,13 @@ En production, vous auriez l'une des options ci-dessus.
Mais c'est la façon la plus simple de se concentrer sur la partie serveur des WebSockets et d'avoir un exemple fonctionnel :
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Créer un `websocket` { #create-a-websocket }
Dans votre application **FastAPI**, créez un `websocket` :
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Détails techniques
@@ -58,7 +58,7 @@ Vous pourriez aussi utiliser `from starlette.websockets import WebSocket`.
Dans votre route WebSocket, vous pouvez `await` des messages et envoyer des messages.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
Vous pouvez recevoir et envoyer des données binaires, texte et JSON.
@@ -109,7 +109,7 @@ Dans les endpoints WebSocket, vous pouvez importer depuis `fastapi` et utiliser
Ils fonctionnent de la même manière que pour les autres endpoints/*chemins d'accès* FastAPI :
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info
@@ -154,7 +154,7 @@ Avec cela, vous pouvez connecter le WebSocket puis envoyer et recevoir des messa
Lorsqu'une connexion WebSocket est fermée, l'instruction `await websocket.receive_text()` lèvera une exception `WebSocketDisconnect`, que vous pouvez ensuite intercepter et gérer comme dans cet exemple.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
Pour l'essayer :

View File

@@ -38,13 +38,13 @@ $ pip install websockets
しかし、これはWebSocketsのサーバーサイドに焦点を当て、動作する例を示す最も簡単な方法です。
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## `websocket` を作成する { #create-a-websocket }
**FastAPI** アプリケーションで、`websocket` を作成します。
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | 技術詳細
@@ -58,7 +58,7 @@ $ pip install websockets
WebSocketルートでは、メッセージを待機して送信するために `await` を使用できます。
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
バイナリやテキストデータ、JSONデータを送受信できます。
@@ -109,7 +109,7 @@ WebSocketエンドポイントでは、`fastapi` から以下をインポート
これらは、他のFastAPI エンドポイント/*path operations* の場合と同じように機能します。
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info | 情報
@@ -154,7 +154,7 @@ $ fastapi dev main.py
WebSocket接続が閉じられると、 `await websocket.receive_text()` は例外 `WebSocketDisconnect` を発生させ、この例のようにキャッチして処理することができます。
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
試してみるには、

View File

@@ -38,13 +38,13 @@ $ pip install websockets
그러나 이는 WebSockets의 서버 측에 집중하고 동작하는 예제를 제공하는 가장 간단한 방법입니다:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## `websocket` 생성하기 { #create-a-websocket }
**FastAPI** 애플리케이션에서 `websocket`을 생성합니다:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | 기술 세부사항
@@ -58,7 +58,7 @@ $ pip install websockets
WebSocket 경로에서 메시지를 대기(`await`)하고 전송할 수 있습니다.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
여러분은 이진 데이터, 텍스트, JSON 데이터를 받을 수 있고 전송할 수 있습니다.
@@ -109,7 +109,7 @@ WebSocket 엔드포인트에서 `fastapi`에서 다음을 가져와 사용할
이들은 다른 FastAPI 엔드포인트/*경로 처리*와 동일하게 동작합니다:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info | 정보
@@ -154,7 +154,7 @@ $ fastapi dev main.py
WebSocket 연결이 닫히면, `await websocket.receive_text()``WebSocketDisconnect` 예외를 발생시킵니다. 그러면 이 예제처럼 이를 잡아 처리할 수 있습니다.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
테스트해보기:

View File

@@ -38,13 +38,13 @@ Na produção, você teria uma das opções acima.
Mas é a maneira mais simples de focar no lado do servidor de WebSockets e ter um exemplo funcional:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Crie um `websocket` { #create-a-websocket }
Em sua aplicação **FastAPI**, crie um `websocket`:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Detalhes Técnicos
@@ -58,7 +58,7 @@ A **FastAPI** fornece o mesmo `WebSocket` diretamente apenas como uma conveniên
Em sua rota WebSocket você pode esperar (`await`) por mensagens e enviar mensagens.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
Você pode receber e enviar dados binários, de texto e JSON.
@@ -109,7 +109,7 @@ Nos endpoints WebSocket você pode importar do `fastapi` e usar:
Eles funcionam da mesma forma que para outros endpoints FastAPI/*operações de rota*:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info | Informação
@@ -154,7 +154,7 @@ Com isso você pode conectar o WebSocket e então enviar e receber mensagens:
Quando uma conexão WebSocket é fechada, o `await websocket.receive_text()` levantará uma exceção `WebSocketDisconnect`, que você pode então capturar e lidar como neste exemplo.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
Para testar:

View File

@@ -38,13 +38,13 @@ $ pip install websockets
Для примера нам нужен наиболее простой способ, который позволит сосредоточиться на серверной части веб‑сокетов и получить рабочий код:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Создание `websocket` { #create-a-websocket }
Создайте `websocket` в своем **FastAPI** приложении:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Технические детали
@@ -58,7 +58,7 @@ $ pip install websockets
Через эндпоинт веб-сокета вы можете получать и отправлять сообщения.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
Вы можете получать и отправлять двоичные, текстовые и JSON данные.
@@ -109,7 +109,7 @@ $ fastapi dev main.py
Они работают так же, как и в других FastAPI эндпоинтах/*операциях пути*:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info | Примечание
@@ -154,7 +154,7 @@ $ fastapi dev main.py
Если веб-сокет соединение закрыто, то `await websocket.receive_text()` вызовет исключение `WebSocketDisconnect`, которое можно поймать и обработать как в этом примере:
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
Чтобы воспроизвести пример:

View File

@@ -38,13 +38,13 @@ Production'da yukarıdaki seçeneklerden birini kullanırsınız.
Ama WebSockets'in server tarafına odaklanmak ve çalışan bir örnek görmek için en basit yol bu:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Bir `websocket` Oluşturun { #create-a-websocket }
**FastAPI** uygulamanızda bir `websocket` oluşturun:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Teknik Detaylar
@@ -58,7 +58,7 @@ Ama WebSockets'in server tarafına odaklanmak ve çalışan bir örnek görmek i
WebSocket route'unuzda mesajları `await` edebilir ve mesaj gönderebilirsiniz.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
Binary, text ve JSON verisi alıp gönderebilirsiniz.
@@ -109,7 +109,7 @@ WebSocket endpoint'lerinde `fastapi` içinden import edip şunları kullanabilir
Diğer FastAPI endpoint'leri/*path operations* ile aynı şekilde çalışırlar:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info | Bilgi
@@ -154,7 +154,7 @@ Bununla WebSocket'e bağlanabilir, ardından mesaj gönderip alabilirsiniz:
Bir WebSocket bağlantısı kapandığında, `await websocket.receive_text()` bir `WebSocketDisconnect` exception'ı raise eder; ardından bunu bu örnekteki gibi yakalayıp (catch) yönetebilirsiniz.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
Denemek için:

View File

@@ -38,13 +38,13 @@ $ pip install websockets
Але це найпростіший спосіб зосередитися на серверній частині WebSockets і мати робочий приклад:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## Створіть `websocket` { #create-a-websocket }
У вашому застосунку **FastAPI** створіть `websocket`:
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | Технічні деталі
@@ -58,7 +58,7 @@ $ pip install websockets
У вашому маршруті WebSocket ви можете `await` повідомлення і надсилати повідомлення.
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
Ви можете отримувати та надсилати бінарні, текстові та JSON-дані.
@@ -109,7 +109,7 @@ $ fastapi dev main.py
Вони працюють так само, як для інших ендпойнтів FastAPI/*операцій шляху*:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info
@@ -154,7 +154,7 @@ $ fastapi dev main.py
Коли з'єднання WebSocket закривається, `await websocket.receive_text()` підніме виняток `WebSocketDisconnect`, який ви можете перехопити й обробити, як у цьому прикладі.
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
Щоб спробувати:

View File

@@ -38,13 +38,13 @@ $ pip install websockets
但這是能讓我們專注於 WebSocket 伺服端並跑起一個可運作範例的最簡單方式:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## 建立一個 `websocket` { #create-a-websocket }
在你的 **FastAPI** 應用中,建立一個 `websocket`
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | 技術細節
@@ -58,7 +58,7 @@ $ pip install websockets
在你的 WebSocket 路由中,你可以 `await` 接收訊息並傳送訊息。
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
你可以接收與傳送二進位、文字與 JSON 資料。
@@ -109,7 +109,7 @@ $ fastapi dev main.py
它們的運作方式與其他 FastAPI 端點/*路徑操作* 相同:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info
@@ -154,7 +154,7 @@ $ fastapi dev main.py
當 WebSocket 連線關閉時,`await websocket.receive_text()` 會拋出 `WebSocketDisconnect` 例外,你可以像範例中那樣捕捉並處理。
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
試用方式:

View File

@@ -38,13 +38,13 @@ $ pip install websockets
但这是一种专注于 WebSockets 的服务器端并提供一个工作示例的最简单方式:
{* ../../docs_src/websockets/tutorial001_py310.py hl[2,6:38,41:43] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[2,6:38,41:43] *}
## 创建 `websocket` { #create-a-websocket }
在您的 **FastAPI** 应用程序中,创建一个 `websocket`
{* ../../docs_src/websockets/tutorial001_py310.py hl[1,46:47] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[1,46:47] *}
/// note | 技术细节
@@ -58,7 +58,7 @@ $ pip install websockets
在您的 WebSocket 路由中,您可以使用 `await` 等待消息并发送消息。
{* ../../docs_src/websockets/tutorial001_py310.py hl[48:52] *}
{* ../../docs_src/websockets_/tutorial001_py310.py hl[48:52] *}
您可以接收和发送二进制、文本和 JSON 数据。
@@ -109,7 +109,7 @@ $ fastapi dev main.py
它们的工作方式与其他 FastAPI 端点/*路径操作* 相同:
{* ../../docs_src/websockets/tutorial002_an_py310.py hl[68:69,82] *}
{* ../../docs_src/websockets_/tutorial002_an_py310.py hl[68:69,82] *}
/// info
@@ -154,7 +154,7 @@ $ fastapi dev main.py
当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。
{* ../../docs_src/websockets/tutorial003_py310.py hl[79:81] *}
{* ../../docs_src/websockets_/tutorial003_py310.py hl[79:81] *}
尝试以下操作:

View File

@@ -0,0 +1,14 @@
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(strict_content_type=False)
class Item(BaseModel):
name: str
price: float
@app.post("/items/")
async def create_item(item: Item):
return item

View File

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.131.0"
__version__ = "0.132.0"
from starlette import status as status

View File

@@ -840,6 +840,29 @@ class FastAPI(Starlette):
"""
),
] = None,
strict_content_type: Annotated[
bool,
Doc(
"""
Enable strict checking for request Content-Type headers.
When `True` (the default), requests with a body that do not include
a `Content-Type` header will **not** be parsed as JSON.
This prevents potential cross-site request forgery (CSRF) attacks
that exploit the browser's ability to send requests without a
Content-Type header, bypassing CORS preflight checks. In particular
applicable for apps that need to be run locally (in localhost).
When `False`, requests without a `Content-Type` header will have
their body parsed as JSON, which maintains compatibility with
certain clients that don't send `Content-Type` headers.
Read more about it in the
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
"""
),
] = True,
**extra: Annotated[
Any,
Doc(
@@ -974,6 +997,7 @@ class FastAPI(Starlette):
include_in_schema=include_in_schema,
responses=responses,
generate_unique_id_function=generate_unique_id_function,
strict_content_type=strict_content_type,
)
self.exception_handlers: dict[
Any, Callable[[Request, Any], Response | Awaitable[Response]]

View File

@@ -329,6 +329,7 @@ def get_request_handler(
response_model_exclude_none: bool = False,
dependency_overrides_provider: Any | None = None,
embed_body_fields: bool = False,
strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
assert dependant.call is not None, "dependant.call must be a function"
is_coroutine = dependant.is_coroutine_callable
@@ -337,6 +338,10 @@ def get_request_handler(
actual_response_class: type[Response] = response_class.value
else:
actual_response_class = response_class
if isinstance(strict_content_type, DefaultPlaceholder):
actual_strict_content_type: bool = strict_content_type.value
else:
actual_strict_content_type = strict_content_type
async def app(request: Request) -> Response:
response: Response | None = None
@@ -370,7 +375,8 @@ def get_request_handler(
json_body: Any = Undefined
content_type_value = request.headers.get("content-type")
if not content_type_value:
json_body = await request.json()
if not actual_strict_content_type:
json_body = await request.json()
else:
message = email.message.Message()
message["content-type"] = content_type_value
@@ -599,6 +605,7 @@ class APIRoute(routing.Route):
openapi_extra: dict[str, Any] | None = None,
generate_unique_id_function: Callable[["APIRoute"], str]
| DefaultPlaceholder = Default(generate_unique_id),
strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> None:
self.path = path
self.endpoint = endpoint
@@ -625,6 +632,7 @@ class APIRoute(routing.Route):
self.callbacks = callbacks
self.openapi_extra = openapi_extra
self.generate_unique_id_function = generate_unique_id_function
self.strict_content_type = strict_content_type
self.tags = tags or []
self.responses = responses or {}
self.name = get_name(endpoint) if name is None else name
@@ -713,6 +721,7 @@ class APIRoute(routing.Route):
response_model_exclude_none=self.response_model_exclude_none,
dependency_overrides_provider=self.dependency_overrides_provider,
embed_body_fields=self._embed_body_fields,
strict_content_type=self.strict_content_type,
)
def matches(self, scope: Scope) -> tuple[Match, Scope]:
@@ -963,6 +972,29 @@ class APIRouter(routing.Router):
"""
),
] = Default(generate_unique_id),
strict_content_type: Annotated[
bool,
Doc(
"""
Enable strict checking for request Content-Type headers.
When `True` (the default), requests with a body that do not include
a `Content-Type` header will **not** be parsed as JSON.
This prevents potential cross-site request forgery (CSRF) attacks
that exploit the browser's ability to send requests without a
Content-Type header, bypassing CORS preflight checks. In particular
applicable for apps that need to be run locally (in localhost).
When `False`, requests without a `Content-Type` header will have
their body parsed as JSON, which maintains compatibility with
certain clients that don't send `Content-Type` headers.
Read more about it in the
[FastAPI docs for Strict Content-Type](https://fastapi.tiangolo.com/advanced/strict-content-type/).
"""
),
] = Default(True),
) -> None:
# Determine the lifespan context to use
if lifespan is None:
@@ -1009,6 +1041,7 @@ class APIRouter(routing.Router):
self.route_class = route_class
self.default_response_class = default_response_class
self.generate_unique_id_function = generate_unique_id_function
self.strict_content_type = strict_content_type
def route(
self,
@@ -1059,6 +1092,7 @@ class APIRouter(routing.Router):
openapi_extra: dict[str, Any] | None = None,
generate_unique_id_function: Callable[[APIRoute], str]
| DefaultPlaceholder = Default(generate_unique_id),
strict_content_type: bool | DefaultPlaceholder = Default(True),
) -> None:
route_class = route_class_override or self.route_class
responses = responses or {}
@@ -1105,6 +1139,9 @@ class APIRouter(routing.Router):
callbacks=current_callbacks,
openapi_extra=openapi_extra,
generate_unique_id_function=current_generate_unique_id,
strict_content_type=get_value_or_default(
strict_content_type, self.strict_content_type
),
)
self.routes.append(route)
@@ -1480,6 +1517,11 @@ class APIRouter(routing.Router):
callbacks=current_callbacks,
openapi_extra=route.openapi_extra,
generate_unique_id_function=current_generate_unique_id,
strict_content_type=get_value_or_default(
route.strict_content_type,
router.strict_content_type,
self.strict_content_type,
),
)
elif isinstance(route, routing.Route):
methods = list(route.methods or [])

View File

@@ -242,6 +242,7 @@ relative_files = true
context = '${CONTEXT}'
dynamic_context = "test_function"
omit = [
"tests/benchmarks/*",
"docs_src/response_model/tutorial003_04_py39.py",
"docs_src/response_model/tutorial003_04_py310.py",
"docs_src/dependencies/tutorial013_an_py310.py", # temporary code example?

View File

@@ -5,6 +5,7 @@ import time
from collections import Counter
from collections.abc import Container
from datetime import datetime, timedelta, timezone
from math import ceil
from pathlib import Path
from typing import Any
@@ -15,12 +16,63 @@ from pydantic import BaseModel, SecretStr
from pydantic_settings import BaseSettings
github_graphql_url = "https://api.github.com/graphql"
questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0"
questions_category_id = "DIC_kwDOCZduT84B6E2a"
POINTS_PER_MINUTE_LIMIT = 84 # 5000 points per hour
class RateLimiter:
def __init__(self) -> None:
self.last_query_cost: int = 1
self.remaining_points: int = 5000
self.reset_at: datetime = datetime.fromtimestamp(0, timezone.utc)
self.last_request_start_time: datetime = datetime.fromtimestamp(0, timezone.utc)
self.speed_multiplier: float = 1.0
def __enter__(self) -> "RateLimiter":
now = datetime.now(tz=timezone.utc)
# Handle primary rate limits
primary_limit_wait_time = 0.0
if self.remaining_points <= self.last_query_cost:
primary_limit_wait_time = (self.reset_at - now).total_seconds() + 2
logging.warning(
f"Approaching GitHub API rate limit, remaining points: {self.remaining_points}, "
f"reset time in {primary_limit_wait_time} seconds"
)
# Handle secondary rate limits
secondary_limit_wait_time = 0.0
points_per_minute = POINTS_PER_MINUTE_LIMIT * self.speed_multiplier
interval = 60 / (points_per_minute / self.last_query_cost)
time_since_last_request = (now - self.last_request_start_time).total_seconds()
if time_since_last_request < interval:
secondary_limit_wait_time = interval - time_since_last_request
final_wait_time = ceil(max(primary_limit_wait_time, secondary_limit_wait_time))
logging.info(f"Sleeping for {final_wait_time} seconds to respect rate limit")
time.sleep(max(final_wait_time, 1))
self.last_request_start_time = datetime.now(tz=timezone.utc)
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
pass
def update_request_info(self, cost: int, remaining: int, reset_at: str) -> None:
self.last_query_cost = cost
self.remaining_points = remaining
self.reset_at = datetime.fromisoformat(reset_at.replace("Z", "+00:00"))
rate_limiter = RateLimiter()
discussions_query = """
query Q($after: String, $category_id: ID) {
repository(name: "fastapi", owner: "fastapi") {
discussions(first: 100, after: $after, categoryId: $category_id) {
discussions(first: 30, after: $after, categoryId: $category_id) {
edges {
cursor
node {
@@ -58,6 +110,11 @@ query Q($after: String, $category_id: ID) {
}
}
}
rateLimit {
cost
remaining
resetAt
}
}
"""
@@ -120,7 +177,7 @@ class Settings(BaseSettings):
github_token: SecretStr
github_repository: str
httpx_timeout: int = 30
sleep_interval: int = 5
speed_multiplier: float = 1.0
def get_graphql_response(
@@ -158,11 +215,18 @@ def get_graphql_question_discussion_edges(
settings: Settings,
after: str | None = None,
) -> list[DiscussionsEdge]:
data = get_graphql_response(
settings=settings,
query=discussions_query,
after=after,
category_id=questions_category_id,
with rate_limiter:
data = get_graphql_response(
settings=settings,
query=discussions_query,
after=after,
category_id=questions_category_id,
)
rate_limiter.update_request_info(
cost=data["data"]["rateLimit"]["cost"],
remaining=data["data"]["rateLimit"]["remaining"],
reset_at=data["data"]["rateLimit"]["resetAt"],
)
graphql_response = DiscussionsResponse.model_validate(data)
return graphql_response.data.repository.discussions.edges
@@ -185,8 +249,6 @@ def get_discussion_nodes(settings: Settings) -> list[DiscussionsNode]:
for discussion_edge in discussion_edges:
discussion_nodes.append(discussion_edge.node)
last_edge = discussion_edges[-1]
# Handle GitHub secondary rate limits, requests per minute
time.sleep(settings.sleep_interval)
discussion_edges = get_graphql_question_discussion_edges(
settings=settings, after=last_edge.cursor
)
@@ -318,6 +380,7 @@ def main() -> None:
logging.basicConfig(level=logging.INFO)
settings = Settings()
logging.info(f"Using config: {settings.model_dump_json()}")
rate_limiter.speed_multiplier = settings.speed_multiplier
g = Github(settings.github_token.get_secret_value())
repo = g.get_repo(settings.github_repository)

View File

@@ -0,0 +1,44 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
app_default = FastAPI()
@app_default.post("/items/")
async def app_default_post(data: dict):
return data
app_lax = FastAPI(strict_content_type=False)
@app_lax.post("/items/")
async def app_lax_post(data: dict):
return data
client_default = TestClient(app_default)
client_lax = TestClient(app_lax)
def test_default_strict_rejects_no_content_type():
response = client_default.post("/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_default_strict_accepts_json_content_type():
response = client_default.post("/items/", json={"key": "value"})
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_lax_accepts_no_content_type():
response = client_lax.post("/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_lax_accepts_json_content_type():
response = client_lax.post("/items/", json={"key": "value"})
assert response.status_code == 200
assert response.json() == {"key": "value"}

View File

@@ -0,0 +1,91 @@
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
# Lax app with nested routers, inner overrides to strict
app_nested = FastAPI(strict_content_type=False) # lax app
outer_router = APIRouter(prefix="/outer") # inherits lax from app
inner_strict = APIRouter(prefix="/strict", strict_content_type=True)
inner_default = APIRouter(prefix="/default")
@inner_strict.post("/items/")
async def inner_strict_post(data: dict):
return data
@inner_default.post("/items/")
async def inner_default_post(data: dict):
return data
outer_router.include_router(inner_strict)
outer_router.include_router(inner_default)
app_nested.include_router(outer_router)
client_nested = TestClient(app_nested)
def test_strict_inner_on_lax_app_rejects_no_content_type():
response = client_nested.post("/outer/strict/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_default_inner_inherits_lax_from_app():
response = client_nested.post("/outer/default/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_strict_inner_accepts_json_content_type():
response = client_nested.post("/outer/strict/items/", json={"key": "value"})
assert response.status_code == 200
def test_default_inner_accepts_json_content_type():
response = client_nested.post("/outer/default/items/", json={"key": "value"})
assert response.status_code == 200
# Strict app -> lax outer router -> strict inner router
app_mixed = FastAPI(strict_content_type=True)
mixed_outer = APIRouter(prefix="/outer", strict_content_type=False)
mixed_inner = APIRouter(prefix="/inner", strict_content_type=True)
@mixed_outer.post("/items/")
async def mixed_outer_post(data: dict):
return data
@mixed_inner.post("/items/")
async def mixed_inner_post(data: dict):
return data
mixed_outer.include_router(mixed_inner)
app_mixed.include_router(mixed_outer)
client_mixed = TestClient(app_mixed)
def test_lax_outer_on_strict_app_accepts_no_content_type():
response = client_mixed.post("/outer/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_strict_inner_on_lax_outer_rejects_no_content_type():
response = client_mixed.post("/outer/inner/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_lax_outer_accepts_json_content_type():
response = client_mixed.post("/outer/items/", json={"key": "value"})
assert response.status_code == 200
def test_strict_inner_on_lax_outer_accepts_json_content_type():
response = client_mixed.post("/outer/inner/items/", json={"key": "value"})
assert response.status_code == 200

View File

@@ -0,0 +1,61 @@
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
router_lax = APIRouter(prefix="/lax", strict_content_type=False)
router_strict = APIRouter(prefix="/strict", strict_content_type=True)
router_default = APIRouter(prefix="/default")
@router_lax.post("/items/")
async def router_lax_post(data: dict):
return data
@router_strict.post("/items/")
async def router_strict_post(data: dict):
return data
@router_default.post("/items/")
async def router_default_post(data: dict):
return data
app.include_router(router_lax)
app.include_router(router_strict)
app.include_router(router_default)
client = TestClient(app)
def test_lax_router_on_strict_app_accepts_no_content_type():
response = client.post("/lax/items/", content='{"key": "value"}')
assert response.status_code == 200
assert response.json() == {"key": "value"}
def test_strict_router_on_strict_app_rejects_no_content_type():
response = client.post("/strict/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_default_router_inherits_strict_from_app():
response = client.post("/default/items/", content='{"key": "value"}')
assert response.status_code == 422
def test_lax_router_accepts_json_content_type():
response = client.post("/lax/items/", json={"key": "value"})
assert response.status_code == 200
def test_strict_router_accepts_json_content_type():
response = client.post("/strict/items/", json={"key": "value"})
assert response.status_code == 200
def test_default_router_accepts_json_content_type():
response = client.post("/default/items/", json={"key": "value"})
assert response.status_code == 200

View File

@@ -189,18 +189,12 @@ def test_geo_json(client: TestClient):
assert response.status_code == 200, response.text
def test_no_content_type_is_json(client: TestClient):
def test_no_content_type_json(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
)
assert response.status_code == 200, response.text
assert response.json() == {
"name": "Foo",
"description": None,
"price": 50.5,
"tax": None,
}
assert response.status_code == 422, response.text
def test_wrong_headers(client: TestClient):

View File

View File

@@ -0,0 +1,43 @@
import importlib
import pytest
from fastapi.testclient import TestClient
@pytest.fixture(
name="client",
params=[
"tutorial001_py310",
],
)
def get_client(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.strict_content_type.{request.param}")
client = TestClient(mod.app)
return client
def test_lax_post_without_content_type_is_parsed_as_json(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
)
assert response.status_code == 200, response.text
assert response.json() == {"name": "Foo", "price": 50.5}
def test_lax_post_with_json_content_type(client: TestClient):
response = client.post(
"/items/",
json={"name": "Foo", "price": 50.5},
)
assert response.status_code == 200, response.text
assert response.json() == {"name": "Foo", "price": 50.5}
def test_lax_post_with_text_plain_is_still_rejected(client: TestClient):
response = client.post(
"/items/",
content='{"name": "Foo", "price": 50.5}',
headers={"Content-Type": "text/plain"},
)
assert response.status_code == 422, response.text

View File

@@ -2,7 +2,7 @@ import pytest
from fastapi.testclient import TestClient
from fastapi.websockets import WebSocketDisconnect
from docs_src.websockets.tutorial001_py310 import app
from docs_src.websockets_.tutorial001_py310 import app
client = TestClient(app)

View File

@@ -16,7 +16,7 @@ from ...utils import needs_py310
],
)
def get_app(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.websockets.{request.param}")
mod = importlib.import_module(f"docs_src.websockets_.{request.param}")
return mod.app

View File

@@ -12,7 +12,7 @@ from fastapi.testclient import TestClient
],
)
def get_mod(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.websockets.{request.param}")
mod = importlib.import_module(f"docs_src.websockets_.{request.param}")
return mod

60
uv.lock generated
View File

@@ -1607,7 +1607,7 @@ wheels = [
[[package]]
name = "flask"
version = "3.1.2"
version = "3.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
@@ -1617,9 +1617,9 @@ dependencies = [
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
]
[[package]]
@@ -1923,40 +1923,36 @@ wheels = [
]
[[package]]
name = "griffe"
version = "1.15.0"
name = "griffelib"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" },
{ url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
]
[[package]]
name = "griffe-typingdoc"
version = "0.3.0"
version = "0.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffe" },
{ name = "griffelib" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/be/77/d5e5fa0a8391bc2890ae45255847197299739833108dd76ee3c9b2ff0bba/griffe_typingdoc-0.3.0.tar.gz", hash = "sha256:59d9ef98d02caa7aed88d8df1119c9e48c02ed049ea50ce4018ace9331d20f8b", size = 33169, upload-time = "2025-10-23T12:01:39.037Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/26/28182e0c8055842bf3da774dee1d5b789c0f236c078dcbdca1937b5214dc/griffe_typingdoc-0.3.1.tar.gz", hash = "sha256:2ff4703115cb7f8a65b9fdcdd1f3c3a15f813b6554621b52eaad094c4782ce96", size = 31218, upload-time = "2026-02-21T09:38:54.409Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/af/aa32c13f753e2625ec895b1f56eee3c9380a2088a88a2c028955e223856e/griffe_typingdoc-0.3.0-py3-none-any.whl", hash = "sha256:4f6483fff7733a679d1dce142fb029f314125f3caaf0d620eb82e7390c8564bb", size = 9923, upload-time = "2025-10-23T12:01:37.601Z" },
{ url = "https://files.pythonhosted.org/packages/b6/c4/cf543fbde49e1ae44830ef0840a4d6ee9f4e4f338138a7766d4e37cf6440/griffe_typingdoc-0.3.1-py3-none-any.whl", hash = "sha256:ecbd457ef6883126b8b6023abf12e08c58e1c152238a2f0e2afdd67a64b07021", size = 10092, upload-time = "2026-02-20T14:53:47.84Z" },
]
[[package]]
name = "griffe-warnings-deprecated"
version = "1.1.0"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffe" },
{ name = "griffelib" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7e/0e/f034e1714eb2c694d6196c75f77a02f9c69d19f9961c4804a016397bf3e5/griffe_warnings_deprecated-1.1.0.tar.gz", hash = "sha256:7bf21de327d59c66c7ce08d0166aa4292ce0577ff113de5878f428d102b6f7c5", size = 33260, upload-time = "2024-12-10T21:02:18.395Z" }
sdist = { url = "https://files.pythonhosted.org/packages/da/9e/fc86f1e9270f143a395a601de81aa42a871722c34d4b3c7763658dc2e04d/griffe_warnings_deprecated-1.1.1.tar.gz", hash = "sha256:9261369bf2acb8b5d24a0dc7895cce788208513d4349031d4ea315b979b2e99f", size = 26262, upload-time = "2026-02-21T09:38:55.858Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/4c/b7241f03ad1f22ec2eed33b0f90c4f8c949e3395c4b7488670b07225a20b/griffe_warnings_deprecated-1.1.0-py3-none-any.whl", hash = "sha256:e7b0e8bfd6e5add3945d4d9805b2a41c72409e456733965be276d55f01e8a7a2", size = 5854, upload-time = "2024-12-10T21:02:16.96Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3c/c2a9eee79bf2c8002d2fa370534bee93fdca39e8b1fc82e83d552d5d2c07/griffe_warnings_deprecated-1.1.1-py3-none-any.whl", hash = "sha256:4b7d765e82ca9139ed44ffe7bdebed0d3a46ce014ad5a35a2c22e9a16288737a", size = 6565, upload-time = "2026-02-20T15:35:23.577Z" },
]
[[package]]
@@ -3003,17 +2999,17 @@ python = [
[[package]]
name = "mkdocstrings-python"
version = "2.0.1"
version = "2.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "griffe" },
{ name = "griffelib" },
{ name = "mkdocs-autorefs" },
{ name = "mkdocstrings" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" }
sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" },
{ url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" },
]
[[package]]
@@ -3937,33 +3933,33 @@ email = [
[[package]]
name = "pydantic-ai"
version = "1.56.0"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic-ai-slim", extra = ["ag-ui", "anthropic", "bedrock", "cli", "cohere", "evals", "fastmcp", "google", "groq", "huggingface", "logfire", "mcp", "mistral", "openai", "retries", "temporal", "ui", "vertexai", "xai"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/1a/800a1e02b259152a49d4c11d9103784a7482c7e9b067eeea23e949d3d80f/pydantic_ai-1.56.0.tar.gz", hash = "sha256:643ff71612df52315b3b4c4b41543657f603f567223eb33245dc8098f005bdc4", size = 11795, upload-time = "2026-02-06T01:13:21.122Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/97/e3158fa976a29e9580ba1c59601590424bbb81179c359fd29de0dc23aa09/pydantic_ai-1.62.0.tar.gz", hash = "sha256:d6ae517e365ea3ea162ca8ae643f319e105b71b0b6218b83dcad1d1eb2e38c9b", size = 12130, upload-time = "2026-02-19T05:07:07.853Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/35/f4a7fd2b9962ddb9b021f76f293e74fda71da190bb74b57ed5b343c93022/pydantic_ai-1.56.0-py3-none-any.whl", hash = "sha256:b6b3ac74bdc004693834750da4420ea2cde0d3cbc3f134c0b7544f98f1c00859", size = 7222, upload-time = "2026-02-06T01:13:11.755Z" },
{ url = "https://files.pythonhosted.org/packages/bc/7a/053aebfab576603e95fcfce1139de4a87e12bd5a2ef1ba00007a931c3ff0/pydantic_ai-1.62.0-py3-none-any.whl", hash = "sha256:1eb88f745ae045e63da41ad68966e8876c964d0f023fbf5d6a3f5d243370bd04", size = 7227, upload-time = "2026-02-19T05:06:58.341Z" },
]
[[package]]
name = "pydantic-ai-slim"
version = "1.56.0"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "genai-prices" },
{ name = "griffe" },
{ name = "griffelib" },
{ name = "httpx" },
{ name = "opentelemetry-api" },
{ name = "pydantic" },
{ name = "pydantic-graph" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/5c/3a577825b9c1da8f287be7f2ee6fe9aab48bc8a80e65c8518052c589f51c/pydantic_ai_slim-1.56.0.tar.gz", hash = "sha256:9f9f9c56b1c735837880a515ae5661b465b40207b25f3a3434178098b2137f05", size = 415265, upload-time = "2026-02-06T01:13:23.58Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/8d/6350a49f2e4b636efbcfc233221420ab576e4ba4edba38254cb84ae4a1e6/pydantic_ai_slim-1.62.0.tar.gz", hash = "sha256:00d84f659107bbbd88823a3d3dbe7348385935a9870b9d7d4ba799256f6b6983", size = 422452, upload-time = "2026-02-19T05:07:10.292Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/4b/34682036528eeb9aaf093c2073540ddf399ab37b99d282a69ca41356f1aa/pydantic_ai_slim-1.56.0-py3-none-any.whl", hash = "sha256:d657e4113485020500b23b7390b0066e2a0277edc7577eaad2290735ca5dd7d5", size = 542270, upload-time = "2026-02-06T01:13:14.918Z" },
{ url = "https://files.pythonhosted.org/packages/3d/67/21e9b3b0944568662e3790c936226bd48a9f27c6b5f27b5916f5857bc4d8/pydantic_ai_slim-1.62.0-py3-none-any.whl", hash = "sha256:5210073fadd46f65859a67da67845093c487f025fa430ed027151f22ec684ab2", size = 549296, upload-time = "2026-02-19T05:07:01.624Z" },
]
[package.optional-dependencies]
@@ -4181,7 +4177,7 @@ wheels = [
[[package]]
name = "pydantic-graph"
version = "1.56.0"
version = "1.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@@ -4189,9 +4185,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ff/03/f92881cdb12d6f43e60e9bfd602e41c95408f06e2324d3729f7a194e2bcd/pydantic_graph-1.56.0.tar.gz", hash = "sha256:5e22972dbb43dbc379ab9944252ff864019abf3c7d465dcdf572fc8aec9a44a1", size = 58460, upload-time = "2026-02-06T01:13:26.708Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/b6/0b084c847ecd99624f4fbc5c8ecd3f67a2388a282a32612b2a68c3b3595f/pydantic_graph-1.62.0.tar.gz", hash = "sha256:efe56bee3a8ca35b11a3be6a5f7352419fe182ef1e1323a3267ee12dec95f3c7", size = 58529, upload-time = "2026-02-19T05:07:12.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/07/8c823eb4d196137c123d4d67434e185901d3cbaea3b0c2b7667da84e72c1/pydantic_graph-1.56.0-py3-none-any.whl", hash = "sha256:ec3f0a1d6fcedd4eb9c59fef45079c2ee4d4185878d70dae26440a9c974c6bb3", size = 72346, upload-time = "2026-02-06T01:13:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/f0/12/1a9cbcd59fd070ba72b0fe544caa6ca97758518643523ec2bf1162084e0d/pydantic_graph-1.62.0-py3-none-any.whl", hash = "sha256:abe0e7b356b4d3202b069ec020d8dd1f647f55e9a0e85cd272dab48250bde87d", size = 72350, upload-time = "2026-02-19T05:07:05.305Z" },
]
[[package]]