diff --git a/.github/labeler.yml b/.github/labeler.yml
index c5b1f84f3..cdaefbf2d 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -17,6 +17,7 @@ lang-all:
- docs/*/docs/**
- all-globs-to-all-files:
- '!docs/en/docs/**'
+ - '!docs/*/**/_*.md'
- '!fastapi/**'
- '!pyproject.toml'
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index f78b6730e..73e1c6b67 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -21,7 +21,7 @@ jobs:
outputs:
docs: ${{ steps.filter.outputs.docs }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
# For pull requests it's not necessary to checkout the code but for the main branch it is
- uses: dorny/paths-filter@v3
id: filter
@@ -32,12 +32,9 @@ jobs:
- docs/**
- docs_src/**
- requirements-docs.txt
- - requirements-docs-insiders.txt
- pyproject.toml
- mkdocs.yml
- - mkdocs.insiders.yml
- - mkdocs.maybe-insiders.yml
- - mkdocs.no-insiders.yml
+ - mkdocs.env.yml
- .github/workflows/build-docs.yml
- .github/workflows/deploy-docs.yml
- scripts/mkdocs_hooks.py
@@ -48,7 +45,7 @@ jobs:
outputs:
langs: ${{ steps.show-langs.outputs.langs }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -63,12 +60,6 @@ jobs:
pyproject.toml
- name: Install docs extras
run: uv pip install -r requirements-docs.txt
- # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps
- - name: Install Material for MkDocs Insiders
- if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
- run: uv pip install -r requirements-docs-insiders.txt
- env:
- TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}
- name: Verify Docs
run: python ./scripts/docs.py verify-docs
- name: Export Language Codes
@@ -90,7 +81,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -105,11 +96,6 @@ jobs:
pyproject.toml
- name: Install docs extras
run: uv pip install -r requirements-docs.txt
- - name: Install Material for MkDocs Insiders
- if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' )
- run: uv pip install -r requirements-docs-insiders.txt
- env:
- TOKEN: ${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}
- name: Update Languages
run: python ./scripts/docs.py update-languages
- uses: actions/cache@v4
diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml
index 7d5449c6a..2abd2fdcf 100644
--- a/.github/workflows/contributors.yml
+++ b/.github/workflows/contributors.yml
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index aa4fd6b65..50662a190 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -23,7 +23,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml
index e6ae3d963..7f16254db 100644
--- a/.github/workflows/label-approved.yml
+++ b/.github/workflows/label-approved.yml
@@ -20,7 +20,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml
index 2fa832fab..b9e45ea62 100644
--- a/.github/workflows/latest-changes.yml
+++ b/.github/workflows/latest-changes.yml
@@ -24,6 +24,8 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
+ # pin to actions/checkout@v5 for compatibility with latest-changes
+ # Ref: https://github.com/actions/checkout/issues/2313
- uses: actions/checkout@v5
with:
# To allow latest-changes to commit to the main branch
@@ -34,7 +36,7 @@ jobs:
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
with:
limit-access-to-actor: true
- - uses: tiangolo/latest-changes@0.4.0
+ - uses: tiangolo/latest-changes@0.4.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
latest_changes_file: docs/en/docs/release-notes.md
diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml
index 04beeb64e..971e6bbd8 100644
--- a/.github/workflows/notify-translations.yml
+++ b/.github/workflows/notify-translations.yml
@@ -28,7 +28,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml
index f15b92137..9b35a3d7e 100644
--- a/.github/workflows/people.yml
+++ b/.github/workflows/people.yml
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 000000000..fa0574d7d
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,88 @@
+name: pre-commit
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+
+env:
+ IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
+
+jobs:
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Dump GitHub context
+ env:
+ GITHUB_CONTEXT: ${{ toJson(github) }}
+ run: echo "$GITHUB_CONTEXT"
+ - uses: actions/checkout@v5
+ name: Checkout PR for own repo
+ if: env.IS_FORK == 'false'
+ with:
+ # To be able to commit it needs more than the last commit
+ ref: ${{ github.head_ref }}
+ # A token other than the default GITHUB_TOKEN is needed to be able to trigger CI
+ token: ${{ secrets.PRE_COMMIT }}
+ # pre-commit lite ci needs the default checkout configs to work
+ - uses: actions/checkout@v5
+ name: Checkout PR for fork
+ if: env.IS_FORK == 'true'
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.14"
+ - name: Setup uv
+ uses: astral-sh/setup-uv@v7
+ with:
+ cache-dependency-glob: |
+ requirements**.txt
+ pyproject.toml
+ uv.lock
+ - name: Install Dependencies
+ run: |
+ uv venv
+ uv pip install -r requirements.txt
+ - name: Run pre-commit
+ id: precommit
+ run: |
+ # Fetch the base branch for comparison
+ git fetch origin ${{ github.base_ref }}
+ uvx pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD --show-diff-on-failure
+ continue-on-error: true
+ - name: Commit and push changes
+ if: env.IS_FORK == 'false'
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add -A
+ if git diff --staged --quiet; then
+ echo "No changes to commit"
+ else
+ git commit -m "🎨 Auto format"
+ git push
+ fi
+ - uses: pre-commit-ci/lite-action@v1.1.0
+ if: env.IS_FORK == 'true'
+ with:
+ msg: 🎨 Auto format
+ - name: Error out on pre-commit errors
+ if: steps.precommit.outcome == 'failure'
+ run: exit 1
+
+ # https://github.com/marketplace/actions/alls-green#why
+ pre-commit-alls-green: # This job does nothing and is only used for the branch protection
+ if: always()
+ needs:
+ - pre-commit
+ runs-on: ubuntu-latest
+ steps:
+ - name: Dump GitHub context
+ env:
+ GITHUB_CONTEXT: ${{ toJson(github) }}
+ run: echo "$GITHUB_CONTEXT"
+ - name: Decide whether the needed jobs succeeded or failed
+ uses: re-actors/alls-green@release/v1
+ with:
+ jobs: ${{ toJSON(needs) }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 441eb4560..6d9a00b49 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -20,7 +20,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml
index eed5fbec0..84c743019 100644
--- a/.github/workflows/smokeshow.yml
+++ b/.github/workflows/smokeshow.yml
@@ -21,7 +21,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.9'
diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml
index 7d29469a5..8b0249001 100644
--- a/.github/workflows/sponsors.yml
+++ b/.github/workflows/sponsors.yml
@@ -24,7 +24,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/test-redistribute.yml b/.github/workflows/test-redistribute.yml
index a44f0b681..653ab2a74 100644
--- a/.github/workflows/test-redistribute.yml
+++ b/.github/workflows/test-redistribute.yml
@@ -22,7 +22,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9c3e2218b..8157e364b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -23,7 +23,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -65,7 +65,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -111,7 +111,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.8'
diff --git a/.github/workflows/topic-repos.yml b/.github/workflows/topic-repos.yml
index 22b37d59d..41dabee1e 100644
--- a/.github/workflows/topic-repos.yml
+++ b/.github/workflows/topic-repos.yml
@@ -19,7 +19,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/translate.yml b/.github/workflows/translate.yml
index a7fcf84df..6506b8e28 100644
--- a/.github/workflows/translate.yml
+++ b/.github/workflows/translate.yml
@@ -42,7 +42,7 @@ jobs:
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
diff --git a/.gitignore b/.gitignore
index ef6364a9a..6016ffa59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,6 @@ archive.zip
# macOS
.DS_Store
+
+# Ignore while the setup still depends on requirements.txt files
+uv.lock
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8e5eba4c4..8e6d93fb7 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,25 +1,29 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
-default_language_version:
- python: python3.10
repos:
-- repo: https://github.com/pre-commit/pre-commit-hooks
+ - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- - id: check-added-large-files
- - id: check-toml
- - id: check-yaml
+ - id: check-added-large-files
+ - id: check-toml
+ - id: check-yaml
args:
- - --unsafe
- - id: end-of-file-fixer
- - id: trailing-whitespace
-- repo: https://github.com/astral-sh/ruff-pre-commit
+ - --unsafe
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.3
hooks:
- - id: ruff
+ - id: ruff
args:
- --fix
- - id: ruff-format
-ci:
- autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
- autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
+ - id: ruff-format
+ - repo: local
+ hooks:
+ - id: local-script
+ language: unsupported
+ name: local script
+ entry: uv run ./scripts/docs.py add-permalinks-pages
+ args:
+ - --update-existing
+ files: ^docs/en/docs/.*\.md$
diff --git a/docs/en/data/contributors.yml b/docs/en/data/contributors.yml
index 592c79af0..163dc68e3 100644
--- a/docs/en/data/contributors.yml
+++ b/docs/en/data/contributors.yml
@@ -1,21 +1,21 @@
tiangolo:
login: tiangolo
- count: 794
+ count: 808
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo
dependabot:
login: dependabot
- count: 126
+ count: 130
avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4
url: https://github.com/apps/dependabot
alejsdev:
login: alejsdev
count: 52
- avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev
pre-commit-ci:
login: pre-commit-ci
- count: 49
+ count: 50
avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4
url: https://github.com/apps/pre-commit-ci
github-actions:
@@ -28,31 +28,31 @@ Kludex:
count: 25
avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=df8a3f06ba8f55ae1967a3e2d5ed882903a4e330&v=4
url: https://github.com/Kludex
+YuriiMotov:
+ login: YuriiMotov
+ count: 20
+ avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
+ url: https://github.com/YuriiMotov
dmontagu:
login: dmontagu
count: 17
avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4
url: https://github.com/dmontagu
-YuriiMotov:
- login: YuriiMotov
- count: 15
- avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
- url: https://github.com/YuriiMotov
nilslindemann:
login: nilslindemann
- count: 14
+ count: 15
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann
+svlandeg:
+ login: svlandeg
+ count: 14
+ avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
+ url: https://github.com/svlandeg
euri10:
login: euri10
count: 13
avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4
url: https://github.com/euri10
-svlandeg:
- login: svlandeg
- count: 13
- avatarUrl: https://avatars.githubusercontent.com/u/8796347?u=556c97650c27021911b0b9447ec55e75987b0e8a&v=4
- url: https://github.com/svlandeg
kantandane:
login: kantandane
count: 13
@@ -103,6 +103,11 @@ waynerv:
count: 5
avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4
url: https://github.com/waynerv
+musicinmybrain:
+ login: musicinmybrain
+ count: 5
+ avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
+ url: https://github.com/musicinmybrain
krishnamadhavan:
login: krishnamadhavan
count: 5
@@ -133,11 +138,6 @@ iudeen:
count: 4
avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=f09cdd745e5bf16138f29b42732dd57c7f02bee1&v=4
url: https://github.com/iudeen
-musicinmybrain:
- login: musicinmybrain
- count: 4
- avatarUrl: https://avatars.githubusercontent.com/u/6898909?u=9010312053e7141383b9bdf538036c7f37fbaba0&v=4
- url: https://github.com/musicinmybrain
philipokiokio:
login: philipokiokio
count: 4
@@ -483,6 +483,11 @@ nzig:
count: 2
avatarUrl: https://avatars.githubusercontent.com/u/7372858?u=e769add36ed73c778cdb136eb10bf96b1e119671&v=4
url: https://github.com/nzig
+kristjanvalur:
+ login: kristjanvalur
+ count: 2
+ avatarUrl: https://avatars.githubusercontent.com/u/6009543?u=1419f20bbfff8f031be8cb470962e7e62de2595e&v=4
+ url: https://github.com/kristjanvalur
yezz123:
login: yezz123
count: 2
diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml
index 3d8ecdb7a..24780603d 100644
--- a/docs/en/data/github_sponsors.yml
+++ b/docs/en/data/github_sponsors.yml
@@ -23,9 +23,6 @@ sponsors:
- login: railwayapp
avatarUrl: https://avatars.githubusercontent.com/u/66716858?v=4
url: https://github.com/railwayapp
- - login: scalar
- avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
- url: https://github.com/scalar
- - login: dribia
avatarUrl: https://avatars.githubusercontent.com/u/41189616?v=4
url: https://github.com/dribia
@@ -44,25 +41,25 @@ sponsors:
- login: permitio
avatarUrl: https://avatars.githubusercontent.com/u/71775833?v=4
url: https://github.com/permitio
-- - login: BoostryJP
- avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
- url: https://github.com/BoostryJP
- - login: mercedes-benz
- avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4
- url: https://github.com/mercedes-benz
- - login: Ponte-Energy-Partners
+- - login: Ponte-Energy-Partners
avatarUrl: https://avatars.githubusercontent.com/u/114745848?v=4
url: https://github.com/Ponte-Energy-Partners
- login: LambdaTest-Inc
avatarUrl: https://avatars.githubusercontent.com/u/171592363?u=96606606a45fa170427206199014f2a5a2a4920b&v=4
url: https://github.com/LambdaTest-Inc
+ - login: BoostryJP
+ avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4
+ url: https://github.com/BoostryJP
- login: requestly
avatarUrl: https://avatars.githubusercontent.com/u/12287519?v=4
url: https://github.com/requestly
- login: acsone
avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4
url: https://github.com/acsone
-- - login: Trivie
+- - login: scalar
+ avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4
+ url: https://github.com/scalar
+ - login: Trivie
avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4
url: https://github.com/Trivie
- - login: takashi-yoneya
@@ -71,42 +68,30 @@ sponsors:
- login: Doist
avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4
url: https://github.com/Doist
+ - login: bholagabbar
+ avatarUrl: https://avatars.githubusercontent.com/u/11693595?v=4
+ url: https://github.com/bholagabbar
- - login: mainframeindustries
avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4
url: https://github.com/mainframeindustries
- - login: alixlahuec
avatarUrl: https://avatars.githubusercontent.com/u/29543316?u=44357eb2a93bccf30fb9d389b8befe94a3d00985&v=4
url: https://github.com/alixlahuec
- - login: Partho
- avatarUrl: https://avatars.githubusercontent.com/u/2034301?u=ce195ac36835cca0cdfe6dd6e897bd38873a1524&v=4
- url: https://github.com/Partho
- - login: primer-io
avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4
url: https://github.com/primer-io
- - login: xsalagarcia
- avatarUrl: https://avatars.githubusercontent.com/u/66035908?v=4
- url: https://github.com/xsalagarcia
- - login: upciti
avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4
url: https://github.com/upciti
- - login: GonnaFlyMethod
- avatarUrl: https://avatars.githubusercontent.com/u/60840539?u=edf70b373fd4f1a83d3eb7c6802f4b6addb572cf&v=4
- url: https://github.com/GonnaFlyMethod
- login: ChargeStorm
avatarUrl: https://avatars.githubusercontent.com/u/26000165?v=4
url: https://github.com/ChargeStorm
- - login: DanielYang59
- avatarUrl: https://avatars.githubusercontent.com/u/80093591?u=63873f701c7c74aac83c906800a1dddc0bc8c92f&v=4
- url: https://github.com/DanielYang59
- login: nilslindemann
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann
- - login: samuelcolvin
avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4
url: https://github.com/samuelcolvin
- - login: vincentkoc
- avatarUrl: https://avatars.githubusercontent.com/u/25068?u=fbd5b2d51142daa4bdbc21e21953a3b8b8188a4a&v=4
- url: https://github.com/vincentkoc
- login: otosky
avatarUrl: https://avatars.githubusercontent.com/u/42260747?u=69d089387c743d89427aa4ad8740cfb34045a9e0&v=4
url: https://github.com/otosky
@@ -137,9 +122,6 @@ sponsors:
- login: jugeeem
avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4
url: https://github.com/jugeeem
- - login: connorpark24
- avatarUrl: https://avatars.githubusercontent.com/u/142128990?u=09b84a4beb1f629b77287a837bcf3729785cdd89&v=4
- url: https://github.com/connorpark24
- login: patsatsia
avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4
url: https://github.com/patsatsia
@@ -155,9 +137,9 @@ sponsors:
- login: kaoru0310
avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4
url: https://github.com/kaoru0310
- - login: DelfinaCare
- avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4
- url: https://github.com/DelfinaCare
+ - login: jstanden
+ avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
+ url: https://github.com/jstanden
- login: knallgelb
avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4
url: https://github.com/knallgelb
@@ -191,9 +173,6 @@ sponsors:
- login: oliverxchen
avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4
url: https://github.com/oliverxchen
- - login: jstanden
- avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4
- url: https://github.com/jstanden
- login: paulcwatts
avatarUrl: https://avatars.githubusercontent.com/u/150269?u=1819e145d573b44f0ad74b87206d21cd60331d4e&v=4
url: https://github.com/paulcwatts
@@ -233,9 +212,6 @@ sponsors:
- login: mjohnsey
avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4
url: https://github.com/mjohnsey
- - login: enguy-hub
- avatarUrl: https://avatars.githubusercontent.com/u/16822912?u=2c45f9e7f427b2f2f3b023d7fdb0d44764c92ae8&v=4
- url: https://github.com/enguy-hub
- login: ashi-agrawal
avatarUrl: https://avatars.githubusercontent.com/u/17105294?u=99c7a854035e5398d8e7b674f2d42baae6c957f8&v=4
url: https://github.com/ashi-agrawal
@@ -260,10 +236,7 @@ sponsors:
- - login: manoelpqueiroz
avatarUrl: https://avatars.githubusercontent.com/u/23669137?u=b12e84b28a84369ab5b30bd5a79e5788df5a0756&v=4
url: https://github.com/manoelpqueiroz
-- - login: ceb10n
- avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
- url: https://github.com/ceb10n
- - login: pawamoy
+- - login: pawamoy
avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4
url: https://github.com/pawamoy
- login: siavashyj
@@ -281,9 +254,9 @@ sponsors:
- login: hgalytoby
avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=6cc9028f3db63f8f60ad21c17b1ce4b88c4e2e60&v=4
url: https://github.com/hgalytoby
- - login: johnl28
- avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4
- url: https://github.com/johnl28
+ - login: nisutec
+ avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
+ url: https://github.com/nisutec
- login: hoenie-ams
avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4
url: https://github.com/hoenie-ams
@@ -299,21 +272,24 @@ sponsors:
- login: petercool
avatarUrl: https://avatars.githubusercontent.com/u/37613029?u=75aa8c6729e6e8f85a300561c4dbeef9d65c8797&v=4
url: https://github.com/petercool
+ - login: johnl28
+ avatarUrl: https://avatars.githubusercontent.com/u/54412955?u=47dd06082d1c39caa90c752eb55566e4f3813957&v=4
+ url: https://github.com/johnl28
- login: PunRabbit
avatarUrl: https://avatars.githubusercontent.com/u/70463212?u=1a835cfbc99295a60c8282f6aa6199d1b42241a5&v=4
url: https://github.com/PunRabbit
- login: PelicanQ
avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4
url: https://github.com/PelicanQ
+ - login: WillHogan
+ avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4
+ url: https://github.com/WillHogan
- login: my3
avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4
url: https://github.com/my3
- login: danielunderwood
avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4
url: https://github.com/danielunderwood
- - login: rangulvers
- avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
- url: https://github.com/rangulvers
- login: ddanier
avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4
url: https://github.com/ddanier
@@ -323,15 +299,21 @@ sponsors:
- login: slafs
avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4
url: https://github.com/slafs
+ - login: ceb10n
+ avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
+ url: https://github.com/ceb10n
- login: tochikuji
avatarUrl: https://avatars.githubusercontent.com/u/851759?v=4
url: https://github.com/tochikuji
- login: miguelgr
avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4
url: https://github.com/miguelgr
- - login: WillHogan
- avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=8a80356e3e7d5a417157aba7ea565dabc8678327&v=4
- url: https://github.com/WillHogan
+ - login: xncbf
+ avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
+ url: https://github.com/xncbf
+ - login: DMantis
+ avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
+ url: https://github.com/DMantis
- login: hard-coders
avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4
url: https://github.com/hard-coders
@@ -347,9 +329,9 @@ sponsors:
- login: joshuatz
avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4
url: https://github.com/joshuatz
- - login: nisutec
- avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4
- url: https://github.com/nisutec
+ - login: rangulvers
+ avatarUrl: https://avatars.githubusercontent.com/u/5235430?u=e254d4af4ace5a05fa58372ae677c7d26f0d5a53&v=4
+ url: https://github.com/rangulvers
- login: sdevkota
avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4
url: https://github.com/sdevkota
@@ -368,19 +350,7 @@ sponsors:
- login: moonape1226
avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4
url: https://github.com/moonape1226
- - login: xncbf
- avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=a80a7bb349555b277645632ed66639ff43400614&v=4
- url: https://github.com/xncbf
- - login: DMantis
- avatarUrl: https://avatars.githubusercontent.com/u/9536869?u=652dd0d49717803c0cbcbf44f7740e53cf2d4892&v=4
- url: https://github.com/DMantis
-- - login: morzan1001
- avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
- url: https://github.com/morzan1001
- - login: larsyngvelundin
- avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
- url: https://github.com/larsyngvelundin
- - login: andrecorumba
+- - login: andrecorumba
avatarUrl: https://avatars.githubusercontent.com/u/37807517?u=9b9be3b41da9bda60957da9ef37b50dbf65baa61&v=4
url: https://github.com/andrecorumba
- login: KOZ39
@@ -389,21 +359,30 @@ sponsors:
- login: rwxd
avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4
url: https://github.com/rwxd
+ - login: morzan1001
+ avatarUrl: https://avatars.githubusercontent.com/u/47593005?u=c30ab7230f82a12a9b938dcb54f84a996931409a&v=4
+ url: https://github.com/morzan1001
+ - login: Olegt0rr
+ avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
+ url: https://github.com/Olegt0rr
+ - login: dinoz0rg
+ avatarUrl: https://avatars.githubusercontent.com/u/32940067?u=739cda1eb123a2dd5e1db45c361396f239e23f8b&v=4
+ url: https://github.com/dinoz0rg
+ - login: larsyngvelundin
+ avatarUrl: https://avatars.githubusercontent.com/u/34173819?u=74958599695bf83ac9f1addd935a51548a10c6b0&v=4
+ url: https://github.com/larsyngvelundin
- login: hippoley
avatarUrl: https://avatars.githubusercontent.com/u/135493401?u=1164ef48a645a7c12664fabc1638fbb7e1c459b0&v=4
url: https://github.com/hippoley
+ - login: 4anklee
+ avatarUrl: https://avatars.githubusercontent.com/u/144109238?u=a79c0d581b2a3d8f3897e7ef4c012640a6c1eb3a&v=4
+ url: https://github.com/4anklee
- login: CoderDeltaLAN
avatarUrl: https://avatars.githubusercontent.com/u/152043745?u=4ff541efffb7d134e60c5fcf2dd1e343f90bb782&v=4
url: https://github.com/CoderDeltaLAN
- - login: chris1ding1
- avatarUrl: https://avatars.githubusercontent.com/u/194386334?u=5500604b50e35ed8a5aeb82ce34aa5d3ee3f88c7&v=4
- url: https://github.com/chris1ding1
- login: onestn
avatarUrl: https://avatars.githubusercontent.com/u/62360849?u=746dd21c34e7e06eefb11b03e8bb01aaae3c2a4f&v=4
url: https://github.com/onestn
- - login: Rubinskiy
- avatarUrl: https://avatars.githubusercontent.com/u/62457878?u=f2e35ed3d196a99cfadb5a29a91950342af07e34&v=4
- url: https://github.com/Rubinskiy
- login: nayasinghania
avatarUrl: https://avatars.githubusercontent.com/u/74111380?u=752e99a5e139389fdc0a0677122adc08438eb076&v=4
url: https://github.com/nayasinghania
@@ -413,9 +392,6 @@ sponsors:
- login: andreagrandi
avatarUrl: https://avatars.githubusercontent.com/u/636391?u=13d90cb8ec313593a5b71fbd4e33b78d6da736f5&v=4
url: https://github.com/andreagrandi
- - login: Olegt0rr
- avatarUrl: https://avatars.githubusercontent.com/u/25399456?u=3e87b5239a2f4600975ba13be73054f8567c6060&v=4
- url: https://github.com/Olegt0rr
- login: msserpa
avatarUrl: https://avatars.githubusercontent.com/u/6334934?u=82c4489eb1559d88d2990d60001901b14f722bbb&v=4
url: https://github.com/msserpa
diff --git a/docs/en/data/translation_reviewers.yml b/docs/en/data/translation_reviewers.yml
index 45aa55e5e..c3d3d0388 100644
--- a/docs/en/data/translation_reviewers.yml
+++ b/docs/en/data/translation_reviewers.yml
@@ -75,7 +75,7 @@ mattwang44:
url: https://github.com/mattwang44
tiangolo:
login: tiangolo
- count: 55
+ count: 56
avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=cb5d06e73a9e1998141b1641aa88e443c6717651&v=4
url: https://github.com/tiangolo
Laineyzhang55:
@@ -136,7 +136,7 @@ JavierSanchezCastro:
alejsdev:
login: alejsdev
count: 37
- avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev
stlucasgarcia:
login: stlucasgarcia
@@ -436,7 +436,7 @@ jburckel:
peidrao:
login: peidrao
count: 13
- avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=64c634bb10381905038ff7faf3c8c3df47fb799a&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=979c62398e16ff000cc0faa028e028efd679887c&v=4
url: https://github.com/peidrao
impocode:
login: impocode
@@ -1006,7 +1006,7 @@ takacs:
anton2yakovlev:
login: anton2yakovlev
count: 5
- avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=bdd445ba99074b378e7298d23c4bf6d707d2c282&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/44229180?u=ac245e57bc834ff80f08ca8128000bb650a77a3d&v=4
url: https://github.com/anton2yakovlev
ILoveSorasakiHina:
login: ILoveSorasakiHina
@@ -1161,7 +1161,7 @@ cookie-byte217:
AbolfazlKameli:
login: AbolfazlKameli
count: 4
- avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=e41743da3c1820efafc59c5870cacd4f4425334c&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/120686133?u=af8f025278cce0d489007071254e4055df60b78c&v=4
url: https://github.com/AbolfazlKameli
tyronedamasceno:
login: tyronedamasceno
@@ -1196,7 +1196,7 @@ Xaraxx:
Suyoung789:
login: Suyoung789
count: 3
- avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=744bd3e641413e19bfad6b06a90bb0887c3f9332&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/31277231?u=1591aaf651eb860017231a36590050e154c026b6&v=4
url: https://github.com/Suyoung789
akagaeng:
login: akagaeng
@@ -1806,7 +1806,7 @@ MrL8199:
ivintoiu:
login: ivintoiu
count: 2
- avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=b537c905ad08b69993de8796fb235c8d4d47f039&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/1853336?u=e3de5fd0ab17efc12256b4295285b504ca281440&v=4
url: https://github.com/ivintoiu
TechnoService2:
login: TechnoService2
@@ -1841,7 +1841,7 @@ NavesSapnis:
eqsdxr:
login: eqsdxr
count: 2
- avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=d7aaffb29f542b647cf0f6b0e05722490863658a&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/157279130?u=7927dc0366995334f9a18c3204a41d3a34d6d96f&v=4
url: https://github.com/eqsdxr
syedasamina56:
login: syedasamina56
diff --git a/docs/en/data/translators.yml b/docs/en/data/translators.yml
index a4b87e1bf..c66eff4d4 100644
--- a/docs/en/data/translators.yml
+++ b/docs/en/data/translators.yml
@@ -1,6 +1,6 @@
nilslindemann:
login: nilslindemann
- count: 124
+ count: 125
avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4
url: https://github.com/nilslindemann
jaystone776:
@@ -8,16 +8,16 @@ jaystone776:
count: 46
avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4
url: https://github.com/jaystone776
+ceb10n:
+ login: ceb10n
+ count: 29
+ avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
+ url: https://github.com/ceb10n
valentinDruzhinin:
login: valentinDruzhinin
count: 29
avatarUrl: https://avatars.githubusercontent.com/u/12831905?u=aae1ebc675c91e8fa582df4fcc4fc4128106344d&v=4
url: https://github.com/valentinDruzhinin
-ceb10n:
- login: ceb10n
- count: 27
- avatarUrl: https://avatars.githubusercontent.com/u/235213?u=edcce471814a1eba9f0cdaa4cd0de18921a940a6&v=4
- url: https://github.com/ceb10n
tokusumi:
login: tokusumi
count: 23
@@ -286,7 +286,7 @@ hsuanchi:
alejsdev:
login: alejsdev
count: 3
- avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=447d12a1b347f466b35378bee4c7104cc9b2c571&v=4
+ avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=85ceac49fb87138aebe8d663912e359447329090&v=4
url: https://github.com/alejsdev
riroan:
login: riroan
@@ -358,6 +358,11 @@ ruzia:
count: 3
avatarUrl: https://avatars.githubusercontent.com/u/24503?v=4
url: https://github.com/ruzia
+YuriiMotov:
+ login: YuriiMotov
+ count: 3
+ avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
+ url: https://github.com/YuriiMotov
izaguerreiro:
login: izaguerreiro
count: 2
@@ -543,8 +548,3 @@ EdmilsonRodrigues:
count: 2
avatarUrl: https://avatars.githubusercontent.com/u/62777025?u=217d6f3cd6cc750bb8818a3af7726c8d74eb7c2d&v=4
url: https://github.com/EdmilsonRodrigues
-YuriiMotov:
- login: YuriiMotov
- count: 2
- avatarUrl: https://avatars.githubusercontent.com/u/109919500?u=b9b13d598dddfab529a52d264df80a900bfe7060&v=4
- url: https://github.com/YuriiMotov
diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css
index a38df772f..87111ff64 100644
--- a/docs/en/docs/css/custom.css
+++ b/docs/en/docs/css/custom.css
@@ -1,3 +1,18 @@
+/* Fira Code, including characters used by Rich output, like the "heavy right-pointing angle bracket ornament", not included in Google Fonts */
+@import url(https://cdn.jsdelivr.net/npm/firacode@6.2.0/distr/fira_code.css);
+/* Noto Color Emoji for emoji support with the same font everywhere */
+@import url(https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap);
+
+/* Override default code font in Material for MkDocs to Fira Code */
+:root {
+ --md-code-font: "Fira Code", monospace, "Noto Color Emoji";
+}
+
+/* Override default regular font in Material for MkDocs to include Noto Color Emoji */
+:root {
+ --md-text-font: "Roboto", "Noto Color Emoji";
+}
+
.termynal-comment {
color: #4a968f;
font-style: italic;
diff --git a/docs/en/docs/css/termynal.css b/docs/en/docs/css/termynal.css
index 8534f9102..a2564e286 100644
--- a/docs/en/docs/css/termynal.css
+++ b/docs/en/docs/css/termynal.css
@@ -20,7 +20,7 @@
/* font-size: 18px; */
font-size: 15px;
/* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */
- font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
+ font-family: var(--md-code-font-family), 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace;
border-radius: 4px;
padding: 75px 45px 35px;
position: relative;
diff --git a/docs/en/docs/how-to/authentication-error-status-code.md b/docs/en/docs/how-to/authentication-error-status-code.md
new file mode 100644
index 000000000..f9433e5dd
--- /dev/null
+++ b/docs/en/docs/how-to/authentication-error-status-code.md
@@ -0,0 +1,17 @@
+# Use Old 403 Authentication Error Status Codes { #use-old-403-authentication-error-status-codes }
+
+Before FastAPI version `0.122.0`, when the integrated security utilities returned an error to the client after a failed authentication, they used the HTTP status code `403 Forbidden`.
+
+Starting with FastAPI version `0.122.0`, they use the more appropriate HTTP status code `401 Unauthorized`, and return a sensible `WWW-Authenticate` header in the response, following the HTTP specifications, RFC 7235, RFC 9110.
+
+But if for some reason your clients depend on the old behavior, you can revert to it by overriding the method `make_not_authenticated_error` in your security classes.
+
+For example, you can create a subclass of `HTTPBearer` that returns a `403 Forbidden` error instead of the default `401 Unauthorized` error:
+
+{* ../../docs_src/authentication_error_status_code/tutorial001_an_py39.py hl[9:13] *}
+
+/// tip
+
+Notice that the function returns the exception instance, it doesn't raise it. The raising is done in the rest of the internal code.
+
+///
diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md
index c02fe075d..93a4c5c97 100644
--- a/docs/en/docs/release-notes.md
+++ b/docs/en/docs/release-notes.md
@@ -7,6 +7,50 @@ hide:
## Latest Changes
+### Internal
+
+* ⬆ Bump markdown-include-variants from 0.0.6 to 0.0.7. PR [#14423](https://github.com/fastapi/fastapi/pull/14423) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 👥 Update FastAPI People - Sponsors. PR [#14422](https://github.com/fastapi/fastapi/pull/14422) by [@tiangolo](https://github.com/tiangolo).
+* 👥 Update FastAPI People - Contributors and Translators. PR [#14420](https://github.com/fastapi/fastapi/pull/14420) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.123.0
+
+### Fixes
+
+* 🐛 Cache dependencies that don't use scopes and don't have sub-dependencies with scopes. PR [#14419](https://github.com/fastapi/fastapi/pull/14419) by [@tiangolo](https://github.com/tiangolo).
+
+## 0.122.1
+
+### Fixes
+
+* 🐛 Fix hierarchical security scope propagation. PR [#5624](https://github.com/fastapi/fastapi/pull/5624) by [@kristjanvalur](https://github.com/kristjanvalur).
+
+### Docs
+
+* 💅 Update CSS to explicitly use emoji font. PR [#14415](https://github.com/fastapi/fastapi/pull/14415) by [@tiangolo](https://github.com/tiangolo).
+
+### Internal
+
+* ⬆ Bump markdown-include-variants from 0.0.5 to 0.0.6. PR [#14418](https://github.com/fastapi/fastapi/pull/14418) by [@YuriiMotov](https://github.com/YuriiMotov).
+
+## 0.122.0
+
+### Fixes
+
+* 🐛 Use `401` status code in security classes when credentials are missing. PR [#13786](https://github.com/fastapi/fastapi/pull/13786) by [@YuriiMotov](https://github.com/YuriiMotov).
+ * If your code depended on these classes raising the old (less correct) `403` status code, check the new docs about how to override the classes, to use the same old behavior: [Use Old 403 Authentication Error Status Codes](https://fastapi.tiangolo.com/how-to/authentication-error-status-code/).
+
+### Internal
+
+* 🔧 Configure labeler to exclude files that start from underscore for `lang-all` label. PR [#14213](https://github.com/fastapi/fastapi/pull/14213) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 👷 Add pre-commit config with local script for permalinks. PR [#14398](https://github.com/fastapi/fastapi/pull/14398) by [@tiangolo](https://github.com/tiangolo).
+* 💄 Use font Fira Code to fix display of Rich panels in docs in Windows. PR [#14387](https://github.com/fastapi/fastapi/pull/14387) by [@tiangolo](https://github.com/tiangolo).
+* 👷 Add custom pre-commit CI. PR [#14397](https://github.com/fastapi/fastapi/pull/14397) by [@tiangolo](https://github.com/tiangolo).
+* ⬆ Bump actions/checkout from 5 to 6. PR [#14381](https://github.com/fastapi/fastapi/pull/14381) by [@dependabot[bot]](https://github.com/apps/dependabot).
+* 👷 Upgrade `latest-changes` GitHub Action and pin `actions/checkout@v5`. PR [#14403](https://github.com/fastapi/fastapi/pull/14403) by [@svlandeg](https://github.com/svlandeg).
+* 🛠️ Add `add-permalinks` and `add-permalinks-page` to `scripts/docs.py`. PR [#14033](https://github.com/fastapi/fastapi/pull/14033) by [@YuriiMotov](https://github.com/YuriiMotov).
+* 🔧 Upgrade Material for MkDocs and remove insiders. PR [#14375](https://github.com/fastapi/fastapi/pull/14375) by [@tiangolo](https://github.com/tiangolo).
+
## 0.121.3
### Refactors
diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.env.yml
similarity index 78%
rename from docs/en/mkdocs.maybe-insiders.yml
rename to docs/en/mkdocs.env.yml
index 37fd9338e..c5f6e07d7 100644
--- a/docs/en/mkdocs.maybe-insiders.yml
+++ b/docs/en/mkdocs.env.yml
@@ -1,6 +1,5 @@
# Define this here and not in the main mkdocs.yml file because that one is auto
# updated and written, and the script would remove the env var
-INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml']
markdown_extensions:
pymdownx.highlight:
linenums: !ENV [LINENUMS, false]
diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml
deleted file mode 100644
index 8d6d26e17..000000000
--- a/docs/en/mkdocs.insiders.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-plugins:
- social:
- cards_layout_options:
- logo: ../en/docs/img/icon-white.svg
- typeset:
-markdown_extensions:
- material.extensions.preview:
- targets:
- include:
- - "*"
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index df47c6f9c..fd346a3d3 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -1,4 +1,4 @@
-INHERIT: ../en/mkdocs.maybe-insiders.yml
+INHERIT: ../en/mkdocs.env.yml
site_name: FastAPI
site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production
site_url: https://fastapi.tiangolo.com/
@@ -52,6 +52,10 @@ theme:
repo_name: fastapi/fastapi
repo_url: https://github.com/fastapi/fastapi
plugins:
+ social:
+ cards_layout_options:
+ logo: ../en/docs/img/icon-white.svg
+ typeset:
search: null
macros:
include_yaml:
@@ -211,6 +215,7 @@ nav:
- how-to/custom-docs-ui-assets.md
- how-to/configure-swagger-ui.md
- how-to/testing-database.md
+ - how-to/authentication-error-status-code.md
- Reference (Code API):
- reference/index.md
- reference/fastapi.md
@@ -253,6 +258,10 @@ nav:
- management.md
- release-notes.md
markdown_extensions:
+ material.extensions.preview:
+ targets:
+ include:
+ - "*"
abbr: null
attr_list: null
footnotes: null
diff --git a/docs_src/authentication_error_status_code/tutorial001_an.py b/docs_src/authentication_error_status_code/tutorial001_an.py
new file mode 100644
index 000000000..40678e858
--- /dev/null
+++ b/docs_src/authentication_error_status_code/tutorial001_an.py
@@ -0,0 +1,20 @@
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+from typing_extensions import Annotated
+
+app = FastAPI()
+
+
+class HTTPBearer403(HTTPBearer):
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
+ )
+
+
+CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
+
+
+@app.get("/me")
+def read_me(credentials: CredentialsDep):
+ return {"message": "You are authenticated", "token": credentials.credentials}
diff --git a/docs_src/authentication_error_status_code/tutorial001_an_py39.py b/docs_src/authentication_error_status_code/tutorial001_an_py39.py
new file mode 100644
index 000000000..7bbc2f717
--- /dev/null
+++ b/docs_src/authentication_error_status_code/tutorial001_an_py39.py
@@ -0,0 +1,21 @@
+from typing import Annotated
+
+from fastapi import Depends, FastAPI, HTTPException, status
+from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
+
+app = FastAPI()
+
+
+class HTTPBearer403(HTTPBearer):
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN, detail="Not authenticated"
+ )
+
+
+CredentialsDep = Annotated[HTTPAuthorizationCredentials, Depends(HTTPBearer403())]
+
+
+@app.get("/me")
+def read_me(credentials: CredentialsDep):
+ return {"message": "You are authenticated", "token": credentials.credentials}
diff --git a/docs_src/security/tutorial003.py b/docs_src/security/tutorial003.py
index 4b324866f..ce7a71b68 100644
--- a/docs_src/security/tutorial003.py
+++ b/docs_src/security/tutorial003.py
@@ -60,7 +60,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py
index 8fb40dd4a..1b7056a20 100644
--- a/docs_src/security/tutorial003_an.py
+++ b/docs_src/security/tutorial003_an.py
@@ -61,7 +61,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py
index ced4a2fbc..4a2743f6f 100644
--- a/docs_src/security/tutorial003_an_py310.py
+++ b/docs_src/security/tutorial003_an_py310.py
@@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py
index 068a3933e..b396210c8 100644
--- a/docs_src/security/tutorial003_an_py39.py
+++ b/docs_src/security/tutorial003_an_py39.py
@@ -60,7 +60,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/docs_src/security/tutorial003_py310.py b/docs_src/security/tutorial003_py310.py
index af935e997..081259b31 100644
--- a/docs_src/security/tutorial003_py310.py
+++ b/docs_src/security/tutorial003_py310.py
@@ -58,7 +58,7 @@ async def get_current_user(token: str = Depends(oauth2_scheme)):
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
+ detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
return user
diff --git a/fastapi/__init__.py b/fastapi/__init__.py
index 85a7ea7b5..25ed2bbeb 100644
--- a/fastapi/__init__.py
+++ b/fastapi/__init__.py
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
-__version__ = "0.121.3"
+__version__ = "0.123.0"
from starlette import status as status
diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index ae487a6f2..0522b8841 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -38,19 +38,43 @@ class Dependant:
response_param_names: Set[str] = field(default_factory=set)
background_tasks_param_names: Set[str] = field(default_factory=set)
security_scopes_param_names: Set[str] = field(default_factory=set)
- security_scopes: Optional[List[str]] = None
+ own_oauth_scopes: Optional[List[str]] = None
+ parent_oauth_scopes: Optional[List[str]] = None
use_cache: bool = True
path: Optional[str] = None
scope: Union[Literal["function", "request"], None] = None
+ @cached_property
+ def oauth_scopes(self) -> List[str]:
+ scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else []
+ # This doesn't use a set to preserve order, just in case
+ for scope in self.own_oauth_scopes or []:
+ if scope not in scopes:
+ scopes.append(scope)
+ return scopes
+
@cached_property
def cache_key(self) -> DependencyCacheKey:
+ scopes_for_cache = (
+ tuple(sorted(set(self.oauth_scopes or []))) if self._uses_scopes else ()
+ )
return (
self.call,
- tuple(sorted(set(self.security_scopes or []))),
+ scopes_for_cache,
self.computed_scope or "",
)
+ @cached_property
+ def _uses_scopes(self) -> bool:
+ if self.own_oauth_scopes:
+ return True
+ if self.security_scopes_param_name is not None:
+ return True
+ for sub_dep in self.dependencies:
+ if sub_dep._uses_scopes:
+ return True
+ return False
+
@cached_property
def is_gen_callable(self) -> bool:
if inspect.isgeneratorfunction(self.call):
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 10405c58f..07bf2cbb6 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -58,8 +58,7 @@ from fastapi.dependencies.models import Dependant, SecurityRequirement
from fastapi.exceptions import DependencyScopeError
from fastapi.logger import logger
from fastapi.security.base import SecurityBase
-from fastapi.security.oauth2 import OAuth2, SecurityScopes
-from fastapi.security.open_id_connect_url import OpenIdConnect
+from fastapi.security.oauth2 import SecurityScopes
from fastapi.types import DependencyCacheKey
from fastapi.utils import create_model_field, get_path_param_names
from pydantic import BaseModel
@@ -126,14 +125,14 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De
assert callable(depends.dependency), (
"A parameter-less dependency must have a callable dependency"
)
- use_security_scopes: List[str] = []
+ own_oauth_scopes: List[str] = []
if isinstance(depends, params.Security) and depends.scopes:
- use_security_scopes.extend(depends.scopes)
+ own_oauth_scopes.extend(depends.scopes)
return get_dependant(
path=path,
call=depends.dependency,
scope=depends.scope,
- security_scopes=use_security_scopes,
+ own_oauth_scopes=own_oauth_scopes,
)
@@ -232,7 +231,8 @@ def get_dependant(
path: str,
call: Callable[..., Any],
name: Optional[str] = None,
- security_scopes: Optional[List[str]] = None,
+ own_oauth_scopes: Optional[List[str]] = None,
+ parent_oauth_scopes: Optional[List[str]] = None,
use_cache: bool = True,
scope: Union[Literal["function", "request"], None] = None,
) -> Dependant:
@@ -240,19 +240,18 @@ def get_dependant(
call=call,
name=name,
path=path,
- security_scopes=security_scopes,
use_cache=use_cache,
scope=scope,
+ own_oauth_scopes=own_oauth_scopes,
+ parent_oauth_scopes=parent_oauth_scopes,
)
+ current_scopes = (parent_oauth_scopes or []) + (own_oauth_scopes or [])
path_param_names = get_path_param_names(path)
endpoint_signature = get_typed_signature(call)
signature_params = endpoint_signature.parameters
if isinstance(call, SecurityBase):
- use_scopes: List[str] = []
- if isinstance(call, (OAuth2, OpenIdConnect)):
- use_scopes = security_scopes or use_scopes
security_requirement = SecurityRequirement(
- security_scheme=call, scopes=use_scopes
+ security_scheme=call, scopes=current_scopes
)
dependant.security_requirements.append(security_requirement)
for param_name, param in signature_params.items():
@@ -275,15 +274,16 @@ def get_dependant(
f'The dependency "{dependant.call.__name__}" has a scope of '
'"request", it cannot depend on dependencies with scope "function".'
)
- use_security_scopes = security_scopes or []
+ sub_own_oauth_scopes: List[str] = []
if isinstance(param_details.depends, params.Security):
if param_details.depends.scopes:
- use_security_scopes.extend(param_details.depends.scopes)
+ sub_own_oauth_scopes = list(param_details.depends.scopes)
sub_dependant = get_dependant(
path=path,
call=param_details.depends.dependency,
name=param_name,
- security_scopes=use_security_scopes,
+ own_oauth_scopes=sub_own_oauth_scopes,
+ parent_oauth_scopes=current_scopes,
use_cache=param_details.depends.use_cache,
scope=param_details.depends.scope,
)
@@ -609,7 +609,7 @@ async def solve_dependencies(
path=use_path,
call=call,
name=sub_dependant.name,
- security_scopes=sub_dependant.security_scopes,
+ parent_oauth_scopes=sub_dependant.oauth_scopes,
scope=sub_dependant.scope,
)
@@ -693,7 +693,7 @@ async def solve_dependencies(
for name in dependant.response_param_names:
values[name] = response
if dependant.security_scopes_param_names:
- security_scope = SecurityScopes(scopes=dependant.security_scopes)
+ security_scope = SecurityScopes(scopes=dependant.oauth_scopes)
for name in dependant.security_scopes_param_names:
values[name] = security_scope
return SolvedDependency(
diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py
index 496c815a7..81c7be10d 100644
--- a/fastapi/security/api_key.py
+++ b/fastapi/security/api_key.py
@@ -1,22 +1,52 @@
-from typing import Optional
+from typing import Optional, Union
from annotated_doc import Doc
from fastapi.openapi.models import APIKey, APIKeyIn
from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException
from starlette.requests import Request
-from starlette.status import HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated
class APIKeyBase(SecurityBase):
- @staticmethod
- def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]:
+ def __init__(
+ self,
+ location: APIKeyIn,
+ name: str,
+ description: Union[str, None],
+ scheme_name: Union[str, None],
+ auto_error: bool,
+ ):
+ self.auto_error = auto_error
+
+ self.model: APIKey = APIKey(
+ **{"in": location},
+ name=name,
+ description=description,
+ )
+ self.scheme_name = scheme_name or self.__class__.__name__
+
+ def make_not_authenticated_error(self) -> HTTPException:
+ """
+ The WWW-Authenticate header is not standardized for API Key authentication but
+ the HTTP specification requires that an error of 401 "Unauthorized" must
+ include a WWW-Authenticate header.
+
+ Ref: https://datatracker.ietf.org/doc/html/rfc9110#name-401-unauthorized
+
+ For this, this method sends a custom challenge `APIKey`.
+ """
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "APIKey"},
+ )
+
+ def check_api_key(self, api_key: Optional[str]) -> Optional[str]:
if not api_key:
- if auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ if self.auto_error:
+ raise self.make_not_authenticated_error()
return None
return api_key
@@ -100,17 +130,17 @@ class APIKeyQuery(APIKeyBase):
),
] = True,
):
- self.model: APIKey = APIKey(
- **{"in": APIKeyIn.query},
+ super().__init__(
+ location=APIKeyIn.query,
name=name,
+ scheme_name=scheme_name,
description=description,
+ auto_error=auto_error,
)
- self.scheme_name = scheme_name or self.__class__.__name__
- self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.query_params.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key)
class APIKeyHeader(APIKeyBase):
@@ -188,17 +218,17 @@ class APIKeyHeader(APIKeyBase):
),
] = True,
):
- self.model: APIKey = APIKey(
- **{"in": APIKeyIn.header},
+ super().__init__(
+ location=APIKeyIn.header,
name=name,
+ scheme_name=scheme_name,
description=description,
+ auto_error=auto_error,
)
- self.scheme_name = scheme_name or self.__class__.__name__
- self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.headers.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key)
class APIKeyCookie(APIKeyBase):
@@ -276,14 +306,14 @@ class APIKeyCookie(APIKeyBase):
),
] = True,
):
- self.model: APIKey = APIKey(
- **{"in": APIKeyIn.cookie},
+ super().__init__(
+ location=APIKeyIn.cookie,
name=name,
+ scheme_name=scheme_name,
description=description,
+ auto_error=auto_error,
)
- self.scheme_name = scheme_name or self.__class__.__name__
- self.auto_error = auto_error
async def __call__(self, request: Request) -> Optional[str]:
api_key = request.cookies.get(self.model.name)
- return self.check_api_key(api_key, self.auto_error)
+ return self.check_api_key(api_key)
diff --git a/fastapi/security/http.py b/fastapi/security/http.py
index 3a5985650..0d1bbba3a 100644
--- a/fastapi/security/http.py
+++ b/fastapi/security/http.py
@@ -1,6 +1,6 @@
import binascii
from base64 import b64decode
-from typing import Optional
+from typing import Dict, Optional
from annotated_doc import Doc
from fastapi.exceptions import HTTPException
@@ -10,7 +10,7 @@ from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel
from starlette.requests import Request
-from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated
@@ -76,10 +76,22 @@ class HTTPBase(SecurityBase):
description: Optional[str] = None,
auto_error: bool = True,
):
- self.model = HTTPBaseModel(scheme=scheme, description=description)
+ self.model: HTTPBaseModel = HTTPBaseModel(
+ scheme=scheme, description=description
+ )
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
+ def make_authenticate_headers(self) -> Dict[str, str]:
+ return {"WWW-Authenticate": f"{self.model.scheme.title()}"}
+
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers=self.make_authenticate_headers(),
+ )
+
async def __call__(
self, request: Request
) -> Optional[HTTPAuthorizationCredentials]:
@@ -87,9 +99,7 @@ class HTTPBase(SecurityBase):
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@@ -99,6 +109,8 @@ class HTTPBasic(HTTPBase):
"""
HTTP Basic authentication.
+ Ref: https://datatracker.ietf.org/doc/html/rfc7617
+
## Usage
Create an instance object and use that object as the dependency in `Depends()`.
@@ -185,36 +197,28 @@ class HTTPBasic(HTTPBase):
self.realm = realm
self.auto_error = auto_error
+ def make_authenticate_headers(self) -> Dict[str, str]:
+ if self.realm:
+ return {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
+ return {"WWW-Authenticate": "Basic"}
+
async def __call__( # type: ignore
self, request: Request
) -> Optional[HTTPBasicCredentials]:
authorization = request.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
- if self.realm:
- unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'}
- else:
- unauthorized_headers = {"WWW-Authenticate": "Basic"}
if not authorization or scheme.lower() != "basic":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers=unauthorized_headers,
- )
+ raise self.make_not_authenticated_error()
else:
return None
- invalid_user_credentials_exc = HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Invalid authentication credentials",
- headers=unauthorized_headers,
- )
try:
data = b64decode(param).decode("ascii")
- except (ValueError, UnicodeDecodeError, binascii.Error):
- raise invalid_user_credentials_exc # noqa: B904
+ except (ValueError, UnicodeDecodeError, binascii.Error) as e:
+ raise self.make_not_authenticated_error() from e
username, separator, password = data.partition(":")
if not separator:
- raise invalid_user_credentials_exc
+ raise self.make_not_authenticated_error()
return HTTPBasicCredentials(username=username, password=password)
@@ -306,17 +310,12 @@ class HTTPBearer(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
if scheme.lower() != "bearer":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN,
- detail="Invalid authentication credentials",
- )
+ raise self.make_not_authenticated_error()
else:
return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
@@ -326,6 +325,12 @@ class HTTPDigest(HTTPBase):
"""
HTTP Digest authentication.
+ **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
+ but it doesn't implement the full Digest scheme, you would need to to subclass it
+ and implement it in your code.
+
+ Ref: https://datatracker.ietf.org/doc/html/rfc7616
+
## Usage
Create an instance object and use that object as the dependency in `Depends()`.
@@ -408,17 +413,12 @@ class HTTPDigest(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials):
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
if scheme.lower() != "digest":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN,
- detail="Invalid authentication credentials",
- )
+ raise self.make_not_authenticated_error()
else:
return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py
index f8d97d762..b41b0f877 100644
--- a/fastapi/security/oauth2.py
+++ b/fastapi/security/oauth2.py
@@ -8,7 +8,7 @@ from fastapi.param_functions import Form
from fastapi.security.base import SecurityBase
from fastapi.security.utils import get_authorization_scheme_param
from starlette.requests import Request
-from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
# TODO: import from typing when deprecating Python 3.9
from typing_extensions import Annotated
@@ -377,13 +377,33 @@ class OAuth2(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
+ def make_not_authenticated_error(self) -> HTTPException:
+ """
+ The OAuth 2 specification doesn't define the challenge that should be used,
+ because a `Bearer` token is not really the only option to authenticate.
+
+ But declaring any other authentication challenge would be application-specific
+ as it's not defined in the specification.
+
+ For practical reasons, this method uses the `Bearer` challenge by default, as
+ it's probably the most common one.
+
+ If you are implementing an OAuth2 authentication scheme other than the provided
+ ones in FastAPI (based on bearer tokens), you might want to override this.
+
+ Ref: https://datatracker.ietf.org/doc/html/rfc6749
+ """
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization")
if not authorization:
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
return authorization
@@ -491,11 +511,7 @@ class OAuth2PasswordBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers={"WWW-Authenticate": "Bearer"},
- )
+ raise self.make_not_authenticated_error()
else:
return None
return param
@@ -601,11 +617,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2):
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "bearer":
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_401_UNAUTHORIZED,
- detail="Not authenticated",
- headers={"WWW-Authenticate": "Bearer"},
- )
+ raise self.make_not_authenticated_error()
else:
return None # pragma: nocover
return param
diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py
index 5e99798e6..e574a56a8 100644
--- a/fastapi/security/open_id_connect_url.py
+++ b/fastapi/security/open_id_connect_url.py
@@ -5,7 +5,7 @@ from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel
from fastapi.security.base import SecurityBase
from starlette.exceptions import HTTPException
from starlette.requests import Request
-from starlette.status import HTTP_403_FORBIDDEN
+from starlette.status import HTTP_401_UNAUTHORIZED
from typing_extensions import Annotated
@@ -13,6 +13,11 @@ class OpenIdConnect(SecurityBase):
"""
OpenID Connect authentication class. An instance of it would be used as a
dependency.
+
+ **Warning**: this is only a stub to connect the components with OpenAPI in FastAPI,
+ but it doesn't implement the full OpenIdConnect scheme, for example, it doesn't use
+ the OpenIDConnect URL. You would need to to subclass it and implement it in your
+ code.
"""
def __init__(
@@ -73,13 +78,18 @@ class OpenIdConnect(SecurityBase):
self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error
+ def make_not_authenticated_error(self) -> HTTPException:
+ return HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Not authenticated",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
async def __call__(self, request: Request) -> Optional[str]:
authorization = request.headers.get("Authorization")
if not authorization:
if self.auto_error:
- raise HTTPException(
- status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
- )
+ raise self.make_not_authenticated_error()
else:
return None
return authorization
diff --git a/requirements-docs-insiders.txt b/requirements-docs-insiders.txt
deleted file mode 100644
index d8d3c37a9..000000000
--- a/requirements-docs-insiders.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-git+https://${TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.30-insiders-4.53.11
-git+https://${TOKEN}@github.com/pawamoy-insiders/griffe-typing-deprecated.git
-git+https://${TOKEN}@github.com/pawamoy-insiders/mkdocstrings-python.git
diff --git a/requirements-docs.txt b/requirements-docs.txt
index 696eb2a33..4f1863a4a 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,6 +1,6 @@
-e .
-r requirements-docs-tests.txt
-mkdocs-material==9.6.16
+mkdocs-material==9.7.0
mdx-include >=1.4.1,<2.0.0
mkdocs-redirects>=1.2.1,<1.3.0
typer == 0.16.0
@@ -13,7 +13,9 @@ pillow==11.3.0
cairosvg==2.8.2
mkdocstrings[python]==0.30.1
griffe-typingdoc==0.3.0
+griffe-warnings-deprecated==1.1.0
# For griffe, it formats with black
black==25.1.0
mkdocs-macros-plugin==1.4.1
-markdown-include-variants==0.0.5
+markdown-include-variants==0.0.7
+python-slugify==8.0.4
diff --git a/requirements.txt b/requirements.txt
index 9180bf1be..5d9f97b75 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,6 @@
-e .[all]
-r requirements-tests.txt
-r requirements-docs.txt
-pre-commit >=2.17.0,<5.0.0
+pre-commit >=4.5.0,<5.0.0
# For generating screenshots
playwright
diff --git a/scripts/docs.py b/scripts/docs.py
index 1a336a036..73f60e68c 100644
--- a/scripts/docs.py
+++ b/scripts/docs.py
@@ -4,9 +4,8 @@ import os
import re
import shutil
import subprocess
-from functools import lru_cache
+from html.parser import HTMLParser
from http.server import HTTPServer, SimpleHTTPRequestHandler
-from importlib import metadata
from multiprocessing import Pool
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
@@ -16,6 +15,7 @@ import typer
import yaml
from jinja2 import Template
from ruff.__main__ import find_ruff_bin
+from slugify import slugify as py_slugify
logging.basicConfig(level=logging.INFO)
@@ -27,8 +27,8 @@ missing_translation_snippet = """
{!../../docs/missing-translation.md!}
"""
-non_translated_sections = [
- "reference/",
+non_translated_sections = (
+ f"reference{os.sep}",
"release-notes.md",
"fastapi-people.md",
"external-links.md",
@@ -36,7 +36,7 @@ non_translated_sections = [
"management-tasks.md",
"management.md",
"contributing.md",
-]
+)
docs_path = Path("docs")
en_docs_path = Path("docs/en")
@@ -44,13 +44,39 @@ en_config_path: Path = en_docs_path / mkdocs_name
site_path = Path("site").absolute()
build_site_path = Path("site_build").absolute()
+header_pattern = re.compile(r"^(#{1,6}) (.+?)(?:\s*\{\s*(#.*)\s*\})?\s*$")
header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})\s*$")
+code_block3_pattern = re.compile(r"^\s*```")
+code_block4_pattern = re.compile(r"^\s*````")
-@lru_cache
-def is_mkdocs_insiders() -> bool:
- version = metadata.version("mkdocs-material")
- return "insiders" in version
+class VisibleTextExtractor(HTMLParser):
+ """Extract visible text from a string with HTML tags."""
+
+ def __init__(self):
+ super().__init__()
+ self.text_parts = []
+
+ def handle_data(self, data):
+ self.text_parts.append(data)
+
+ def extract_visible_text(self, html: str) -> str:
+ self.reset()
+ self.text_parts = []
+ self.feed(html)
+ return "".join(self.text_parts).strip()
+
+
+def slugify(text: str) -> str:
+ return py_slugify(
+ text,
+ replacements=[
+ ("`", ""), # `dict`s -> dicts
+ ("'s", "s"), # it's -> its
+ ("'t", "t"), # don't -> dont
+ ("**", ""), # **FastAPI**s -> FastAPIs
+ ],
+ )
def get_en_config() -> Dict[str, Any]:
@@ -77,9 +103,7 @@ def complete_existing_lang(incomplete: str):
@app.callback()
def callback() -> None:
- if is_mkdocs_insiders():
- os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml"
- # For MacOS with insiders and Cairo
+ # For MacOS with Cairo
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib"
@@ -115,10 +139,6 @@ def build_lang(
"""
Build the docs for a language.
"""
- insiders_env_file = os.environ.get("INSIDERS_FILE")
- print(f"Insiders file {insiders_env_file}")
- if is_mkdocs_insiders():
- print("Using insiders")
lang_path: Path = Path("docs") / lang
if not lang_path.is_dir():
typer.echo(f"The language translation doesn't seem to exist yet: {lang}")
@@ -440,5 +460,83 @@ def generate_docs_src_versions_for_file(file_path: Path) -> None:
version_file.write_text(content_format, encoding="utf-8")
+@app.command()
+def add_permalinks_page(path: Path, update_existing: bool = False):
+ """
+ Add or update header permalinks in specific page of En docs.
+ """
+
+ if not path.is_relative_to(en_docs_path / "docs"):
+ raise RuntimeError(f"Path must be inside {en_docs_path}")
+ rel_path = path.relative_to(en_docs_path / "docs")
+
+ # Skip excluded sections
+ if str(rel_path).startswith(non_translated_sections):
+ return
+
+ visible_text_extractor = VisibleTextExtractor()
+ updated_lines = []
+ in_code_block3 = False
+ in_code_block4 = False
+ permalinks = set()
+
+ with path.open("r", encoding="utf-8") as f:
+ lines = f.readlines()
+
+ for line in lines:
+ # Handle codeblocks start and end
+ if not (in_code_block3 or in_code_block4):
+ if code_block4_pattern.match(line):
+ in_code_block4 = True
+ elif code_block3_pattern.match(line):
+ in_code_block3 = True
+ else:
+ if in_code_block4 and code_block4_pattern.match(line):
+ in_code_block4 = False
+ elif in_code_block3 and code_block3_pattern.match(line):
+ in_code_block3 = False
+
+ # Process Headers only outside codeblocks
+ if not (in_code_block3 or in_code_block4):
+ match = header_pattern.match(line)
+ if match:
+ hashes, title, _permalink = match.groups()
+ if (not _permalink) or update_existing:
+ slug = slugify(visible_text_extractor.extract_visible_text(title))
+ if slug in permalinks:
+ # If the slug is already used, append a number to make it unique
+ count = 1
+ original_slug = slug
+ while slug in permalinks:
+ slug = f"{original_slug}_{count}"
+ count += 1
+ permalinks.add(slug)
+
+ line = f"{hashes} {title} {{ #{slug} }}\n"
+
+ updated_lines.append(line)
+
+ with path.open("w", encoding="utf-8") as f:
+ f.writelines(updated_lines)
+
+
+@app.command()
+def add_permalinks_pages(pages: List[Path], update_existing: bool = False) -> None:
+ """
+ Add or update header permalinks in specific pages of En docs.
+ """
+ for md_file in pages:
+ add_permalinks_page(md_file, update_existing=update_existing)
+
+
+@app.command()
+def add_permalinks(update_existing: bool = False) -> None:
+ """
+ Add or update header permalinks in all pages of En docs.
+ """
+ for md_file in en_docs_path.rglob("*.md"):
+ add_permalinks_page(md_file, update_existing=update_existing)
+
+
if __name__ == "__main__":
app()
diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py
index 4ddb8e2ee..9bacfc56e 100644
--- a/tests/test_security_api_key_cookie.py
+++ b/tests/test_security_api_key_cookie.py
@@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
client = TestClient(app)
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py
index d99d616e0..d0cab324e 100644
--- a/tests/test_security_api_key_cookie_description.py
+++ b/tests/test_security_api_key_cookie_description.py
@@ -32,8 +32,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
client = TestClient(app)
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py
index 1ff883703..3e761b150 100644
--- a/tests/test_security_api_key_header.py
+++ b/tests/test_security_api_key_header.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py
index 27f9d0f29..38a1a8881 100644
--- a/tests/test_security_api_key_header_description.py
+++ b/tests/test_security_api_key_header_description.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py
index dc7a0a621..11ed19468 100644
--- a/tests/test_security_api_key_query.py
+++ b/tests/test_security_api_key_query.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py
index 35dc7743a..658798326 100644
--- a/tests/test_security_api_key_query_description.py
+++ b/tests/test_security_api_key_query_description.py
@@ -33,8 +33,9 @@ def test_security_api_key():
def test_security_api_key_no_key():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "APIKey"
def test_openapi_schema():
diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py
index 51928bafd..8cf259a75 100644
--- a/tests/test_security_http_base.py
+++ b/tests/test_security_http_base.py
@@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema():
diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py
index bc79f3242..791ea59f4 100644
--- a/tests/test_security_http_base_description.py
+++ b/tests/test_security_http_base_description.py
@@ -23,8 +23,9 @@ def test_security_http_base():
def test_security_http_base_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Other"
def test_openapi_schema():
diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py
index 9b6cb6c45..7071f381a 100644
--- a/tests/test_security_http_basic_optional.py
+++ b/tests/test_security_http_basic_optional.py
@@ -38,7 +38,7 @@ def test_security_http_basic_invalid_credentials():
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials():
@@ -47,7 +47,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema():
diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py
index 9fc33971a..ec7371f90 100644
--- a/tests/test_security_http_basic_realm.py
+++ b/tests/test_security_http_basic_realm.py
@@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials():
@@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema():
diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py
index 02122442e..a93d5fc86 100644
--- a/tests/test_security_http_basic_realm_description.py
+++ b/tests/test_security_http_basic_realm_description.py
@@ -36,7 +36,7 @@ def test_security_http_basic_invalid_credentials():
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials():
@@ -45,7 +45,7 @@ def test_security_http_basic_non_basic_credentials():
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"'
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema():
diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py
index 5b9e2d691..961b42f4d 100644
--- a/tests/test_security_http_bearer.py
+++ b/tests/test_security_http_bearer.py
@@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py
index 2f11c3a14..e16994abc 100644
--- a/tests/test_security_http_bearer_description.py
+++ b/tests/test_security_http_bearer_description.py
@@ -23,14 +23,16 @@ def test_security_http_bearer():
def test_security_http_bearer_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_security_http_bearer_incorrect_scheme_credentials():
response = client.get("/users/me", headers={"Authorization": "Basic notreally"})
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py
index 133d35763..3fad4c7a5 100644
--- a/tests/test_security_http_digest.py
+++ b/tests/test_security_http_digest.py
@@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"}
)
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema():
diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py
index 4e31a0c00..319416a07 100644
--- a/tests/test_security_http_digest_description.py
+++ b/tests/test_security_http_digest_description.py
@@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials():
response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"}
)
- assert response.status_code == 403, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.status_code == 401, response.text
+ assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema():
diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py
index 2b7e3457a..804e4152d 100644
--- a/tests/test_security_oauth2.py
+++ b/tests/test_security_oauth2.py
@@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_strict_login_no_data():
diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py
index 1e322e640..c9a0a8db7 100644
--- a/tests/test_security_openid_connect.py
+++ b/tests/test_security_openid_connect.py
@@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py
index 44cf57f86..d008cbc63 100644
--- a/tests/test_security_openid_connect_description.py
+++ b/tests/test_security_openid_connect_description.py
@@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header():
def test_security_oauth2_password_bearer_no_header():
response = client.get("/users/me")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
+ assert response.headers["WWW-Authenticate"] == "Bearer"
def test_openapi_schema():
diff --git a/tests/test_security_scopes.py b/tests/test_security_scopes.py
new file mode 100644
index 000000000..248fd2bcc
--- /dev/null
+++ b/tests/test_security_scopes.py
@@ -0,0 +1,46 @@
+from typing import Dict
+
+import pytest
+from fastapi import Depends, FastAPI, Security
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+
+@pytest.fixture(name="call_counter")
+def call_counter_fixture():
+ return {"count": 0}
+
+
+@pytest.fixture(name="app")
+def app_fixture(call_counter: Dict[str, int]):
+ def get_db():
+ call_counter["count"] += 1
+ return f"db_{call_counter['count']}"
+
+ def get_user(db: Annotated[str, Depends(get_db)]):
+ return "user"
+
+ app = FastAPI()
+
+ @app.get("/")
+ def endpoint(
+ db: Annotated[str, Depends(get_db)],
+ user: Annotated[str, Security(get_user, scopes=["read"])],
+ ):
+ return {"db": db}
+
+ return app
+
+
+@pytest.fixture(name="client")
+def client_fixture(app: FastAPI):
+ return TestClient(app)
+
+
+def test_security_scopes_dependency_called_once(
+ client: TestClient, call_counter: Dict[str, int]
+):
+ response = client.get("/")
+
+ assert response.status_code == 200
+ assert call_counter["count"] == 1
diff --git a/tests/test_security_scopes_dont_propagate.py b/tests/test_security_scopes_dont_propagate.py
new file mode 100644
index 000000000..2bbcc749d
--- /dev/null
+++ b/tests/test_security_scopes_dont_propagate.py
@@ -0,0 +1,45 @@
+# Ref: https://github.com/tiangolo/fastapi/issues/5623
+
+from typing import Any, Dict, List
+
+from fastapi import FastAPI, Security
+from fastapi.security import SecurityScopes
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+
+async def security1(scopes: SecurityScopes):
+ return scopes.scopes
+
+
+async def security2(scopes: SecurityScopes):
+ return scopes.scopes
+
+
+async def dep3(
+ dep1: Annotated[List[str], Security(security1, scopes=["scope1"])],
+ dep2: Annotated[List[str], Security(security2, scopes=["scope2"])],
+):
+ return {"dep1": dep1, "dep2": dep2}
+
+
+app = FastAPI()
+
+
+@app.get("/scopes")
+def get_scopes(
+ dep3: Annotated[Dict[str, Any], Security(dep3, scopes=["scope3"])],
+):
+ return dep3
+
+
+client = TestClient(app)
+
+
+def test_security_scopes_dont_propagate():
+ response = client.get("/scopes")
+ assert response.status_code == 200
+ assert response.json() == {
+ "dep1": ["scope3", "scope1"],
+ "dep2": ["scope3", "scope2"],
+ }
diff --git a/tests/test_security_scopes_sub_dependency.py b/tests/test_security_scopes_sub_dependency.py
new file mode 100644
index 000000000..9cc668d8e
--- /dev/null
+++ b/tests/test_security_scopes_sub_dependency.py
@@ -0,0 +1,107 @@
+# Ref: https://github.com/fastapi/fastapi/discussions/6024#discussioncomment-8541913
+
+from typing import Dict
+
+import pytest
+from fastapi import Depends, FastAPI, Security
+from fastapi.security import SecurityScopes
+from fastapi.testclient import TestClient
+from typing_extensions import Annotated
+
+
+@pytest.fixture(name="call_counts")
+def call_counts_fixture():
+ return {
+ "get_db_session": 0,
+ "get_current_user": 0,
+ "get_user_me": 0,
+ "get_user_items": 0,
+ }
+
+
+@pytest.fixture(name="app")
+def app_fixture(call_counts: Dict[str, int]):
+ def get_db_session():
+ call_counts["get_db_session"] += 1
+ return f"db_session_{call_counts['get_db_session']}"
+
+ def get_current_user(
+ security_scopes: SecurityScopes,
+ db_session: Annotated[str, Depends(get_db_session)],
+ ):
+ call_counts["get_current_user"] += 1
+ return {
+ "user": f"user_{call_counts['get_current_user']}",
+ "scopes": security_scopes.scopes,
+ "db_session": db_session,
+ }
+
+ def get_user_me(
+ current_user: Annotated[dict, Security(get_current_user, scopes=["me"])],
+ ):
+ call_counts["get_user_me"] += 1
+ return {
+ "user_me": f"user_me_{call_counts['get_user_me']}",
+ "current_user": current_user,
+ }
+
+ def get_user_items(
+ user_me: Annotated[dict, Depends(get_user_me)],
+ ):
+ call_counts["get_user_items"] += 1
+ return {
+ "user_items": f"user_items_{call_counts['get_user_items']}",
+ "user_me": user_me,
+ }
+
+ app = FastAPI()
+
+ @app.get("/")
+ def path_operation(
+ user_me: Annotated[dict, Depends(get_user_me)],
+ user_items: Annotated[dict, Security(get_user_items, scopes=["items"])],
+ ):
+ return {
+ "user_me": user_me,
+ "user_items": user_items,
+ }
+
+ return app
+
+
+@pytest.fixture(name="client")
+def client_fixture(app: FastAPI):
+ return TestClient(app)
+
+
+def test_security_scopes_sub_dependency_caching(
+ client: TestClient, call_counts: Dict[str, int]
+):
+ response = client.get("/")
+
+ assert response.status_code == 200
+ assert call_counts["get_db_session"] == 1
+ assert call_counts["get_current_user"] == 2
+ assert call_counts["get_user_me"] == 2
+ assert call_counts["get_user_items"] == 1
+ assert response.json() == {
+ "user_me": {
+ "user_me": "user_me_1",
+ "current_user": {
+ "user": "user_1",
+ "scopes": ["me"],
+ "db_session": "db_session_1",
+ },
+ },
+ "user_items": {
+ "user_items": "user_items_1",
+ "user_me": {
+ "user_me": "user_me_2",
+ "current_user": {
+ "user": "user_2",
+ "scopes": ["items", "me"],
+ "db_session": "db_session_1",
+ },
+ },
+ },
+ }
diff --git a/tests/test_top_level_security_scheme_in_openapi.py b/tests/test_top_level_security_scheme_in_openapi.py
index e2de31af5..a36c66d1a 100644
--- a/tests/test_top_level_security_scheme_in_openapi.py
+++ b/tests/test_top_level_security_scheme_in_openapi.py
@@ -27,7 +27,7 @@ def test_get_root():
def test_get_root_no_token():
response = client.get("/")
- assert response.status_code == 403, response.text
+ assert response.status_code == 401, response.text
assert response.json() == {"detail": "Not authenticated"}
diff --git a/docs/en/mkdocs.no-insiders.yml b/tests/test_tutorial/test_authentication_error_status_code/__init__.py
similarity index 100%
rename from docs/en/mkdocs.no-insiders.yml
rename to tests/test_tutorial/test_authentication_error_status_code/__init__.py
diff --git a/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py
new file mode 100644
index 000000000..bbd7bff30
--- /dev/null
+++ b/tests/test_tutorial/test_authentication_error_status_code/test_tutorial001.py
@@ -0,0 +1,69 @@
+import importlib
+
+import pytest
+from fastapi.testclient import TestClient
+from inline_snapshot import snapshot
+
+from ...utils import needs_py39
+
+
+@pytest.fixture(
+ name="client",
+ params=[
+ "tutorial001_an",
+ pytest.param("tutorial001_an_py39", marks=needs_py39),
+ ],
+)
+def get_client(request: pytest.FixtureRequest):
+ mod = importlib.import_module(
+ f"docs_src.authentication_error_status_code.{request.param}"
+ )
+
+ client = TestClient(mod.app)
+ return client
+
+
+def test_get_me(client: TestClient):
+ response = client.get("/me", headers={"Authorization": "Bearer secrettoken"})
+ assert response.status_code == 200
+ assert response.json() == {
+ "message": "You are authenticated",
+ "token": "secrettoken",
+ }
+
+
+def test_get_me_no_credentials(client: TestClient):
+ response = client.get("/me")
+ assert response.status_code == 403
+ assert response.json() == {"detail": "Not authenticated"}
+
+
+def test_openapi_schema(client: TestClient):
+ response = client.get("/openapi.json")
+ assert response.status_code == 200, response.text
+ assert response.json() == snapshot(
+ {
+ "openapi": "3.1.0",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/me": {
+ "get": {
+ "summary": "Read Me",
+ "operationId": "read_me_me_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ "security": [{"HTTPBearer403": []}],
+ }
+ }
+ },
+ "components": {
+ "securitySchemes": {
+ "HTTPBearer403": {"type": "http", "scheme": "bearer"}
+ }
+ },
+ }
+ )
diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py
index 2bbb2e851..6b8735113 100644
--- a/tests/test_tutorial/test_security/test_tutorial003.py
+++ b/tests/test_tutorial/test_security/test_tutorial003.py
@@ -66,7 +66,7 @@ def test_token(client: TestClient):
def test_incorrect_token(client: TestClient):
response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"})
assert response.status_code == 401, response.text
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Bearer"
diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py
index 40b413806..9587159dc 100644
--- a/tests/test_tutorial/test_security/test_tutorial006.py
+++ b/tests/test_tutorial/test_security/test_tutorial006.py
@@ -41,7 +41,7 @@ def test_security_http_basic_invalid_credentials(client: TestClient):
)
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_security_http_basic_non_basic_credentials(client: TestClient):
@@ -50,7 +50,7 @@ def test_security_http_basic_non_basic_credentials(client: TestClient):
response = client.get("/users/me", headers={"Authorization": auth_header})
assert response.status_code == 401, response.text
assert response.headers["WWW-Authenticate"] == "Basic"
- assert response.json() == {"detail": "Invalid authentication credentials"}
+ assert response.json() == {"detail": "Not authenticated"}
def test_openapi_schema(client: TestClient):