Browse Source

Merge branch 'master' into fix/sse-format-docstring-escape

pull/15613/head
AshNicolus 2 weeks ago
committed by GitHub
parent
commit
0df4aff2fe
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      .github/dependabot.yml
  2. 1
      docs/en/docs/deployment/manually.md
  3. 10
      docs/en/docs/fastapi-cli.md
  4. 26
      docs/en/docs/release-notes.md
  5. 2
      docs/en/docs/tutorial/body-multiple-params.md
  6. 10
      docs/en/docs/tutorial/first-steps.md
  7. 2
      docs/en/docs/tutorial/security/oauth2-jwt.md
  8. 2
      fastapi/__init__.py
  9. 4
      fastapi/dependencies/utils.py
  10. 23
      fastapi/sse.py
  11. 35
      tests/test_query_cookie_header_model_extra_params.py
  12. 9
      tests/test_sse.py
  13. 16
      tests/test_tutorial/test_security/test_tutorial005.py
  14. 1
      tests/test_tutorial/test_sql_databases/test_tutorial001.py
  15. 1
      tests/test_tutorial/test_sql_databases/test_tutorial002.py
  16. 899
      uv.lock

27
.github/dependabot.yml

@ -4,26 +4,47 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
cooldown:
default-days: 7
commit-message:
prefix:
labels:
- "internal"
- "dependencies"
- "github_actions"
groups:
github-actions:
patterns:
- "*"
# Python
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
cooldown:
default-days: 7
commit-message:
prefix:
groups:
python-packages:
dependency-type: "development"
patterns:
- "*"
# pre-commit
- package-ecosystem: "pre-commit"
directory: "/"
schedule:
interval: "daily"
interval: "weekly"
cooldown:
default-days: 7
commit-message:
prefix:
labels:
- "internal"
- "dependencies"
- "pre-commit"
groups:
pre-commit:
patterns:
- "*"

1
docs/en/docs/deployment/manually.md

@ -56,7 +56,6 @@ There are several alternatives, including:
* [Hypercorn](https://hypercorn.readthedocs.io/): an ASGI server compatible with HTTP/2 and Trio among other features.
* [Daphne](https://github.com/django/daphne): the ASGI server built for Django Channels.
* [Granian](https://github.com/emmett-framework/granian): A Rust HTTP server for Python applications.
* [NGINX Unit](https://unit.nginx.org/howto/fastapi/): NGINX Unit is a lightweight and versatile web application runtime.
## Server Machine and Server Program { #server-machine-and-server-program }

10
docs/en/docs/fastapi-cli.md

@ -95,7 +95,7 @@ which would be equivalent to:
from backend.main import app
```
### `fastapi dev` with path { #fastapi-dev-with-path }
### `fastapi dev` with path or with `--entrypoint` CLI option { #fastapi-dev-with-path-or-with-entrypoint-cli-option }
You can also pass the file path to the `fastapi dev` command, and it will guess the FastAPI app object to use:
@ -103,7 +103,13 @@ You can also pass the file path to the `fastapi dev` command, and it will guess
$ fastapi dev main.py
```
But you would have to remember to pass the correct path every time you call the `fastapi` command.
Or, you can also pass the `--entrypoint` option to the `fastapi dev` command:
```console
$ fastapi dev --entrypoint main:app
```
But you would have to remember to pass the correct path\entrypoint every time you call the `fastapi` command.
Additionally, other tools might not be able to find it, for example the [VS Code Extension](editor-support.md) or [FastAPI Cloud](https://fastapicloud.com), so it is recommended to use the `entrypoint` in `pyproject.toml`.

26
docs/en/docs/release-notes.md

@ -9,6 +9,30 @@ hide:
### Docs
* ✏️ Use `Annotated` in inline example in `docs/en/docs/tutorial/body-multiple-params.md`. PR [#15591](https://github.com/fastapi/fastapi/pull/15591) by [@TheArchons](https://github.com/TheArchons).
* 📝 Remove "NGINX Unit" from the list of ASGI-servers in docs. PR [#15475](https://github.com/fastapi/fastapi/pull/15475) by [@angryfoxx](https://github.com/angryfoxx).
* 📝 Update `docs/en/docs/tutorial/security/oauth2-jwt.md`. PR [#14781](https://github.com/fastapi/fastapi/pull/14781) by [@zadevhub](https://github.com/zadevhub).
### Internal
* ⬆ Bump the python-packages group with 15 updates. PR [#15594](https://github.com/fastapi/fastapi/pull/15594) by [@dependabot[bot]](https://github.com/apps/dependabot).
* 👷 Configure Dependabot to group updates and update weekly. PR [#15560](https://github.com/fastapi/fastapi/pull/15560) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.136.3 (2026-05-23)
### Refactors
* ♻️ Do not accept underscore headers when using `convert_underscores=True` (the default). PR [#15589](https://github.com/fastapi/fastapi/pull/15589) by [@tiangolo](https://github.com/tiangolo).
## 0.136.2 (2026-05-23)
### Refactors
* ♻️ Validate Server Sent Event fields to avoid applications from sending broken data. PR [#15588](https://github.com/fastapi/fastapi/pull/15588) by [@tiangolo](https://github.com/tiangolo).
### Docs
* 📝 Document `--entrypoint` CLI option. PR [#15464](https://github.com/fastapi/fastapi/pull/15464) by [@YuriiMotov](https://github.com/YuriiMotov).
* 📝 Update and simplify docs about help and management. PR [#15583](https://github.com/fastapi/fastapi/pull/15583) by [@tiangolo](https://github.com/tiangolo).
* 📝 Add docs references to central contributing docs. PR [#15580](https://github.com/fastapi/fastapi/pull/15580) by [@tiangolo](https://github.com/tiangolo).
* 📝 Update security policy. PR [#15577](https://github.com/fastapi/fastapi/pull/15577) by [@tiangolo](https://github.com/tiangolo).
@ -38,6 +62,8 @@ hide:
### Internal
* ✅ Update tests, don't double dispose the engine. PR [#15587](https://github.com/fastapi/fastapi/pull/15587) by [@tiangolo](https://github.com/tiangolo).
* ⚡️ Speed up test suite via caching and fixture scopes to make it ~24% faster. PR [#13583](https://github.com/fastapi/fastapi/pull/13583) by [@dikos1337](https://github.com/dikos1337).
* 🔥 Remove config files now in central GitHub repo. PR [#15585](https://github.com/fastapi/fastapi/pull/15585) by [@tiangolo](https://github.com/tiangolo).
* ⬆ Bump urllib3 from 2.6.3 to 2.7.0. PR [#15502](https://github.com/fastapi/fastapi/pull/15502) by [@dependabot[bot]](https://github.com/apps/dependabot).
* ⬆ Bump idna from 3.11 to 3.15. PR [#15565](https://github.com/fastapi/fastapi/pull/15565) by [@dependabot[bot]](https://github.com/apps/dependabot).

2
docs/en/docs/tutorial/body-multiple-params.md

@ -126,7 +126,7 @@ By default, **FastAPI** will then expect its body directly.
But if you want it to expect a JSON with a key `item` and inside of it the model contents, as it does when you declare extra body parameters, you can use the special `Body` parameter `embed`:
```Python
item: Item = Body(embed=True)
item: Annotated[Item, Body(embed=True)]
```
as in:

10
docs/en/docs/tutorial/first-steps.md

@ -180,7 +180,7 @@ which would be equivalent to:
from backend.main import app
```
### `fastapi dev` with path { #fastapi-dev-with-path }
### `fastapi dev` with path or with `--entrypoint` CLI option { #fastapi-dev-with-path-or-with-entrypoint-cli-option }
You can also pass the file path to the `fastapi dev` command, and it will guess the FastAPI app object to use:
@ -188,7 +188,13 @@ You can also pass the file path to the `fastapi dev` command, and it will guess
$ fastapi dev main.py
```
But you would have to remember to pass the correct path every time you call the `fastapi` command.
Or, you can also pass the `--entrypoint` option to the `fastapi dev` command:
```console
$ fastapi dev --entrypoint main:app
```
But you would have to remember to pass the correct path\entrypoint every time you call the `fastapi` command.
Additionally, other tools might not be able to find it, for example the [VS Code Extension](../editor-support.md) or [FastAPI Cloud](https://fastapicloud.com), so it is recommended to use the `entrypoint` in `pyproject.toml`.

2
docs/en/docs/tutorial/security/oauth2-jwt.md

@ -18,7 +18,7 @@ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4
It is not encrypted, so, anyone could recover the information from the contents.
But it's signed. So, when you receive a token that you emitted, you can verify that you actually emitted it.
But it's signed. So, when you receive a token that you issued, you can verify that it was you who issued it.
That way, you can create a token with an expiration of, let's say, 1 week. And then when the user comes back the next day with the token, you know that user is still logged in to your system.

2
fastapi/__init__.py

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

4
fastapi/dependencies/utils.py

@ -826,6 +826,10 @@ def request_params_to_args(
if value is not None:
params_to_process[get_validation_alias(field)] = value
processed_keys.add(alias or get_validation_alias(field))
# For headers with convert_underscores=True, mark both the converted
# header name and the original field alias as processed to avoid
# accepting the original alias as an extra header.
processed_keys.add(get_validation_alias(field))
for key in received_params.keys():
if key not in processed_keys:

23
fastapi/sse.py

@ -33,10 +33,20 @@ class EventSourceResponse(StreamingResponse):
media_type = "text/event-stream"
def _check_id_no_null(v: str | None) -> str | None:
def _check_single_line(v: str | None, field_name: str) -> str | None:
if v is not None and ("\r" in v or "\n" in v):
raise ValueError(f"SSE '{field_name}' must be a single line")
return v
def _check_event_single_line(v: str | None) -> str | None:
return _check_single_line(v, "event")
def _check_id_valid(v: str | None) -> str | None:
if v is not None and "\0" in v:
raise ValueError("SSE 'id' must not contain null characters")
return v
return _check_single_line(v, "id")
class ServerSentEvent(BaseModel):
@ -86,24 +96,27 @@ class ServerSentEvent(BaseModel):
] = None
event: Annotated[
str | None,
AfterValidator(_check_event_single_line),
Doc(
"""
Optional event type name.
Maps to `addEventListener(event, ...)` on the browser. When omitted,
the browser dispatches on the generic `message` event.
the browser dispatches on the generic `message` event. Must be a
single line.
"""
),
] = None
id: Annotated[
str | None,
AfterValidator(_check_id_no_null),
AfterValidator(_check_id_valid),
Doc(
"""
Optional event ID.
The browser sends this value back as the `Last-Event-ID` header on
automatic reconnection. **Must not contain null (`\\0`) characters.**
automatic reconnection. **Must be a single line** and must not contain
null (`\\0`) characters.
"""
),
] = None

35
tests/test_query_cookie_header_model_extra_params.py

@ -11,6 +11,10 @@ class Model(BaseModel):
model_config = {"extra": "allow"}
class AuthHeaders(BaseModel):
x_user_id: str
@app.get("/query")
async def query_model_with_extra(data: Model = Query()):
return data
@ -26,6 +30,11 @@ async def cookies_model_with_extra(data: Model = Cookie()):
return data
@app.get("/header-requires-hyphen")
async def header_model_requires_hyphen(data: AuthHeaders = Header()):
return data
def test_query_pass_extra_list():
client = TestClient(app)
resp = client.get(
@ -91,6 +100,32 @@ def test_header_pass_extra_single():
assert resp_json["param2"] == "456"
def test_header_model_prefers_hyphenated_header_with_convert_underscores():
client = TestClient(app)
resp = client.get(
"/header-requires-hyphen",
headers=[
("x-user-id", "hyphenated-value"),
("x_user_id", "underscore-value"),
],
)
assert resp.status_code == 200
assert resp.json() == {"x_user_id": "hyphenated-value"}
def test_header_model_rejects_underscore_header_with_convert_underscores():
client = TestClient(app)
resp = client.get(
"/header-requires-hyphen", headers={"x_user_id": "underscore-value"}
)
assert resp.status_code == 422
assert resp.json()["detail"][0]["loc"] == ["header", "x_user_id"]
def test_cookie_pass_extra_list():
client = TestClient(app)
client.cookies = [

9
tests/test_sse.py

@ -221,6 +221,15 @@ def test_server_sent_event_null_id_rejected():
ServerSentEvent(data="test", id="has\0null")
@pytest.mark.parametrize("field_name", ["event", "id"])
@pytest.mark.parametrize("value", ["first\nsecond", "first\rsecond", "first\r\nsecond"])
def test_server_sent_event_single_line_fields_reject_newlines(
field_name: str, value: str
):
with pytest.raises(ValueError, match=f"SSE '{field_name}' must be a single line"):
ServerSentEvent(data="test", **{field_name: value})
def test_server_sent_event_negative_retry_rejected():
with pytest.raises(ValueError):
ServerSentEvent(data="test", retry=-1)

16
tests/test_tutorial/test_security/test_tutorial005.py

@ -1,4 +1,5 @@
import importlib
from functools import lru_cache
from types import ModuleType
import pytest
@ -14,6 +15,7 @@ from ...utils import needs_py310
pytest.param("tutorial005_py310", marks=needs_py310),
pytest.param("tutorial005_an_py310", marks=needs_py310),
],
scope="module",
)
def get_mod(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.security.{request.param}")
@ -21,6 +23,20 @@ def get_mod(request: pytest.FixtureRequest):
return mod
@pytest.fixture(scope="module", autouse=True)
def cache_verify_password(mod: ModuleType):
assert hasattr(mod, "verify_password"), (
f"Module {mod.__name__} does not have attribute 'verify_password'"
)
original_func = mod.verify_password
cached_func = lru_cache()(original_func)
mod.verify_password = cached_func
yield
mod.verify_password = original_func
def get_access_token(
*, username="johndoe", password="secret", scope=None, client: TestClient
):

1
tests/test_tutorial/test_sql_databases/test_tutorial001.py

@ -25,6 +25,7 @@ def clear_sqlmodel():
pytest.param("tutorial001_py310", marks=needs_py310),
pytest.param("tutorial001_an_py310", marks=needs_py310),
],
scope="module",
)
def get_client(request: pytest.FixtureRequest):
clear_sqlmodel()

1
tests/test_tutorial/test_sql_databases/test_tutorial002.py

@ -25,6 +25,7 @@ def clear_sqlmodel():
pytest.param("tutorial002_py310", marks=needs_py310),
pytest.param("tutorial002_an_py310", marks=needs_py310),
],
scope="module",
)
def get_client(request: pytest.FixtureRequest):
clear_sqlmodel()

899
uv.lock

File diff suppressed because it is too large
Loading…
Cancel
Save