Browse Source

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

pull/15613/head
AshNicolus 1 month 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" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"
cooldown: cooldown:
default-days: 7 default-days: 7
commit-message: commit-message:
prefix: prefix:
labels:
- "internal"
- "dependencies"
- "github_actions"
groups:
github-actions:
patterns:
- "*"
# Python # Python
- package-ecosystem: "uv" - package-ecosystem: "uv"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"
cooldown: cooldown:
default-days: 7 default-days: 7
commit-message: commit-message:
prefix: prefix:
groups:
python-packages:
dependency-type: "development"
patterns:
- "*"
# pre-commit # pre-commit
- package-ecosystem: "pre-commit" - package-ecosystem: "pre-commit"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"
cooldown: cooldown:
default-days: 7 default-days: 7
commit-message: commit-message:
prefix: 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. * [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. * [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. * [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 } ## 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 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: 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 $ 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`. 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 ### 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). * 📝 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). * 📝 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). * 📝 Update security policy. PR [#15577](https://github.com/fastapi/fastapi/pull/15577) by [@tiangolo](https://github.com/tiangolo).
@ -38,6 +62,8 @@ hide:
### Internal ### 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). * 🔥 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 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). * ⬆ 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`: 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 ```Python
item: Item = Body(embed=True) item: Annotated[Item, Body(embed=True)]
``` ```
as in: as in:

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

@ -180,7 +180,7 @@ which would be equivalent to:
from backend.main import app 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: 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 $ 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`. 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. 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. 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""" """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 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: if value is not None:
params_to_process[get_validation_alias(field)] = value params_to_process[get_validation_alias(field)] = value
processed_keys.add(alias or get_validation_alias(field)) 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(): for key in received_params.keys():
if key not in processed_keys: if key not in processed_keys:

23
fastapi/sse.py

@ -33,10 +33,20 @@ class EventSourceResponse(StreamingResponse):
media_type = "text/event-stream" 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: if v is not None and "\0" in v:
raise ValueError("SSE 'id' must not contain null characters") raise ValueError("SSE 'id' must not contain null characters")
return v return _check_single_line(v, "id")
class ServerSentEvent(BaseModel): class ServerSentEvent(BaseModel):
@ -86,24 +96,27 @@ class ServerSentEvent(BaseModel):
] = None ] = None
event: Annotated[ event: Annotated[
str | None, str | None,
AfterValidator(_check_event_single_line),
Doc( Doc(
""" """
Optional event type name. Optional event type name.
Maps to `addEventListener(event, ...)` on the browser. When omitted, 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 ] = None
id: Annotated[ id: Annotated[
str | None, str | None,
AfterValidator(_check_id_no_null), AfterValidator(_check_id_valid),
Doc( Doc(
""" """
Optional event ID. Optional event ID.
The browser sends this value back as the `Last-Event-ID` header on 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 ] = None

35
tests/test_query_cookie_header_model_extra_params.py

@ -11,6 +11,10 @@ class Model(BaseModel):
model_config = {"extra": "allow"} model_config = {"extra": "allow"}
class AuthHeaders(BaseModel):
x_user_id: str
@app.get("/query") @app.get("/query")
async def query_model_with_extra(data: Model = Query()): async def query_model_with_extra(data: Model = Query()):
return data return data
@ -26,6 +30,11 @@ async def cookies_model_with_extra(data: Model = Cookie()):
return data return data
@app.get("/header-requires-hyphen")
async def header_model_requires_hyphen(data: AuthHeaders = Header()):
return data
def test_query_pass_extra_list(): def test_query_pass_extra_list():
client = TestClient(app) client = TestClient(app)
resp = client.get( resp = client.get(
@ -91,6 +100,32 @@ def test_header_pass_extra_single():
assert resp_json["param2"] == "456" 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(): def test_cookie_pass_extra_list():
client = TestClient(app) client = TestClient(app)
client.cookies = [ 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") 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(): def test_server_sent_event_negative_retry_rejected():
with pytest.raises(ValueError): with pytest.raises(ValueError):
ServerSentEvent(data="test", retry=-1) ServerSentEvent(data="test", retry=-1)

16
tests/test_tutorial/test_security/test_tutorial005.py

@ -1,4 +1,5 @@
import importlib import importlib
from functools import lru_cache
from types import ModuleType from types import ModuleType
import pytest import pytest
@ -14,6 +15,7 @@ from ...utils import needs_py310
pytest.param("tutorial005_py310", marks=needs_py310), pytest.param("tutorial005_py310", marks=needs_py310),
pytest.param("tutorial005_an_py310", marks=needs_py310), pytest.param("tutorial005_an_py310", marks=needs_py310),
], ],
scope="module",
) )
def get_mod(request: pytest.FixtureRequest): def get_mod(request: pytest.FixtureRequest):
mod = importlib.import_module(f"docs_src.security.{request.param}") mod = importlib.import_module(f"docs_src.security.{request.param}")
@ -21,6 +23,20 @@ def get_mod(request: pytest.FixtureRequest):
return mod 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( def get_access_token(
*, username="johndoe", password="secret", scope=None, client: TestClient *, 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_py310", marks=needs_py310),
pytest.param("tutorial001_an_py310", marks=needs_py310), pytest.param("tutorial001_an_py310", marks=needs_py310),
], ],
scope="module",
) )
def get_client(request: pytest.FixtureRequest): def get_client(request: pytest.FixtureRequest):
clear_sqlmodel() 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_py310", marks=needs_py310),
pytest.param("tutorial002_an_py310", marks=needs_py310), pytest.param("tutorial002_an_py310", marks=needs_py310),
], ],
scope="module",
) )
def get_client(request: pytest.FixtureRequest): def get_client(request: pytest.FixtureRequest):
clear_sqlmodel() clear_sqlmodel()

899
uv.lock

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