mirror of
https://github.com/fastapi/fastapi.git
synced 2026-05-27 09:46:50 -04:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbfd55cea3 | ||
|
|
59d4a80fcf | ||
|
|
6cbdde2315 | ||
|
|
1464678ba6 | ||
|
|
1a84bbc00b | ||
|
|
3fdc54edab | ||
|
|
09ba2cec98 | ||
|
|
3e3d38930e | ||
|
|
84f205c8f7 | ||
|
|
7baefe7144 | ||
|
|
2895c51ba8 | ||
|
|
21c46919fc | ||
|
|
8206485753 | ||
|
|
c910e0139f | ||
|
|
063b5bf582 |
27
.github/dependabot.yml
vendored
27
.github/dependabot.yml
vendored
@@ -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
.github/workflows/publish.yml
vendored
1
.github/workflows/publish.yml
vendored
@@ -29,6 +29,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||
with:
|
||||
version: "0.11.4"
|
||||
enable-cache: "false"
|
||||
- name: Build distribution
|
||||
run: uv build
|
||||
- name: Publish
|
||||
|
||||
11
.github/workflows/test.yml
vendored
11
.github/workflows/test.yml
vendored
@@ -81,6 +81,11 @@ jobs:
|
||||
uv-resolution: highest
|
||||
codspeed: codspeed
|
||||
deprecated-tests: "no-deprecation"
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.13"
|
||||
uv-resolution: highest
|
||||
deprecated-tests: "no-deprecation"
|
||||
without-httpx2: true
|
||||
- os: ubuntu-latest
|
||||
python-version: "3.14"
|
||||
coverage: coverage
|
||||
@@ -129,15 +134,19 @@ jobs:
|
||||
- name: Install deprecated libraries just for testing
|
||||
if: matrix.deprecated-tests == 'test-deprecation'
|
||||
run: uv pip install orjson ujson
|
||||
- name: Uninstall httpx2 to run tests with httpx
|
||||
if: matrix.without-httpx2 == 'true'
|
||||
run: uv pip uninstall httpx2
|
||||
- name: Reinstall SQLAlchemy without Cython extensions
|
||||
if: matrix.python-version == '3.14t' && matrix.os == 'ubuntu-latest'
|
||||
run: "DISABLE_SQLALCHEMY_CEXT=1 uv pip install --force-reinstall --no-binary :all: sqlalchemy"
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
run: uv run --no-sync bash scripts/test-cov.sh
|
||||
run: uv run --no-sync bash scripts/test-cov.sh $PYTEST_OPTIONS
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.deprecated-tests}}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-${{ matrix.deprecated-tests}}
|
||||
PYTEST_OPTIONS: ${{ (matrix.without-httpx2 == 'true') && '-W ignore::UserWarning' || '' }}
|
||||
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
|
||||
- name: Store coverage files
|
||||
if: matrix.coverage == 'coverage'
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -7,6 +7,24 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
### 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
|
||||
|
||||
* ✅ Add `httpx2` test dependency to avoid deprecation warning. PR [#15603](https://github.com/fastapi/fastapi/pull/15603) by [@YuriiMotov](https://github.com/YuriiMotov).
|
||||
* ⬆ 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.136.2"
|
||||
__version__ = "0.136.3"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -146,6 +146,7 @@ docs = [
|
||||
]
|
||||
docs-tests = [
|
||||
"httpx >=0.23.0,<1.0.0",
|
||||
"httpx2>=2.0.0",
|
||||
"ruff >=0.14.14",
|
||||
]
|
||||
github-actions = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
@@ -68,7 +69,9 @@ def test_header_param_model_invalid(client: TestClient):
|
||||
"x_tag": [],
|
||||
"host": "testserver",
|
||||
"accept": "*/*",
|
||||
"accept-encoding": "gzip, deflate",
|
||||
"accept-encoding": IsOneOf(
|
||||
"gzip, deflate", "gzip, deflate, zstd"
|
||||
),
|
||||
"connection": "keep-alive",
|
||||
"user-agent": "testclient",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsOneOf
|
||||
from fastapi.testclient import TestClient
|
||||
from inline_snapshot import snapshot
|
||||
|
||||
@@ -66,7 +67,9 @@ def test_header_param_model_no_underscore(client: TestClient):
|
||||
"traceparent": "123",
|
||||
"x_tag": [],
|
||||
"accept": "*/*",
|
||||
"accept-encoding": "gzip, deflate",
|
||||
"accept-encoding": IsOneOf(
|
||||
"gzip, deflate", "gzip, deflate, zstd"
|
||||
),
|
||||
"connection": "keep-alive",
|
||||
"user-agent": "testclient",
|
||||
"save-data": "true",
|
||||
@@ -105,7 +108,9 @@ def test_header_param_model_invalid(client: TestClient):
|
||||
"x_tag": [],
|
||||
"host": "testserver",
|
||||
"accept": "*/*",
|
||||
"accept-encoding": "gzip, deflate",
|
||||
"accept-encoding": IsOneOf(
|
||||
"gzip, deflate", "gzip, deflate, zstd"
|
||||
),
|
||||
"connection": "keep-alive",
|
||||
"user-agent": "testclient",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user