Compare commits
260 Commits
feat/error
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479a2c0c89 | ||
|
|
89347fd166 | ||
|
|
767315b38a | ||
|
|
f25ee43e68 | ||
|
|
8782666189 | ||
|
|
ad5ff87c86 | ||
|
|
6761b2c8f9 | ||
|
|
438f64834d | ||
|
|
10ddc6dd29 | ||
|
|
b499bc4510 | ||
|
|
b224045f59 | ||
|
|
7375b5bf66 | ||
|
|
d438fb16fe | ||
|
|
3e6b964466 | ||
|
|
2c423bd82b | ||
|
|
7f027f8e25 | ||
|
|
73a80c3cc8 | ||
|
|
45c0b568d3 | ||
|
|
850d92656d | ||
|
|
fdcacb4b83 | ||
|
|
70f247f9ee | ||
|
|
18edfa7012 | ||
|
|
77843e06dc | ||
|
|
3703339cdc | ||
|
|
fda70f37b0 | ||
|
|
f05fc928c0 | ||
|
|
6cdd61d15e | ||
|
|
12b823eb8a | ||
|
|
1f2abe357a | ||
|
|
edb54c43c0 | ||
|
|
029be08867 | ||
|
|
8d397c7319 | ||
|
|
587042d68f | ||
|
|
c9a75fb67b | ||
|
|
84fd578224 | ||
|
|
cd52d34b55 | ||
|
|
5211880320 | ||
|
|
1cb8e747e2 | ||
|
|
28efbb24bd | ||
|
|
042ffeb7d6 | ||
|
|
c61f9d4ebd | ||
|
|
02bed6f8c3 | ||
|
|
d8f2501316 | ||
|
|
9dbb7836bb | ||
|
|
b3c69da8c1 | ||
|
|
3f3ebee20f | ||
|
|
d072de754f | ||
|
|
e300c2c75d | ||
|
|
1fa697651b | ||
|
|
59ec1de7a4 | ||
|
|
2fc0efcdd9 | ||
|
|
c825f4eb6e | ||
|
|
38731c31d1 | ||
|
|
9283c0f15c | ||
|
|
a01a33eb8f | ||
|
|
2ce65bde15 | ||
|
|
654f2ed7d7 | ||
|
|
a03d9f6f0e | ||
|
|
e377de40d0 | ||
|
|
0779f7f8a4 | ||
|
|
7e9ce2c974 | ||
|
|
99f0d8734d | ||
|
|
7ae2e6375a | ||
|
|
4532a39a67 | ||
|
|
4aff1b95f4 | ||
|
|
a148fc5a71 | ||
|
|
422ce367ae | ||
|
|
918dae6ef9 | ||
|
|
ae56d07af7 | ||
|
|
9ff60042a5 | ||
|
|
19df042c54 | ||
|
|
865ce7c0b4 | ||
|
|
4f40b84957 | ||
|
|
5692dfc416 | ||
|
|
4194764a26 | ||
|
|
d94bf28743 | ||
|
|
8ae0bcbecb | ||
|
|
4744ff9a1a | ||
|
|
0391372376 | ||
|
|
69a6ae3198 | ||
|
|
3850ad6520 | ||
|
|
9b3f17a549 | ||
|
|
ce79f95d06 | ||
|
|
dbf8797b47 | ||
|
|
58f28be98e | ||
|
|
93d9510749 | ||
|
|
9b1c6c45ed | ||
|
|
57a61d86f2 | ||
|
|
7ef5f9f5e7 | ||
|
|
6d26d88970 | ||
|
|
4098bcac97 | ||
|
|
8c057fa3fc | ||
|
|
bbe119e4e8 | ||
|
|
2744f14c99 | ||
|
|
1dfb0bd885 | ||
|
|
3d0d46a704 | ||
|
|
ad9e5b90dd | ||
|
|
0e3b18c399 | ||
|
|
f84661d56e | ||
|
|
dcd7e6c94e | ||
|
|
e5e7344a25 | ||
|
|
b40c4288c7 | ||
|
|
dca6b1c529 | ||
|
|
fe50e5d993 | ||
|
|
d41737158f | ||
|
|
bf337d62d5 | ||
|
|
ea9e01e3b0 | ||
|
|
a96eadde97 | ||
|
|
f8c69b432f | ||
|
|
1df5533de8 | ||
|
|
896661c189 | ||
|
|
016db5f7da | ||
|
|
e6c7ce791f | ||
|
|
c1144fd4f1 | ||
|
|
b06d8ecd26 | ||
|
|
f9403a2041 | ||
|
|
374bb6764e | ||
|
|
15cf08a657 | ||
|
|
293eca7657 | ||
|
|
52b3ec62a3 | ||
|
|
1aa02117ba | ||
|
|
050f60265e | ||
|
|
daecb45327 | ||
|
|
22dfd3fa95 | ||
|
|
b9606269a7 | ||
|
|
5432729137 | ||
|
|
5e33d430f1 | ||
|
|
ce129ff120 | ||
|
|
fa3d9d27db | ||
|
|
66b9b58ad9 | ||
|
|
bc795051bd | ||
|
|
2eda7b10be | ||
|
|
c9ba771b39 | ||
|
|
56a9f68c16 | ||
|
|
bdf95fd3e7 | ||
|
|
86dc4560ce | ||
|
|
695820ae9b | ||
|
|
56c45db06c | ||
|
|
f2192cc38c | ||
|
|
7fb029daf8 | ||
|
|
4fdfec4adf | ||
|
|
e5f3663925 | ||
|
|
2445e7921b | ||
|
|
7abc19969b | ||
|
|
4e6786e4de | ||
|
|
8136fb6832 | ||
|
|
aaf201669c | ||
|
|
54d9575e75 | ||
|
|
3014765e73 | ||
|
|
36957371b1 | ||
|
|
1f867aaeb0 | ||
|
|
ae8253f10b | ||
|
|
90f43699c7 | ||
|
|
80c076404e | ||
|
|
be25d3bb3a | ||
|
|
7983c1ae9c | ||
|
|
75d4402f32 | ||
|
|
4156ccb4c9 | ||
|
|
3575cbaa4e | ||
|
|
a500513085 | ||
|
|
038f8ef3fe | ||
|
|
3aa1d010d6 | ||
|
|
a3cc36016e | ||
|
|
6725ebb1ee | ||
|
|
bfa754e21e | ||
|
|
b7241e1c64 | ||
|
|
6215230fd2 | ||
|
|
9a0587922f | ||
|
|
bc412d2a13 | ||
|
|
5279296e62 | ||
|
|
8c3402dd22 | ||
|
|
04c6320f39 | ||
|
|
fc6c51b8bb | ||
|
|
2aea8354ea | ||
|
|
fe3910083e | ||
|
|
967a2dce60 | ||
|
|
079f07a064 | ||
|
|
137f88ea32 | ||
|
|
27d976a8f2 | ||
|
|
1ddd250dcf | ||
|
|
108b1c9d1a | ||
|
|
a1783d864a | ||
|
|
6ffaaf7c2f | ||
|
|
c7668ced8e | ||
|
|
a50753268a | ||
|
|
84dd2c403d | ||
|
|
3d26ab4659 | ||
|
|
47304d9ae7 | ||
|
|
1cb0c32891 | ||
|
|
7dc027d5fb | ||
|
|
587a1ccca5 | ||
|
|
cee31a6891 | ||
|
|
eba64efbc8 | ||
|
|
0f513d26f2 | ||
|
|
ff54b029b1 | ||
|
|
2f25107af2 | ||
|
|
8efa41c7da | ||
|
|
b492349d26 | ||
|
|
ce999aa634 | ||
|
|
d277c25879 | ||
|
|
5bf788f0eb | ||
|
|
8f4c8a7f34 | ||
|
|
9baded3dcf | ||
|
|
9279825417 | ||
|
|
875f6c6a4d | ||
|
|
ca24e1b52c | ||
|
|
c23cd24e66 | ||
|
|
d79f285184 | ||
|
|
4e9f48d249 | ||
|
|
44a3071654 | ||
|
|
6d666d99a2 | ||
|
|
9a6b3a8249 | ||
|
|
53fa273822 | ||
|
|
22873a9918 | ||
|
|
14ffba8316 | ||
|
|
5c78192e8f | ||
|
|
b9c03a80ab | ||
|
|
14bdf047f6 | ||
|
|
0efd3835da | ||
|
|
772c24bd19 | ||
|
|
12c9ee3898 | ||
|
|
35fedeeb61 | ||
|
|
95fa1ac618 | ||
|
|
a2219eb2ed | ||
|
|
fe85206c5c | ||
|
|
9e32e8ebad | ||
|
|
f73b8beeb1 | ||
|
|
76a3812ad5 | ||
|
|
a05ae6426c | ||
|
|
4a503d84fa | ||
|
|
0d4747e602 | ||
|
|
93897b5118 | ||
|
|
ed2d655902 | ||
|
|
1e5f1be767 | ||
|
|
bd552df8f9 | ||
|
|
2ff704b91c | ||
|
|
4f74ed1447 | ||
|
|
f39933c850 | ||
|
|
bda1c8a4c4 | ||
|
|
bcde07dd22 | ||
|
|
892c7888ec | ||
|
|
b0092cb09e | ||
|
|
04c1974212 | ||
|
|
afed732f85 | ||
|
|
baf4ea462e | ||
|
|
99fafb3c3b | ||
|
|
4af46c931c | ||
|
|
ebcd996c69 | ||
|
|
3dd0d44445 | ||
|
|
fc1de0109a | ||
|
|
274d9afb8f | ||
|
|
1cce88d8e8 | ||
|
|
5515893076 | ||
|
|
56aa152a2d | ||
|
|
32fa92fbdf | ||
|
|
e811b4e98c | ||
|
|
b52f3f9784 | ||
|
|
656818496e | ||
|
|
7d274ed389 | ||
|
|
c55af77fe9 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +0,0 @@
|
||||
github: encode
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,11 +1,9 @@
|
||||
# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/encode/uvicorn/discussions
|
||||
about: >
|
||||
The "Discussions" forum is where you want to start. 💖
|
||||
- name: Chat
|
||||
url: https://gitter.im/encode/community
|
||||
about: >
|
||||
Our community chat forum.
|
||||
- name: Discussions
|
||||
url: https://github.com/Kludex/uvicorn/discussions
|
||||
about: The "Discussions" forum is where you want to start. 💖
|
||||
- name: Chat
|
||||
url: https://discord.com/invite/SWU73HffbV
|
||||
about: Our community Discord server. 💬
|
||||
|
||||
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
@ -1,10 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
python-packages:
|
||||
patterns:
|
||||
- "*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
36
.github/workflows/benchmark.yml
vendored
Normal file
36
.github/workflows/benchmark.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
name: CodSpeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
benchmarks:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
enable-cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: scripts/install
|
||||
shell: bash
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: uv run pytest tests/benchmarks/ --codspeed -n 0
|
||||
115
.github/workflows/main.yml
vendored
Normal file
115
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
name: Test Suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: "Python ${{ matrix.python-version }} ${{ matrix.os }}"
|
||||
runs-on: "${{ matrix.os }}"
|
||||
timeout-minutes: 10
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
|
||||
steps:
|
||||
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: ${{ matrix.os != 'windows-latest' }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: scripts/install
|
||||
shell: bash
|
||||
|
||||
- name: Run linting checks
|
||||
run: scripts/check
|
||||
if: "${{ matrix.os == 'ubuntu-latest'}}"
|
||||
|
||||
- name: "Build package & docs"
|
||||
run: scripts/build
|
||||
shell: bash
|
||||
|
||||
- name: "Run tests"
|
||||
run: scripts/test
|
||||
shell: bash
|
||||
|
||||
- name: "Enforce coverage"
|
||||
run: scripts/coverage
|
||||
shell: bash
|
||||
|
||||
docs-preview:
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
environment:
|
||||
name: cloudflare
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: scripts/install
|
||||
|
||||
- name: Build docs
|
||||
run: uv run mkdocs build
|
||||
|
||||
- name: Deploy preview to Cloudflare Pages
|
||||
id: deploy
|
||||
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
command: >
|
||||
pages deploy ./site
|
||||
--project-name uvicorn
|
||||
--commit-hash ${{ github.event.pull_request.head.sha }}
|
||||
--branch ${{ github.head_ref }}
|
||||
|
||||
- name: Comment preview URL on PR
|
||||
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3.0.1
|
||||
with:
|
||||
message: |
|
||||
:book: Docs preview: ${{ steps.deploy.outputs.deployment-url }}
|
||||
comment-tag: docs-preview
|
||||
|
||||
# https://github.com/marketplace/actions/alls-green#why
|
||||
check:
|
||||
if: always()
|
||||
needs: [tests]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Decide whether the needed jobs succeeded or failed
|
||||
uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
134
.github/workflows/publish.yml
vendored
134
.github/workflows/publish.yml
vendored
@ -4,26 +4,126 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: "Publish release"
|
||||
runs-on: "ubuntu-latest"
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: deploy
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/setup-python@v4"
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: "Install dependencies"
|
||||
run: "scripts/install"
|
||||
- name: "Build package & docs"
|
||||
run: "scripts/build"
|
||||
- name: "Publish to PyPI & deploy docs"
|
||||
run: "scripts/publish"
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: scripts/install
|
||||
|
||||
- name: Build package & docs
|
||||
run: scripts/build
|
||||
|
||||
- name: Upload package distributions
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: package-distributions
|
||||
path: dist/
|
||||
|
||||
- name: Upload documentation
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: documentation
|
||||
path: site/
|
||||
|
||||
pypi-publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: success() && startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/project/uvicorn
|
||||
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: package-distributions
|
||||
path: dist/
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
|
||||
docs-publish:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
# `mkdocs gh-deploy` pushes the built docs to `gh-pages`, so this job needs
|
||||
# a real checkout with the authenticated origin remote preserved.
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 # zizmor: ignore[artipacked]
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: documentation
|
||||
path: site/
|
||||
|
||||
- name: Configure Git Credentials
|
||||
run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: scripts/install
|
||||
|
||||
- name: Publish documentation 📚 to GitHub Pages
|
||||
run: uv run mkdocs gh-deploy --force
|
||||
|
||||
docs-cloudflare:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
environment:
|
||||
name: cloudflare
|
||||
url: https://uvicorn.dev
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: documentation
|
||||
path: site/
|
||||
|
||||
- uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
command: >
|
||||
pages deploy ./site
|
||||
--project-name uvicorn
|
||||
--commit-hash ${{ github.sha }}
|
||||
--branch main
|
||||
|
||||
38
.github/workflows/test-suite.yml
vendored
38
.github/workflows/test-suite.yml
vendored
@ -1,38 +0,0 @@
|
||||
---
|
||||
name: Test Suite
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
branches: ["master"]
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: "Python ${{ matrix.python-version }} ${{ matrix.os }}"
|
||||
runs-on: "${{ matrix.os }}"
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
steps:
|
||||
- uses: "actions/checkout@v4"
|
||||
- uses: "actions/setup-python@v4"
|
||||
with:
|
||||
python-version: "${{ matrix.python-version }}"
|
||||
- name: "Install dependencies"
|
||||
run: "scripts/install"
|
||||
shell: bash
|
||||
- name: "Run linting checks"
|
||||
run: "scripts/check"
|
||||
shell: bash
|
||||
if: "${{ matrix.os == 'ubuntu-latest'}}"
|
||||
- name: "Build package & docs"
|
||||
run: "scripts/build"
|
||||
shell: bash
|
||||
- name: "Run tests"
|
||||
run: "scripts/test"
|
||||
shell: bash
|
||||
- name: "Enforce coverage"
|
||||
run: "scripts/coverage"
|
||||
shell: bash
|
||||
25
.github/workflows/zizmor.yml
vendored
Normal file
25
.github/workflows/zizmor.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: GitHub Actions Security Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
.cache
|
||||
.coverage
|
||||
.coverage.*
|
||||
.mypy_cache/
|
||||
__pycache__/
|
||||
uvicorn.egg-info/
|
||||
@ -7,3 +8,4 @@ venv/
|
||||
htmlcov/
|
||||
site/
|
||||
dist/
|
||||
.codspeed/
|
||||
|
||||
491
CHANGELOG.md
491
CHANGELOG.md
@ -1,491 +0,0 @@
|
||||
# Change Log
|
||||
|
||||
## 0.24.0.post1 - 2023-11-06
|
||||
|
||||
### Fixed
|
||||
|
||||
* Revert mkdocs-material from 9.1.21 to 9.2.6 (#2148) 05/11/23
|
||||
|
||||
## 0.24.0 - 2023-11-04
|
||||
|
||||
### Added
|
||||
|
||||
- Support Python 3.12 (#2145) 04/11/23
|
||||
- Allow setting `app` via environment variable `UVICORN_APP` (#2106) 21/09/23
|
||||
|
||||
## 0.23.2 - 2023-07-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Maintain the same behavior of `websockets` from 10.4 on 11.0 (#2061) 30/07/23
|
||||
|
||||
## 0.23.1 - 2023-07-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add `typing_extensions` for Python 3.10 and lower (#2053) 18/07/23
|
||||
|
||||
## 0.23.0 - 2023-07-10
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--ws-max-queue` parameter WebSockets (#2033) 10/07/23
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for Python 3.7 (#1996) 19/06/23
|
||||
- Remove `asgiref` as typing dependency (#1999) 08/06/23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Set `scope["scheme"]` to `ws` or `wss` instead of `http` or `https` on `ProxyHeadersMiddleware` for WebSockets (#2043) 12/07/23
|
||||
|
||||
### Changed
|
||||
|
||||
- Raise `ImportError` on circular import (#2040) 09/07/23
|
||||
- Use `logger.getEffectiveLevel()` instead of `logger.level` to check if log level is `TRACE` (#1966) 01/06/23
|
||||
|
||||
## 0.22.0 - 2023-04-28
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--timeout-graceful-shutdown` parameter (#1950) 26/04/23
|
||||
- Handle `SIGBREAK` on Windows (#1909) 15/04/23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Shutdown event is now being triggered on Windows when using hot reload (#1584) 13/04/23
|
||||
- `--reload-delay` is effectively used on the `watchfiles` reloader (#1930) 22/04/23
|
||||
|
||||
## 0.21.1 - 2023-03-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reset lifespan state on each request (#1903) 16/03/23
|
||||
|
||||
## 0.21.0 - 2023-03-09
|
||||
|
||||
### Added
|
||||
|
||||
- Introduce lifespan state (#1818) 05/03/23
|
||||
- Allow headers to be sent as iterables on H11 implementation (#1782) 27/11/22
|
||||
- Improve discoverability when --port=0 is used (#1890) 09/03/23
|
||||
|
||||
### Changed
|
||||
|
||||
- Avoid importing `h11` and `pyyaml` when not needed to improve import time (#1846) 07/02/23
|
||||
- Replace current native `WSGIMiddleware` implementation by `a2wsgi` (#1825) 16/01/23
|
||||
- Change default `--app-dir` from "." (dot) to "" (empty string) (#1835) 06/01/23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Send code 1012 on shutdown for WebSockets (#1816) 06/01/23
|
||||
- Use `surrogateescape` to encode headers on `websockets` implementation (#1005) 12/12/22
|
||||
- Fix warning message on reload failure (#1784) 29/11/22
|
||||
|
||||
## 0.20.0 - 2022-11-20
|
||||
|
||||
### Added
|
||||
|
||||
- Check if handshake is completed before sending frame on `wsproto` shutdown (#1737)
|
||||
- Add default headers to WebSockets implementations (#1606 & #1747) 28/10/22
|
||||
- Warn user when `reload` and `workers` flag are used together (#1731) 31/10/22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use correct `WebSocket` error codes on `close` (#1753) 20/11/22
|
||||
- Send disconnect event on connection lost for `wsproto` (#996) 29/10/22
|
||||
- Add `SIGQUIT` handler to `UvicornWorker` (#1710) 01/11/22
|
||||
- Fix crash on exist with "--uds" if socket doesn't exist (#1725) 27/10/22
|
||||
- Annotate `CONFIG_KWARGS` in `UvicornWorker` class (#1746) 31/10/22
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove conditional on `RemoteProtocolError.event_hint` on `wsproto` (#1486) 31/10/22
|
||||
- Remove unused `handle_no_connect` on `wsproto` implementation (#1759) 17/11/22
|
||||
|
||||
## 0.19.0 - 2022-10-19
|
||||
|
||||
### Added
|
||||
|
||||
- Support Python 3.11 (#1652) 16/09/22
|
||||
- Bump minimal `httptools` version to `0.5.0` (#1645) 13/09/22
|
||||
- Ignore HTTP/2 upgrade and optionally ignore WebSocket upgrade (#1661) 19/10/22
|
||||
- Add `py.typed` to comply with PEP 561 (#1687) 07/10/22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Set `propagate` to `False` on "uvicorn" logger (#1288) 08/10/22
|
||||
- USR1 signal is now handled correctly on `UvicornWorker`. (#1565) 26/08/22
|
||||
- Use path with query string on `WebSockets` logs (#1385) 11/09/22
|
||||
- Fix behavior on which "Date" headers were not updated on the same connection (#1706) 19/10/22
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the `--debug` flag (#1640) 14/09/22
|
||||
- Remove the `DebugMiddleware` (#1697) 07/10/22
|
||||
|
||||
## 0.18.3 - 2022-08-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove cyclic references on HTTP implementations. (#1604) 24/08/22
|
||||
|
||||
### Changed
|
||||
|
||||
- `reload_delay` default changed from `None` to `0.25` on `uvicorn.run()` and `Config`. `None` is not an acceptable value anymore. (#1545) 02/07/22
|
||||
|
||||
## 0.18.2 - 2022-06-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add default `log_config` on `uvicorn.run()` (#1541) 24/06/22
|
||||
- Revert `logging` file name modification (#1543) 27/06/22
|
||||
|
||||
## 0.18.1 - 2022-06-23
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use `DEFAULT_MAX_INCOMPLETE_EVENT_SIZE` as default to `h11_max_incomplete_event_size` on the CLI (#1534) 23/06/22
|
||||
|
||||
## 0.18.0 - 2022-06-23
|
||||
|
||||
### Added
|
||||
|
||||
- The `reload` flag prioritizes `watchfiles` instead of the deprecated `watchgod` (#1437) 18/06/22
|
||||
- Annotate `uvicorn.run()` function (#1423) 10/05/22
|
||||
- Allow configuring `max_incomplete_event_size` for `h11` implementation (#1514) 22/06/22
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove `asgiref` dependency (#1532) 22/06/22
|
||||
|
||||
### Fixed
|
||||
|
||||
- Turn `raw_path` into bytes on both websockets implementations (#1487) 16/05/22
|
||||
- Revert log exception traceback in case of invalid HTTP request (#1518) 14/06/22
|
||||
- Set `asyncio.WindowsSelectorEventLoopPolicy()` when using multiple workers to avoid "WinError 87" (#1454) 22/06/22
|
||||
|
||||
## 0.17.6 - 2022-03-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Change `httptools` range to `>=0.4.0` (#1400) 11/03/22
|
||||
|
||||
## 0.17.5 - 2022-02-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix case where url is fragmented in httptools protocol (#1263) 16/02/22
|
||||
- Fix WSGI middleware not to explode quadratically in the case of a larger body (#1329) 16/02/16
|
||||
|
||||
### Changed
|
||||
|
||||
- Send HTTP 400 response for invalid request (#1352) 2/11/22
|
||||
|
||||
## 0.17.4 - 2022-02-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Replace `create_server` by `create_unix_server` (#1362) 04/02/22
|
||||
|
||||
## 0.17.3 - 2022-02-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Drop wsproto version checking. (#1359) 03/02/22
|
||||
|
||||
## 0.17.2 - 2022-02-03
|
||||
|
||||
### Fixed
|
||||
|
||||
- Revert #1332. While trying to solve the memory leak, it introduced an issue (#1345) when the server receives big chunks of data using the `httptools` implementation. (#1354) 03/02/22
|
||||
- Revert stream interface changes. This was introduced on 0.14.0, and caused an issue (#1226), which caused a memory leak when sending TCP pings. (#1355) 03/02/22
|
||||
- Fix wsproto version check expression (#1342) 28/01/22
|
||||
|
||||
## 0.17.1 - 2022-01-28
|
||||
|
||||
### Fixed
|
||||
|
||||
- Move all data handling logic to protocol and ensure connection is closed. (#1332) 28/01/22
|
||||
- Change `spec_version` field from "2.1" to "2.3", as Uvicorn is compliant with that version of the ASGI specifications. (#1337) 25/01/22
|
||||
|
||||
## 0.17.0.post1 - 2022-01-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add the `python_requires` version specifier (#1328) 17/01/22
|
||||
|
||||
## 0.17.0 - 2022-01-14
|
||||
|
||||
### Added
|
||||
|
||||
- Allow configurable websocket per-message-deflate setting (#1300) 29/12/21
|
||||
- Support extra_headers for WS accept message (#1293) 06/01/22
|
||||
- Add missing http version on websockets scope (#1309) 08/01/22
|
||||
|
||||
### Fixed/Removed
|
||||
|
||||
- Drop Python 3.6 support (#1261) 06/01/22
|
||||
- Fix reload process behavior when exception is raised (#1313) 11/01/22
|
||||
- Remove `root_path` from logs (#1294) 25/12/21
|
||||
|
||||
## 0.16.0 - 2021-12-08
|
||||
|
||||
### Added
|
||||
|
||||
- Enable read of uvicorn settings from environment variables (#1279) 06/12/21
|
||||
- Bump `websockets` to 10.0. (#1180) 13/09/21
|
||||
- Ensure non-zero exit code when startup fails (#1278) 06/12/21
|
||||
- Increase `httptools` version range from "==0.2.*" to ">=0.2.0,<0.4.0". (#1243) 8/11/21
|
||||
- Override default asyncio event loop with reload only on Windows (#1257) 24/11/21
|
||||
- Replace `HttpToolsProtocol.pipeline` type from `list` to `deque`. (#1213) 10/10/21
|
||||
- Replace `WSGIResponder.send_queue` type from `list` to `deque`. (#1214) 10/10/21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Main process exit after startup failure on reloader classes (#1177) 30/09/21
|
||||
- Add explicit casting on click options (#1217) 11/10/21
|
||||
- Allow WebSocket close event to receive reason being None from ASGI app. (#1259) 23/11/21
|
||||
- Fix a bug in `WebSocketProtocol.asgi_receive` on which we returned a close frame even if there were data messages before that frame in the read queue. (#1252) 25/11/21
|
||||
- The option `--reload-dirs` was splitting a string into single character directories. (#1267) 25/11/21
|
||||
- Only second SIGINT is able to forcefully shutdown the server (#1269) 28/11/21
|
||||
- Allow app-dir parameter on the run() function (#1271) 06/12/21
|
||||
|
||||
|
||||
## 0.15.0 - 2021-08-13
|
||||
|
||||
### Added
|
||||
|
||||
- Change reload to be configurable with glob patterns. Currently only `.py` files are watched, which is different from the previous default behavior. (#820) 08/08/21
|
||||
- Add Python 3.10-rc.1 support. Now the server uses `asyncio.run` which will: start a fresh asyncio event loop, on shutdown cancel any background tasks rather than aborting them, `aexit` any remaining async generators, and shutdown the default `ThreadPoolExecutor`. (#1070) 30/07/21
|
||||
- Exit with status 3 when worker starts failed (#1077) 22/06/21
|
||||
- Add option to set websocket ping interval and timeout (#1048) 09/06/21
|
||||
- Adapt bind_socket to make it usable with multiple processes (#1009) 21/06/21
|
||||
- Add existence check to the reload directory(ies) (#1089) 21/06/21
|
||||
- Add missing trace log for websocket protocols (#1083) 19/06/21
|
||||
- Support disabling default Server and Date headers (#818) 11/06/21
|
||||
|
||||
### Changed
|
||||
|
||||
- Add PEP440 compliant version of click (#1099) 29/06/21
|
||||
- Bump asgiref to 3.4.0 (#1100) 29/06/21
|
||||
|
||||
### Fixed
|
||||
|
||||
- When receiving a `SIGTERM` supervisors now terminate their processes before joining them (#1069) 30/07/21
|
||||
- Fix the need of `httptools` on minimal installation (#1135) 30/07/21
|
||||
- Fix ping parameters annotation in Config class (#1127) 19/07/21
|
||||
|
||||
## 0.14.0 - 2021-06-01
|
||||
|
||||
### Added
|
||||
|
||||
- Defaults ws max_size on server to 16MB (#995) 5/29/21
|
||||
- Improve user feedback if no ws library installed (#926 and #1023) 2/27/21
|
||||
- Support 'reason' field in 'websocket.close' messages (#957) 2/24/21
|
||||
- Implemented lifespan.shutdown.failed (#755) 2/25/21
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded websockets requirements (#1065) 6/1/21
|
||||
- Switch to asyncio streams API (#869) 5/29/21
|
||||
- Update httptools from 0.1.* to 0.2.* (#1024) 5/28/21
|
||||
- Allow Click 8.0, refs #1016 (#1042) 5/23/21
|
||||
- Add search for a trusted host in ProxyHeadersMiddleware (#591) 3/13/21
|
||||
- Up wsproto to 1.0.0 (#892) 2/25/21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Force reload_dirs to be a list (#978) 6/1/21
|
||||
- Fix gunicorn worker not running if extras not installed (#901) 5/28/21
|
||||
- Fix socket port 0 (#975) 3/5/21
|
||||
- Prevent garbage collection of main lifespan task (#972) 3/4/21
|
||||
|
||||
## 0.13.4 - 2021-02-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed wsgi middleware PATH_INFO encoding (#962) 2/20/21
|
||||
- Fixed uvloop dependency (#952) 2/10/21 then (#959) 2/20/21
|
||||
- Relax watchgod up bound (#946) 1/31/21
|
||||
- Return 'connection: close' header in response (#721) 1/25/21
|
||||
|
||||
### Added:
|
||||
|
||||
- Docs: Nginx + websockets (#948) 2/10/21
|
||||
- Document the default value of 1 for workers (#940) (#943) 1/25/21
|
||||
- Enabled permessage-deflate extension in websockets (#764) 1/1/21
|
||||
|
||||
## 0.13.3 - 2020-12-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent swallowing of return codes from `subprocess` when running with Gunicorn by properly resetting signals. (Pull #895)
|
||||
- Tweak detection of app factories to be more robust. A warning is now logged when passing a factory without the `--factory` flag. (Pull #914)
|
||||
- Properly clean tasks when handshake is aborted when running with `--ws websockets`. (Pull #921)
|
||||
|
||||
## 0.13.2 - 2020-12-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Log full exception traceback in case of invalid HTTP request. (Pull #886 and #888)
|
||||
|
||||
## 0.13.1 - 2020-12-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent exceptions when the ASGI application rejects a connection during the WebSocket handshake, when running on both `--ws wsproto` or `--ws websockets`. (Pull #704 and #881)
|
||||
- Ensure connection `scope` doesn't leak in logs when using JSON log formatters. (Pull #859 and #884)
|
||||
|
||||
## 0.13.0 - 2020-12-08
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--factory` flag to support factory-style application imports. (#875) 2020-12-07 50fc0d1c
|
||||
- Skip installation of signal handlers when not in the main thread. Allows using `Server` in multithreaded contexts without having to override `.install_signal_handlers()`. (#871) 2020-12-07 ce2ef45a
|
||||
|
||||
## 0.12.3 - 2020-11-21
|
||||
|
||||
### Fixed
|
||||
- Fix race condition that leads Quart to hang with uvicorn (#848) 11/18/20 de213614
|
||||
- Use latin1 when decoding X-Forwarded-* headers (#701) 11/12/20 45e6e831
|
||||
- Rework IPv6 support (#837) 11/8/20 bdab488e
|
||||
- Cancel old keepalive-trigger before setting new one. (#832) 10/26/20 d5dcf80c
|
||||
|
||||
## 0.12.2 - 2020-10-19
|
||||
|
||||
### Added
|
||||
- Adding ability to decrypt ssl key file (#808) 10/12/20 90dbb6e0
|
||||
- Support .yml log config files (#799) 10/6/20 b468950e
|
||||
- Added python 3.9 support (#804) 10/6/20 08fd0559
|
||||
|
||||
### Fixed
|
||||
- Fixes watchgod with common prefixes (#817) 10/14/20 1b32f997
|
||||
- Fix reload with ipv6 host (#803) 10/14/20 5acaee5b
|
||||
- Added cli support for headers containing colon (#813) 10/12/20 68732899
|
||||
- Sharing socket across workers on windows (#802) 10/12/20 103167a0
|
||||
- Note the need to configure trusted "ips" when using unix sockets (#796) 10/4/20 a504c569
|
||||
|
||||
## 0.12.1 - 2020-09-30
|
||||
|
||||
### Changed
|
||||
- Pinning h11 and python-dotenv to min versions (#789) 9/29/20 bbf19c66
|
||||
- Get docs/index.md in sync with README.md (#784) 9/29/20 70ebcfdf
|
||||
|
||||
### Fixed
|
||||
- Improve changelog by pointing out breaking changes (#792) 9/29/20 e2b75064
|
||||
|
||||
## 0.12.0 - 2020-09-28
|
||||
|
||||
### Added
|
||||
- Make reload delay configurable (#774) 9/28/20 98010027
|
||||
- Upgrade maximum h11 dependency version to 0.10 (#772) 8/28/20 54d729cc
|
||||
- Allow .json or .yaml --log-config files (#665) 8/18/20 093a1f7c
|
||||
- Add ASGI dict to the lifespan scope (#754) 8/15/20 8150c3eb
|
||||
- Upgrade wsproto to 0.15.0 (#750) 8/13/20 fbce393f
|
||||
- Use optional package installs (#666) 8/10/20 5fa99a11
|
||||
|
||||
### Changed
|
||||
- Dont set log level for root logger (#767) 8/28/20 df81b168
|
||||
- Uvicorn no longer ships extra dependencies `uvloop`, `websockets` and
|
||||
`httptools` as default. To install these dependencies use
|
||||
`uvicorn[standard]`.
|
||||
|
||||
### Fixed
|
||||
- Revert "Improve shutdown robustness when using `--reload` or multiprocessing (#620)" (#756) 8/28/20 ff4af12d
|
||||
- Fix terminate error in windows (#744) 8/27/20 dd3b842d
|
||||
- Fix bug where --log-config disables uvicorn loggers (#512) 8/11/20 a9c37cc4
|
||||
|
||||
## 0.11.8 - 2020-07-30
|
||||
|
||||
* Fix a regression that caused Uvicorn to crash when using `--interface=wsgi`. (Pull #730)
|
||||
* Fix a regression that caused Uvicorn to crash when using unix domain sockets. (Pull #729)
|
||||
|
||||
## 0.11.7 - 2020-28-07
|
||||
|
||||
* SECURITY FIX: Prevent sending invalid HTTP header names and values. (Pull #725)
|
||||
* SECURITY FIX: Ensure path value is escaped before logging to the console. (Pull #724)
|
||||
* Fix `--proxy-headers` client IP and host when using a Unix socket. (Pull #636)
|
||||
|
||||
## 0.11.6
|
||||
|
||||
* Fix overriding the root logger.
|
||||
|
||||
## 0.11.5
|
||||
|
||||
* Revert "Watch all files, not just .py" due to unexpected side effects.
|
||||
* Revert "Pass through gunicorn timeout config." due to unexpected side effects.
|
||||
|
||||
## 0.11.4
|
||||
|
||||
* Use `watchgod`, if installed, for watching code changes.
|
||||
* Watch all files, not just .py.
|
||||
* Pass through gunicorn timeout config.
|
||||
|
||||
## 0.11.3
|
||||
|
||||
* Update dependencies.
|
||||
|
||||
## 0.11.2
|
||||
|
||||
* Don't open socket until after application startup.
|
||||
* Support `--backlog`.
|
||||
|
||||
## 0.11.1
|
||||
|
||||
* Use a more liberal `h11` dependency. Either `0.8.*` or `0.9.*``.
|
||||
|
||||
## 0.11.0
|
||||
|
||||
* Fix reload/multiprocessing on Windows with Python 3.8.
|
||||
* Drop IOCP support. (Required for fix above.)
|
||||
* Add `uvicorn --version` flag.
|
||||
* Add `--use-colors` and `--no-use-colors` flags.
|
||||
* Display port correctly, when auto port selection isused with `--port=0`.
|
||||
|
||||
## 0.10.8
|
||||
|
||||
* Fix reload/multiprocessing error.
|
||||
|
||||
## 0.10.7
|
||||
|
||||
* Use resource_sharer.DupSocket to resolve socket sharing on Windows.
|
||||
|
||||
## 0.10.6
|
||||
|
||||
* Exit if `workers` or `reload` are use without an app import string style.
|
||||
* Reorganise supervisor processes to properly hand over sockets on windows.
|
||||
|
||||
## 0.10.5
|
||||
|
||||
* Update uvloop dependency to 0.14+
|
||||
|
||||
## 0.10.4
|
||||
|
||||
* Error clearly when `workers=<NUM>` is used with app instance, instead of an app import string.
|
||||
* Switch `--reload-dir` to current working directory by default.
|
||||
|
||||
## 0.10.3
|
||||
|
||||
* Add ``--log-level trace`
|
||||
|
||||
## 0.10.2
|
||||
|
||||
* Enable --proxy-headers by default.
|
||||
|
||||
## 0.10.1
|
||||
|
||||
* Resolve issues with logging when using `--reload` or `--workers`.
|
||||
* Setup up root logger to capture output for all logger instances, not just `uvicorn.error` and `uvicorn.access`.
|
||||
|
||||
## 0.10.0
|
||||
|
||||
* Support for Python 3.8
|
||||
* Separated out `uvicorn.error` and `uvicorn.access` logs.
|
||||
* Coloured log output when connected to a terminal.
|
||||
* Dropped `logger=` config setting.
|
||||
* Added `--log-config [FILE]` and `log_config=[str|dict]`. May either be a Python logging config dictionary or the file name of a logging configuration.
|
||||
* Added `--forwarded_allow_ips` and `forwarded_allow_ips`. Defaults to the value of the `$FORWARDED_ALLOW_IPS` environment variable or "127.0.0.1". The `--proxy-headers` flag now defaults to `True`, but only trusted IPs are used to populate forwarding info.
|
||||
* The `--workers` setting now defaults to the value of the `$WEB_CONCURRENCY` environment variable.
|
||||
* Added support for `--env-file`. Requires `python-dotenv`.
|
||||
1
CHANGELOG.md
Symbolic link
1
CHANGELOG.md
Symbolic link
@ -0,0 +1 @@
|
||||
docs/release-notes.md
|
||||
23
CITATION.cff
Normal file
23
CITATION.cff
Normal file
@ -0,0 +1,23 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: Uvicorn
|
||||
message: >-
|
||||
If you use this software, please cite it using the
|
||||
metadata from this file.
|
||||
type: software
|
||||
authors:
|
||||
- given-names: Marcelo
|
||||
family-names: Trylesinski
|
||||
email: marcelotryle@gmail.com
|
||||
- given-names: Tom
|
||||
family-names: Christie
|
||||
email: tom@tomchristie.com
|
||||
repository-code: "https://github.com/Kludex/uvicorn"
|
||||
url: "https://uvicorn.dev/"
|
||||
abstract: Uvicorn is an ASGI web server implementation for Python.
|
||||
keywords:
|
||||
- asgi
|
||||
- server
|
||||
license: BSD-3-Clause
|
||||
22
README.md
22
README.md
@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img width="320" height="320" src="https://raw.githubusercontent.com/tomchristie/uvicorn/master/docs/uvicorn.png" alt='uvicorn'>
|
||||
<img width="320" height="320" src="https://raw.githubusercontent.com/tomchristie/uvicorn/main/docs/uvicorn.png" alt='uvicorn'>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@ -8,13 +8,18 @@
|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/encode/uvicorn/actions)
|
||||
[](https://github.com/Kludex/uvicorn/actions)
|
||||
[](https://pypi.python.org/pypi/uvicorn)
|
||||
[](https://pypi.org/project/uvicorn)
|
||||
[](https://discord.gg/RxKUF5JuHs)
|
||||
|
||||
**Documentation**: [https://www.uvicorn.org](https://www.uvicorn.org)
|
||||
---
|
||||
|
||||
**Requirements**: Python 3.8+
|
||||
**Documentation**: [https://uvicorn.dev](https://uvicorn.dev)
|
||||
|
||||
**Source Code**: [https://www.github.com/Kludex/uvicorn](https://www.github.com/Kludex/uvicorn)
|
||||
|
||||
---
|
||||
|
||||
Uvicorn is an ASGI web server implementation for Python.
|
||||
|
||||
@ -131,12 +136,17 @@ $ hypercorn app:App
|
||||
|
||||
[Mangum][mangum] is an adapter for using ASGI applications with AWS Lambda & API Gateway.
|
||||
|
||||
### Granian
|
||||
|
||||
[Granian][granian] is an ASGI compatible Rust HTTP server which supports HTTP/2, TLS and WebSockets.
|
||||
|
||||
---
|
||||
|
||||
<p align="center"><i>Uvicorn is <a href="https://github.com/encode/uvicorn/blob/master/LICENSE.md">BSD licensed</a> code.<br/>Designed & crafted with care.</i><br/>— 🦄 —</p>
|
||||
<p align="center"><i>Uvicorn is <a href="https://github.com/Kludex/uvicorn/blob/main/LICENSE.md">BSD licensed</a> code.<br/>Designed & crafted with care.</i><br/>— 🦄 —</p>
|
||||
|
||||
[asgi]: https://asgi.readthedocs.io/en/latest/
|
||||
[daphne]: https://github.com/django/daphne
|
||||
[hypercorn]: https://github.com/pgjones/hypercorn
|
||||
[mangum]: https://mangum.io
|
||||
[trio]: https://trio.readthedocs.io
|
||||
[mangum]: https://github.com/jordaneremieff/mangum
|
||||
[granian]: https://github.com/emmett-framework/granian
|
||||
|
||||
279
docs/concepts/asgi.md
Normal file
279
docs/concepts/asgi.md
Normal file
@ -0,0 +1,279 @@
|
||||
## ASGI
|
||||
|
||||
**Uvicorn** uses the [ASGI specification](https://asgi.readthedocs.io/en/latest/) for interacting with an application.
|
||||
|
||||
The application should expose an async callable which takes three arguments:
|
||||
|
||||
* `scope` - A dictionary containing information about the incoming connection.
|
||||
* `receive` - A channel on which to receive incoming messages from the server.
|
||||
* `send` - A channel on which to send outgoing messages to the server.
|
||||
|
||||
Two common patterns you might use are either function-based applications:
|
||||
|
||||
```python
|
||||
async def app(scope, receive, send):
|
||||
assert scope['type'] == 'http'
|
||||
...
|
||||
```
|
||||
|
||||
Or instance-based applications:
|
||||
|
||||
```python
|
||||
class App:
|
||||
async def __call__(self, scope, receive, send):
|
||||
assert scope['type'] == 'http'
|
||||
...
|
||||
|
||||
app = App()
|
||||
```
|
||||
|
||||
It's good practice for applications to raise an exception on scope types
|
||||
that they do not handle.
|
||||
|
||||
The content of the `scope` argument, and the messages expected by `receive` and `send` depend on the protocol being used.
|
||||
|
||||
The format for HTTP messages is described in the [ASGI HTTP Message format](https://asgi.readthedocs.io/en/latest/specs/www.html).
|
||||
|
||||
### HTTP Scope
|
||||
|
||||
An incoming HTTP request might have a connection `scope` like this:
|
||||
|
||||
```python
|
||||
{
|
||||
'type': 'http',
|
||||
'scheme': 'http',
|
||||
'root_path': '',
|
||||
'server': ('127.0.0.1', 8000),
|
||||
'http_version': '1.1',
|
||||
'method': 'GET',
|
||||
'path': '/',
|
||||
'headers': [
|
||||
(b'host', b'127.0.0.1:8000'),
|
||||
(b'user-agent', b'curl/7.51.0'),
|
||||
(b'accept', b'*/*')
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Messages
|
||||
|
||||
The instance coroutine communicates back to the server by sending messages to the `send` coroutine.
|
||||
|
||||
```python
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
[b'content-type', b'text/plain'],
|
||||
]
|
||||
})
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': b'Hello, world!',
|
||||
})
|
||||
```
|
||||
|
||||
### Requests & responses
|
||||
|
||||
Here's an example that displays the method and path used in the incoming request:
|
||||
|
||||
```python
|
||||
async def app(scope, receive, send):
|
||||
"""
|
||||
Echo the method and path back in an HTTP response.
|
||||
"""
|
||||
assert scope['type'] == 'http'
|
||||
|
||||
body = f'Received {scope["method"]} request to {scope["path"]}'
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
[b'content-type', b'text/plain'],
|
||||
]
|
||||
})
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': body.encode('utf-8'),
|
||||
})
|
||||
```
|
||||
|
||||
### Reading the request body
|
||||
|
||||
You can stream the request body without blocking the asyncio task pool,
|
||||
by fetching messages from the `receive` coroutine.
|
||||
|
||||
```python
|
||||
async def read_body(receive):
|
||||
"""
|
||||
Read and return the entire body from an incoming ASGI message.
|
||||
"""
|
||||
body = b''
|
||||
more_body = True
|
||||
|
||||
while more_body:
|
||||
message = await receive()
|
||||
body += message.get('body', b'')
|
||||
more_body = message.get('more_body', False)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
"""
|
||||
Echo the request body back in an HTTP response.
|
||||
"""
|
||||
body = await read_body(receive)
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
(b'content-type', b'text/plain'),
|
||||
(b'content-length', str(len(body)).encode())
|
||||
]
|
||||
})
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': body,
|
||||
})
|
||||
```
|
||||
|
||||
### Streaming responses
|
||||
|
||||
You can stream responses by sending multiple `http.response.body` messages to
|
||||
the `send` coroutine.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
"""
|
||||
Send a slowly streaming HTTP response back to the client.
|
||||
"""
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
[b'content-type', b'text/plain'],
|
||||
]
|
||||
})
|
||||
for chunk in [b'Hello', b', ', b'world!']:
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': chunk,
|
||||
'more_body': True
|
||||
})
|
||||
await asyncio.sleep(1)
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': b'',
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why ASGI?
|
||||
|
||||
Most well established Python Web frameworks started out as WSGI-based frameworks.
|
||||
|
||||
WSGI applications are a single, synchronous callable that takes a request and returns a response.
|
||||
This doesn’t allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections,
|
||||
which WSGI doesn't support well.
|
||||
|
||||
Having an async concurrency model also allows for options such as lightweight background tasks,
|
||||
and can be less of a limiting factor for endpoints that have long periods being blocked on network
|
||||
I/O such as dealing with slow HTTP requests.
|
||||
|
||||
---
|
||||
|
||||
## Alternative ASGI servers
|
||||
|
||||
A strength of the ASGI protocol is that it decouples the server implementation
|
||||
from the application framework. This allows for an ecosystem of interoperating
|
||||
webservers and application frameworks.
|
||||
|
||||
### Daphne
|
||||
|
||||
The first ASGI server implementation, originally developed to power Django Channels, is
|
||||
[the Daphne webserver](https://github.com/django/daphne).
|
||||
|
||||
It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets.
|
||||
|
||||
Any of the example applications given here can equally well be run using `daphne` instead.
|
||||
|
||||
```shell
|
||||
pip install daphne
|
||||
daphne app:App
|
||||
```
|
||||
|
||||
### Hypercorn
|
||||
|
||||
[Hypercorn](https://github.com/pgjones/hypercorn) was initially part of the Quart web framework,
|
||||
before being separated out into a standalone ASGI server.
|
||||
|
||||
Hypercorn supports HTTP/1.1, HTTP/2, HTTP/3 and WebSockets.
|
||||
|
||||
```shell
|
||||
pip install hypercorn
|
||||
hypercorn app:App
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ASGI frameworks
|
||||
|
||||
You can use Uvicorn, Daphne, or Hypercorn to run any ASGI framework.
|
||||
|
||||
For small services you can also write ASGI applications directly.
|
||||
|
||||
### Starlette
|
||||
|
||||
[Starlette](https://github.com/Kludex/starlette) is a lightweight ASGI framework/toolkit.
|
||||
|
||||
It is ideal for building high performance asyncio services, and supports both HTTP and WebSockets.
|
||||
|
||||
### Django Channels
|
||||
|
||||
The ASGI specification was originally designed for use with [Django Channels](https://channels.readthedocs.io/en/latest/).
|
||||
|
||||
Channels is a little different to other ASGI frameworks in that it provides
|
||||
an asynchronous frontend onto a threaded-framework backend. It allows Django
|
||||
to support WebSockets, background tasks, and long-running connections,
|
||||
with application code still running in a standard threaded context.
|
||||
|
||||
### Quart
|
||||
|
||||
[Quart](https://pgjones.gitlab.io/quart/) is a Flask-like ASGI web framework.
|
||||
|
||||
### FastAPI
|
||||
|
||||
[**FastAPI**](https://github.com/tiangolo/fastapi) is an API framework based on **Starlette** and **Pydantic**, heavily inspired by previous server versions of **APIStar**.
|
||||
|
||||
You write your API function parameters with Python 3.6+ type declarations and get automatic data conversion, data validation, OpenAPI schemas (with JSON Schemas) and interactive API documentation UIs.
|
||||
|
||||
### BlackSheep
|
||||
|
||||
[BlackSheep](https://www.neoteroi.dev/blacksheep/) is a web framework based on ASGI, inspired by Flask and ASP.NET Core.
|
||||
|
||||
Its most distinctive features are built-in support for dependency injection, automatic binding of parameters by request handler's type annotations, and automatic generation of OpenAPI documentation and Swagger UI.
|
||||
|
||||
### Falcon
|
||||
|
||||
[Falcon](https://falconframework.org) is a minimalist REST and app backend framework for Python, with a focus on reliability, correctness, and performance at scale.
|
||||
|
||||
### Muffin
|
||||
|
||||
[Muffin](https://github.com/klen/muffin) is a fast, lightweight and asynchronous ASGI web-framework for Python 3.
|
||||
|
||||
### Litestar
|
||||
|
||||
[Litestar](https://litestar.dev) is a powerful, lightweight and flexible ASGI framework.
|
||||
|
||||
It includes everything that's needed to build modern APIs - from data serialization and validation to websockets, ORM integration, session management, authentication and more.
|
||||
|
||||
### Panther
|
||||
|
||||
[Panther](https://PantherPy.github.io/) is a fast & friendly web framework for building async APIs with Python 3.10+.
|
||||
|
||||
It has built-in Document-oriented Database, Caching System, Authentication and Permission Classes, Visual API Monitoring and also supports Websocket, Throttling, Middlewares.
|
||||
75
docs/concepts/event-loop.md
Normal file
75
docs/concepts/event-loop.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Event Loop
|
||||
|
||||
Uvicorn provides two event loop implementations that you can choose from using the [`--loop`](../settings.md#implementation) option:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --loop <auto|asyncio|uvloop>
|
||||
```
|
||||
|
||||
By default, Uvicorn uses `--loop auto`, which automatically selects:
|
||||
|
||||
1. **uvloop** - If [uvloop](https://github.com/MagicStack/uvloop) is installed, Uvicorn will use it for maximum performance
|
||||
2. **asyncio** - If uvloop is not available, Uvicorn falls back to Python's built-in asyncio event loop
|
||||
|
||||
Since `uvloop` is not compatible with Windows or PyPy, it is not available on these platforms.
|
||||
|
||||
On Windows, the asyncio implementation uses the standard [`ProactorEventLoop`][asyncio.ProactorEventLoop] in single-process mode.
|
||||
When running with `--reload` or multiple workers, it uses [`SelectorEventLoop`][asyncio.SelectorEventLoop] instead.
|
||||
|
||||
??? info "Why can `ProactorEventLoop` fail with multiple processes on Windows?"
|
||||
If you want to know more about it, you can read the issue [#cpython/122240](https://github.com/python/cpython/issues/122240).
|
||||
|
||||
## Custom Event Loop
|
||||
|
||||
You can use custom event loop implementations by specifying a module path and function name using the colon notation:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --loop <module>:<function>
|
||||
```
|
||||
|
||||
The function should return a callable that creates a new event loop instance.
|
||||
|
||||
### rloop
|
||||
|
||||
[rloop](https://github.com/gi0baro/rloop) is an experimental AsyncIO event loop implemented in Rust on top of the [mio](https://github.com/tokio-rs/mio) crate. It aims to provide high performance through Rust's systems programming capabilities.
|
||||
|
||||
You can install it with:
|
||||
|
||||
=== "pip"
|
||||
```bash
|
||||
pip install rloop
|
||||
```
|
||||
=== "uv"
|
||||
```bash
|
||||
uv add rloop
|
||||
```
|
||||
|
||||
You can run `uvicorn` with `rloop` with the following command:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --loop rloop:new_event_loop
|
||||
```
|
||||
|
||||
!!! warning "Experimental"
|
||||
rloop is currently **experimental** and **not suited for production usage**. It is only available on **Unix systems**.
|
||||
|
||||
### Winloop
|
||||
|
||||
[Winloop](https://github.com/Vizonex/Winloop) is an alternative library that brings uvloop-like performance to Windows. Since uvloop is based on libuv and doesn't support Windows, Winloop provides a Windows-compatible implementation with significant performance improvements over the standard Windows event loop policies.
|
||||
|
||||
You can install it with:
|
||||
|
||||
=== "pip"
|
||||
```bash
|
||||
pip install winloop
|
||||
```
|
||||
=== "uv"
|
||||
```bash
|
||||
uv add winloop
|
||||
```
|
||||
|
||||
You can run `uvicorn` with `Winloop` with the following command:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --loop winloop:new_event_loop
|
||||
```
|
||||
109
docs/concepts/lifespan.md
Normal file
109
docs/concepts/lifespan.md
Normal file
@ -0,0 +1,109 @@
|
||||
Since Uvicorn is an ASGI server, it supports the
|
||||
[ASGI lifespan protocol](https://asgi.readthedocs.io/en/latest/specs/lifespan.html).
|
||||
This allows you to run **startup** and **shutdown** events for your application.
|
||||
|
||||
The lifespan protocol is useful for initializing resources that need to be available throughout
|
||||
the lifetime of the application, such as database connections, caches, or other services.
|
||||
|
||||
Keep in mind that the lifespan is executed **only once per application instance**. If you have
|
||||
multiple workers, each worker will execute the lifespan independently.
|
||||
|
||||
## Lifespan Architecture
|
||||
|
||||
The lifespan protocol runs as a sibling task alongside your main application, allowing both to execute concurrently.
|
||||
|
||||
Let's see how Uvicorn handles the lifespan and main application tasks:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Server as Uvicorn Server
|
||||
participant LifespanTask as Lifespan Task
|
||||
participant AppTask as Application Task
|
||||
participant UserApp as User Application
|
||||
|
||||
Note over Server: ✅ Server starts
|
||||
|
||||
Server->>+LifespanTask: spawn_task(lifespan_handler)
|
||||
|
||||
LifespanTask->>UserApp: {"type": "lifespan.startup"}
|
||||
|
||||
Note over UserApp: Initialize databases, caches, etc.
|
||||
|
||||
UserApp-->>LifespanTask: {"type": "lifespan.startup.complete"}
|
||||
LifespanTask->>Server: ✅ Startup complete
|
||||
|
||||
Server->>+AppTask: spawn_task(application_handler)
|
||||
Note over AppTask: ✅ Ready for requests
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
Note over LifespanTask, AppTask: Both tasks running concurrently
|
||||
|
||||
par Lifespan maintains state
|
||||
LifespanTask->>LifespanTask: Keep lifespan connection alive
|
||||
and Application serves requests
|
||||
AppTask->>UserApp: HTTP/WebSocket requests
|
||||
UserApp-->>AppTask: Responses
|
||||
end
|
||||
end
|
||||
|
||||
Note over Server: Shutdown signal received
|
||||
|
||||
Server->>AppTask: Stop accepting new connections
|
||||
AppTask->>AppTask: Complete pending requests
|
||||
|
||||
LifespanTask->>UserApp: {"type": "lifespan.shutdown"}
|
||||
|
||||
Note over UserApp: Cleanup databases, caches, etc.
|
||||
|
||||
UserApp-->>LifespanTask: {"type": "lifespan.shutdown.complete"}
|
||||
|
||||
LifespanTask->>-Server: Lifespan task complete
|
||||
AppTask->>-Server: Application task complete
|
||||
|
||||
Note over Server: ✅ Server stopped
|
||||
```
|
||||
|
||||
Having the lifespan task run as a sibling task is a deliberate design choice. It could have been implemented as a parent task that spawns the
|
||||
application task. This decision has the implication that if you create a [`ContextVar`][contextvars.ContextVar]
|
||||
in the lifespan task, it will not be available in the application task.
|
||||
|
||||
## Usage
|
||||
|
||||
Let's see an example of a minimal (but complete) ASGI application that implements the lifespan protocol:
|
||||
|
||||
```python title="ASGI application with lifespan" hl_lines="3-11"
|
||||
async def app(scope, receive, send):
|
||||
if scope['type'] == 'lifespan':
|
||||
while True:
|
||||
message = await receive()
|
||||
if message['type'] == 'lifespan.startup':
|
||||
print("Application is starting up...")
|
||||
await send({'type': 'lifespan.startup.complete'})
|
||||
elif message['type'] == 'lifespan.shutdown':
|
||||
print("Application is shutting down...")
|
||||
await send({'type': 'lifespan.shutdown.complete'})
|
||||
return
|
||||
elif scope['type'] == 'http':
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [(b'content-type', b'text/plain')],
|
||||
})
|
||||
await send({'type': 'http.response.body', 'body': b'Hello, World!'})
|
||||
else:
|
||||
raise RuntimeError("This server doesn't support WebSocket.")
|
||||
```
|
||||
|
||||
You can run the above application with `uvicorn main:app`. Then you'll see the print statements when the
|
||||
application starts. You can also try to send some HTTP requests to it, and it will respond with "Hello, World!".
|
||||
And if you stop the server (`CTRL + C`), it will print `"Application is shutting down..."`.
|
||||
|
||||
## Disabling Lifespan
|
||||
|
||||
If you want to disable the lifespan protocol, you can do so by setting the `lifespan` option to `off` when running Uvicorn:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --lifespan off
|
||||
```
|
||||
|
||||
By default, Uvicorn will automatically enable the lifespan protocol if the application supports it.
|
||||
319
docs/concepts/logging.md
Normal file
319
docs/concepts/logging.md
Normal file
@ -0,0 +1,319 @@
|
||||
Uvicorn uses Python's built-in [`logging`](https://docs.python.org/3/library/logging.html)
|
||||
module, and provides three loggers out of the box:
|
||||
|
||||
| Logger name | Purpose |
|
||||
|------------------|----------------------------------------------------|
|
||||
| `uvicorn` | Parent logger (rarely used directly) |
|
||||
| `uvicorn.error` | Server-level messages (startup, shutdown, errors) |
|
||||
| `uvicorn.access` | Per-request access log lines |
|
||||
|
||||
!!! note
|
||||
Despite its name, `uvicorn.error` is **not** limited to error messages.
|
||||
It is the general-purpose server logger, similar to how Gunicorn names its
|
||||
main logger. See [#562](https://github.com/encode/uvicorn/issues/562) for
|
||||
background.
|
||||
|
||||
## Default Configuration
|
||||
|
||||
By default, Uvicorn applies the following
|
||||
[`dictConfig()`](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig)
|
||||
configuration:
|
||||
|
||||
```python
|
||||
LOGGING_CONFIG = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(levelprefix)s %(message)s",
|
||||
"use_colors": None,
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
"access": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn.error": {"level": "INFO"},
|
||||
"uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Logging Configuration
|
||||
|
||||
You can supply a custom logging configuration file with the `--log-config`
|
||||
option (or `log_config` when calling `uvicorn.run()`).
|
||||
|
||||
Uvicorn supports three file formats:
|
||||
|
||||
| Extension | Loader | Notes |
|
||||
|----------------|------------------------------|---------------------------------------------|
|
||||
| `.json` | `logging.config.dictConfig` | Standard JSON `dictConfig` schema. |
|
||||
| `.yaml`/`.yml` | `logging.config.dictConfig` | Requires **PyYAML** (`uvicorn[standard]`). |
|
||||
| Any other | `logging.config.fileConfig` | Classic INI-style format. |
|
||||
|
||||
### YAML Example
|
||||
|
||||
Create a file named `log_config.yaml`:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
disable_existing_loggers: false
|
||||
formatters:
|
||||
default:
|
||||
"()": uvicorn.logging.DefaultFormatter
|
||||
fmt: "%(asctime)s - %(levelprefix)s %(message)s"
|
||||
datefmt: "%Y-%m-%d %H:%M:%S"
|
||||
use_colors: null
|
||||
access:
|
||||
"()": uvicorn.logging.AccessFormatter
|
||||
fmt: '%(asctime)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
|
||||
datefmt: "%Y-%m-%d %H:%M:%S"
|
||||
handlers:
|
||||
default:
|
||||
formatter: default
|
||||
class: logging.StreamHandler
|
||||
stream: ext://sys.stderr
|
||||
access:
|
||||
formatter: access
|
||||
class: logging.StreamHandler
|
||||
stream: ext://sys.stdout
|
||||
loggers:
|
||||
uvicorn:
|
||||
handlers:
|
||||
- default
|
||||
level: INFO
|
||||
propagate: false
|
||||
uvicorn.error:
|
||||
level: INFO
|
||||
uvicorn.access:
|
||||
handlers:
|
||||
- access
|
||||
level: INFO
|
||||
propagate: false
|
||||
```
|
||||
|
||||
Then pass it to Uvicorn:
|
||||
|
||||
=== "CLI"
|
||||
|
||||
```bash
|
||||
uvicorn main:app --log-config log_config.yaml
|
||||
```
|
||||
|
||||
=== "Programmatic"
|
||||
|
||||
```python
|
||||
uvicorn.run("main:app", log_config="log_config.yaml")
|
||||
```
|
||||
|
||||
### JSON Example
|
||||
|
||||
Create a file named `log_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"disable_existing_loggers": false,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(asctime)s - %(levelprefix)s %(message)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
"use_colors": null
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": "%(asctime)s - %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr"
|
||||
},
|
||||
"access": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout"
|
||||
}
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {
|
||||
"handlers": ["default"],
|
||||
"level": "INFO",
|
||||
"propagate": false
|
||||
},
|
||||
"uvicorn.error": {
|
||||
"level": "INFO"
|
||||
},
|
||||
"uvicorn.access": {
|
||||
"handlers": ["access"],
|
||||
"level": "INFO",
|
||||
"propagate": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic `dictConfig`
|
||||
|
||||
You can also pass a dictionary directly when running programmatically:
|
||||
|
||||
```python
|
||||
import uvicorn
|
||||
|
||||
log_config = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(asctime)s - %(levelprefix)s %(message)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": '%(asctime)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s',
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
"access": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
||||
"uvicorn.error": {"level": "INFO"},
|
||||
"uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
|
||||
},
|
||||
}
|
||||
|
||||
uvicorn.run("main:app", log_config=log_config)
|
||||
```
|
||||
|
||||
## Common Recipes
|
||||
|
||||
### Writing Logs to a File
|
||||
|
||||
To write Uvicorn's server logs to a file in addition to the console, add a `FileHandler` to the `uvicorn` logger:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
disable_existing_loggers: false
|
||||
formatters:
|
||||
default:
|
||||
"()": uvicorn.logging.DefaultFormatter
|
||||
fmt: "%(asctime)s - %(levelprefix)s %(message)s"
|
||||
datefmt: "%Y-%m-%d %H:%M:%S"
|
||||
use_colors: false
|
||||
access:
|
||||
"()": uvicorn.logging.AccessFormatter
|
||||
fmt: '%(asctime)s - %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
|
||||
datefmt: "%Y-%m-%d %H:%M:%S"
|
||||
handlers:
|
||||
default:
|
||||
formatter: default
|
||||
class: logging.StreamHandler
|
||||
stream: ext://sys.stderr
|
||||
access:
|
||||
formatter: access
|
||||
class: logging.StreamHandler
|
||||
stream: ext://sys.stdout
|
||||
file:
|
||||
formatter: default
|
||||
class: logging.FileHandler
|
||||
filename: uvicorn.log
|
||||
loggers:
|
||||
uvicorn:
|
||||
handlers:
|
||||
- default
|
||||
- file
|
||||
level: INFO
|
||||
propagate: false
|
||||
uvicorn.error:
|
||||
level: INFO
|
||||
uvicorn.access:
|
||||
handlers:
|
||||
- access
|
||||
level: INFO
|
||||
propagate: false
|
||||
```
|
||||
|
||||
In this example, `uvicorn.access` still writes to stdout only. To write access
|
||||
logs to the file as well, add `file` to the `uvicorn.access.handlers` list.
|
||||
|
||||
### Disabling Access Logs
|
||||
|
||||
Use the `--no-access-log` CLI flag, or set `access_log=False` programmatically.
|
||||
This removes all handlers from `uvicorn.access` without affecting the
|
||||
`uvicorn.error` logger.
|
||||
|
||||
### Disabling Colors
|
||||
|
||||
Pass `--no-use-colors` on the command line, or set `use_colors=False`
|
||||
programmatically. When using a custom `--log-config`, set `use_colors: false`
|
||||
on each formatter that extends `uvicorn.logging.ColourizedFormatter`.
|
||||
|
||||
### Using a Standard Formatter
|
||||
|
||||
If you do not need Uvicorn's colorized output, you can use the standard
|
||||
`logging.Formatter` instead:
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
disable_existing_loggers: false
|
||||
formatters:
|
||||
default:
|
||||
format: "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
datefmt: "%Y-%m-%d %H:%M:%S"
|
||||
handlers:
|
||||
default:
|
||||
formatter: default
|
||||
class: logging.StreamHandler
|
||||
stream: ext://sys.stderr
|
||||
loggers:
|
||||
uvicorn:
|
||||
handlers:
|
||||
- default
|
||||
level: INFO
|
||||
propagate: false
|
||||
uvicorn.error:
|
||||
level: INFO
|
||||
uvicorn.access:
|
||||
handlers:
|
||||
- default
|
||||
level: INFO
|
||||
propagate: false
|
||||
```
|
||||
|
||||
!!! warning
|
||||
When using a standard `logging.Formatter` for the access logger, the
|
||||
`%(client_addr)s`, `%(request_line)s`, and `%(status_code)s` placeholders
|
||||
are **not** available. The access log line will be formatted using only the
|
||||
standard `%(message)s` field.
|
||||
107
docs/concepts/websockets.md
Normal file
107
docs/concepts/websockets.md
Normal file
@ -0,0 +1,107 @@
|
||||
**Uvicorn** supports the WebSocket protocol as defined in [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).
|
||||
|
||||
## Upgrade Process
|
||||
|
||||
The WebSocket protocol starts as an HTTP connection that gets "upgraded" to a WebSocket connection
|
||||
through a handshake process. Here's how it works:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Server
|
||||
participant ASGI App
|
||||
|
||||
Note over Client,ASGI App: WebSocket Handshake Process
|
||||
|
||||
Client->>Server: HTTP GET Request
|
||||
Note right of Client: Headers:<br/>Upgrade: websocket<br/>Connection: Upgrade<br/>Sec-WebSocket-Key: [key]<br/>Sec-WebSocket-Version: 13
|
||||
|
||||
Server->>ASGI App: websocket.connect event
|
||||
Note right of Server: Scope type: "websocket"
|
||||
|
||||
alt Connection Accepted
|
||||
ASGI App->>Server: {"type": "websocket.accept"}
|
||||
Server->>Client: HTTP 101 Switching Protocols
|
||||
Note right of Server: Headers:<br/>Upgrade: websocket<br/>Connection: Upgrade<br/>Sec-WebSocket-Accept: [hash]
|
||||
|
||||
Note over Client,ASGI App: WebSocket Connection Established
|
||||
|
||||
loop Message Exchange
|
||||
Client->>Server: WebSocket Frame
|
||||
Server->>ASGI App: websocket.receive event
|
||||
ASGI App->>Server: {"type": "websocket.send", "text": "..."}
|
||||
Server->>Client: WebSocket Frame
|
||||
end
|
||||
|
||||
alt Client Closes
|
||||
Client->>Server: Close Frame
|
||||
Server->>ASGI App: websocket.disconnect event
|
||||
else Server Closes
|
||||
ASGI App->>Server: {"type": "websocket.close"}
|
||||
Server->>Client: Close Frame
|
||||
end
|
||||
|
||||
else Connection Rejected
|
||||
ASGI App->>Server: {"type": "websocket.http.response.start", "status": 403}
|
||||
Server->>Client: HTTP 403 Forbidden
|
||||
end
|
||||
```
|
||||
|
||||
1. **Initial HTTP Request**: The client sends a regular HTTP GET request with special headers indicating it wants to upgrade to WebSocket:
|
||||
- `Upgrade: websocket`
|
||||
- `Connection: Upgrade`
|
||||
- `Sec-WebSocket-Key`: A base64-encoded random key
|
||||
- `Sec-WebSocket-Version: 13`
|
||||
|
||||
2. **Server Processing**: Uvicorn receives the request and creates a WebSocket scope, sending a `websocket.connect` event to the ASGI application.
|
||||
|
||||
3. **Application Decision**: The ASGI app decides whether to accept or reject the connection based on authentication, authorization, or other logic.
|
||||
|
||||
4. **Handshake Completion**: If accepted, the server responds with HTTP 101 status and the computed `Sec-WebSocket-Accept` header.
|
||||
|
||||
5. **Full-Duplex Communication**: Once upgraded, both client and server can send messages at any time using WebSocket frames.
|
||||
|
||||
6. **Connection Termination**: Either side can initiate closing the connection with a close frame.
|
||||
|
||||
## ASGI WebSocket Events
|
||||
|
||||
**Uvicorn** translates WebSocket protocol messages into ASGI events:
|
||||
|
||||
- `websocket.connect`: Sent when a client requests a WebSocket upgrade
|
||||
- `websocket.receive`: Sent when a message is received from the client
|
||||
- `websocket.disconnect`: Sent when the connection is closed
|
||||
|
||||
The ASGI app can respond with:
|
||||
|
||||
- `websocket.accept`: Accept the connection upgrade with an optional subprotocol
|
||||
- `websocket.send`: Send a message to the client
|
||||
- `websocket.close`: Close the connection with an optional status code
|
||||
|
||||
You can read more about it on the [ASGI documentation](https://asgi.readthedocs.io/en/latest/specs/www.html#websocket).
|
||||
|
||||
## Protocol Implementations
|
||||
|
||||
**Uvicorn** has three implementations of the WebSocket protocol.
|
||||
|
||||
### WSProto Protocol
|
||||
|
||||
This implementation was the first implemented. It uses the
|
||||
[`wsproto`](https://python-hyper.org/projects/wsproto/en/stable/) package underneath.
|
||||
|
||||
You can choose this protocol by setting the `--ws` option to `wsproto`.
|
||||
|
||||
### WebSocket Protocol
|
||||
|
||||
This implementation uses the [`websockets`](https://websockets.readthedocs.io/) package as dependency.
|
||||
|
||||
By default, if you have `websockets` installed, Uvicorn will use this protocol.
|
||||
|
||||
### WebSockets SansIO Protocol
|
||||
|
||||
Since `websockets` deprecated the API Uvicorn uses to run the previous protocol, we had to create this new
|
||||
protocol that uses the `websockets` SansIO API.
|
||||
|
||||
You can choose this protocol by setting the `--ws` option to `websockets-sansio`.
|
||||
|
||||
!!! note
|
||||
The SansIO implementation was released in Uvicorn version 0.35.0 in June 2025.
|
||||
@ -3,9 +3,9 @@
|
||||
Thank you for being interested in contributing to Uvicorn.
|
||||
There are many ways you can contribute to the project:
|
||||
|
||||
- Using Uvicorn on your stack and [reporting bugs/issues you find](https://github.com/encode/uvicorn/issues/new)
|
||||
- [Implementing new features and fixing bugs](https://github.com/encode/uvicorn/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
|
||||
- [Review Pull Requests of others](https://github.com/encode/uvicorn/pulls)
|
||||
- Using Uvicorn on your stack and [reporting bugs/issues you find](https://github.com/Kludex/uvicorn/issues/new)
|
||||
- [Implementing new features and fixing bugs](https://github.com/Kludex/uvicorn/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
|
||||
- [Review Pull Requests of others](https://github.com/Kludex/uvicorn/pulls)
|
||||
- Write documentation
|
||||
- Participate in discussions
|
||||
|
||||
@ -16,8 +16,8 @@ Stumbled upon some unexpected behaviour?
|
||||
Need a missing functionality?
|
||||
|
||||
Contributions should generally start out from a previous discussion.
|
||||
You can reach out someone at the [community chat](https://gitter.im/encode/community)
|
||||
or at the [github discussions tab](https://github.com/encode/uvicorn/discussions).
|
||||
You can reach out someone at the [community chat](https://discord.com/invite/SWU73HffbV)
|
||||
or at the [github discussions tab](https://github.com/Kludex/uvicorn/discussions).
|
||||
|
||||
When creating a new topic in the discussions tab, possible bugs may be raised
|
||||
as a "Potential Issue" discussion, feature requests may be raised as an
|
||||
@ -45,7 +45,7 @@ Some possibly useful tips for narrowing down potential issues...
|
||||
## Development
|
||||
|
||||
To start developing Uvicorn create a **fork** of the
|
||||
[Uvicorn repository](https://github.com/encode/uvicorn) on GitHub.
|
||||
[Uvicorn repository](https://github.com/Kludex/uvicorn) on GitHub.
|
||||
|
||||
Then clone your fork with the following command replacing `YOUR-USERNAME` with
|
||||
your GitHub username:
|
||||
@ -110,7 +110,7 @@ If the test suite fails, you'll want to click through to the
|
||||
"Details" link, and try to identify why the test suite failed.
|
||||
|
||||
<p align="center" style="margin: 0 0 10px">
|
||||
<img src="https://raw.githubusercontent.com/encode/uvicorn/master/docs/img/gh-actions-fail.png" alt='Failing PR commit status'>
|
||||
<img src="https://raw.githubusercontent.com/Kludex/uvicorn/main/docs/img/gh-actions-fail.png" alt='Failing PR commit status'>
|
||||
</p>
|
||||
|
||||
Here are some common ways the test suite can fail:
|
||||
@ -118,7 +118,7 @@ Here are some common ways the test suite can fail:
|
||||
### Check Job Failed
|
||||
|
||||
<p align="center" style="margin: 0 0 10px">
|
||||
<img src="https://raw.githubusercontent.com/encode/uvicorn/master/docs/img/gh-actions-fail-check.png" alt='Failing GitHub action lint job'>
|
||||
<img src="https://raw.githubusercontent.com/Kludex/uvicorn/main/docs/img/gh-actions-fail-check.png" alt='Failing GitHub action lint job'>
|
||||
</p>
|
||||
|
||||
This job failing means there is either a code formatting issue or type-annotation issue.
|
||||
@ -144,10 +144,10 @@ If tests are failing you will see this message under the coverage report:
|
||||
|
||||
`=== 1 failed, 354 passed, 1 skipped, 1 xfailed in 37.08s ===`
|
||||
|
||||
If tests succeed but coverage doesn't reach our current threshold, you will see this
|
||||
If tests succeed but coverage doesn't reach 100%, you will see this
|
||||
message under the coverage report:
|
||||
|
||||
`Coverage failure: total of 88 is less than fail-under=95`
|
||||
`Coverage failure: total of 98 is less than fail-under=100`
|
||||
|
||||
## Releasing
|
||||
|
||||
@ -157,23 +157,20 @@ Before releasing a new version, create a pull request that includes:
|
||||
|
||||
- **An update to the changelog**:
|
||||
- We follow the format from [keepachangelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- [Compare](https://github.com/encode/uvicorn/compare/) `master` with the tag of the latest release, and list all entries that are of interest to our users:
|
||||
- [Compare](https://github.com/Kludex/uvicorn/compare/) `main` with the tag of the latest release, and list all entries that are of interest to our users:
|
||||
- Things that **must** go in the changelog: added, changed, deprecated or removed features, and bug fixes.
|
||||
- Things that **should not** go in the changelog: changes to documentation, tests or tooling.
|
||||
- Try sorting entries in descending order of impact / importance.
|
||||
- Keep it concise and to-the-point. 🎯
|
||||
- **A version bump**: see `__init__.py`.
|
||||
|
||||
For an example, see [#1006](https://github.com/encode/uvicorn/pull/1107).
|
||||
For an example, see [#1006](https://github.com/Kludex/uvicorn/pull/1107).
|
||||
|
||||
Once the release PR is merged, create a
|
||||
[new release](https://github.com/encode/uvicorn/releases/new) including:
|
||||
[new release](https://github.com/Kludex/uvicorn/releases/new) including:
|
||||
|
||||
- Tag version like `0.13.3`.
|
||||
- Release title `Version 0.13.3`
|
||||
- Description copied from the changelog.
|
||||
|
||||
Once created this release will be automatically uploaded to PyPI.
|
||||
|
||||
If something goes wrong with the PyPI job the release can be published using the
|
||||
`scripts/publish` script.
|
||||
|
||||
57
docs/css/extra.css
Normal file
57
docs/css/extra.css
Normal file
@ -0,0 +1,57 @@
|
||||
.md-nav__sponsors {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 1.2rem 0.4rem 0.6rem;
|
||||
padding: 0.9rem 0.6rem 0.8rem;
|
||||
background-color: color-mix(in srgb, var(--md-primary-fg-color) 8%, transparent);
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.md-nav__sponsors-title {
|
||||
margin: 0 0 0.1rem;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--md-default-fg-color--light);
|
||||
}
|
||||
|
||||
.md-nav__sponsor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.2rem;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.md-nav__sponsor:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.md-nav__sponsor img {
|
||||
max-width: 100%;
|
||||
max-height: 1.6rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.md-nav__sponsor-cta {
|
||||
display: inline-block;
|
||||
margin-top: 0.15rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 600;
|
||||
color: var(--md-primary-bg-color);
|
||||
background-color: var(--md-primary-fg-color);
|
||||
border-radius: 0.2rem;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.md-nav__sponsor-cta:hover {
|
||||
opacity: 0.85;
|
||||
color: var(--md-primary-bg-color);
|
||||
}
|
||||
@ -1,339 +0,0 @@
|
||||
# Deployment
|
||||
|
||||
Server deployment is a complex area, that will depend on what kind of service you're deploying Uvicorn onto.
|
||||
|
||||
As a general rule, you probably want to:
|
||||
|
||||
* Run `uvicorn --reload` from the command line for local development.
|
||||
* Run `gunicorn -k uvicorn.workers.UvicornWorker` for production.
|
||||
* Additionally run behind Nginx for self-hosted deployments.
|
||||
* Finally, run everything behind a CDN for caching support, and serious DDOS protection.
|
||||
|
||||
## Running from the command line
|
||||
|
||||
Typically you'll run `uvicorn` from the command line.
|
||||
|
||||
```bash
|
||||
$ uvicorn main:app --reload --port 5000
|
||||
```
|
||||
|
||||
The ASGI application should be specified in the form `path.to.module:instance.path`.
|
||||
|
||||
When running locally, use `--reload` to turn on auto-reloading.
|
||||
|
||||
The `--reload` and `--workers` arguments are **mutually exclusive**.
|
||||
|
||||
To see the complete set of available options, use `uvicorn --help`:
|
||||
|
||||
<!-- :cli_usage: -->
|
||||
```
|
||||
$ uvicorn --help
|
||||
Usage: uvicorn [OPTIONS] APP
|
||||
|
||||
Options:
|
||||
--host TEXT Bind socket to this host. [default:
|
||||
127.0.0.1]
|
||||
--port INTEGER Bind socket to this port. If 0, an available
|
||||
port will be picked. [default: 8000]
|
||||
--uds TEXT Bind to a UNIX domain socket.
|
||||
--fd INTEGER Bind to socket from this file descriptor.
|
||||
--reload Enable auto-reload.
|
||||
--reload-dir PATH Set reload directories explicitly, instead
|
||||
of using the current working directory.
|
||||
--reload-include TEXT Set glob patterns to include while watching
|
||||
for files. Includes '*.py' by default; these
|
||||
defaults can be overridden with `--reload-
|
||||
exclude`. This option has no effect unless
|
||||
watchfiles is installed.
|
||||
--reload-exclude TEXT Set glob patterns to exclude while watching
|
||||
for files. Includes '.*, .py[cod], .sw.*,
|
||||
~*' by default; these defaults can be
|
||||
overridden with `--reload-include`. This
|
||||
option has no effect unless watchfiles is
|
||||
installed.
|
||||
--reload-delay FLOAT Delay between previous and next check if
|
||||
application needs to be. Defaults to 0.25s.
|
||||
[default: 0.25]
|
||||
--workers INTEGER Number of worker processes. Defaults to the
|
||||
$WEB_CONCURRENCY environment variable if
|
||||
available, or 1. Not valid with --reload.
|
||||
--loop [auto|asyncio|uvloop] Event loop implementation. [default: auto]
|
||||
--http [auto|h11|httptools] HTTP protocol implementation. [default:
|
||||
auto]
|
||||
--ws [auto|none|websockets|wsproto]
|
||||
WebSocket protocol implementation.
|
||||
[default: auto]
|
||||
--ws-max-size INTEGER WebSocket max size message in bytes
|
||||
[default: 16777216]
|
||||
--ws-max-queue INTEGER The maximum length of the WebSocket message
|
||||
queue. [default: 32]
|
||||
--ws-ping-interval FLOAT WebSocket ping interval in seconds.
|
||||
[default: 20.0]
|
||||
--ws-ping-timeout FLOAT WebSocket ping timeout in seconds.
|
||||
[default: 20.0]
|
||||
--ws-per-message-deflate BOOLEAN
|
||||
WebSocket per-message-deflate compression
|
||||
[default: True]
|
||||
--lifespan [auto|on|off] Lifespan implementation. [default: auto]
|
||||
--interface [auto|asgi3|asgi2|wsgi]
|
||||
Select ASGI3, ASGI2, or WSGI as the
|
||||
application interface. [default: auto]
|
||||
--env-file PATH Environment configuration file.
|
||||
--log-config PATH Logging configuration file. Supported
|
||||
formats: .ini, .json, .yaml.
|
||||
--log-level [critical|error|warning|info|debug|trace]
|
||||
Log level. [default: info]
|
||||
--access-log / --no-access-log Enable/Disable access log.
|
||||
--use-colors / --no-use-colors Enable/Disable colorized logging.
|
||||
--proxy-headers / --no-proxy-headers
|
||||
Enable/Disable X-Forwarded-Proto,
|
||||
X-Forwarded-For, X-Forwarded-Port to
|
||||
populate remote address info.
|
||||
--server-header / --no-server-header
|
||||
Enable/Disable default Server header.
|
||||
--date-header / --no-date-header
|
||||
Enable/Disable default Date header.
|
||||
--forwarded-allow-ips TEXT Comma separated list of IPs to trust with
|
||||
proxy headers. Defaults to the
|
||||
$FORWARDED_ALLOW_IPS environment variable if
|
||||
available, or '127.0.0.1'.
|
||||
--root-path TEXT Set the ASGI 'root_path' for applications
|
||||
submounted below a given URL path.
|
||||
--limit-concurrency INTEGER Maximum number of concurrent connections or
|
||||
tasks to allow, before issuing HTTP 503
|
||||
responses.
|
||||
--backlog INTEGER Maximum number of connections to hold in
|
||||
backlog
|
||||
--limit-max-requests INTEGER Maximum number of requests to service before
|
||||
terminating the process.
|
||||
--timeout-keep-alive INTEGER Close Keep-Alive connections if no new data
|
||||
is received within this timeout. [default:
|
||||
5]
|
||||
--timeout-graceful-shutdown INTEGER
|
||||
Maximum number of seconds to wait for
|
||||
graceful shutdown.
|
||||
--ssl-keyfile TEXT SSL key file
|
||||
--ssl-certfile TEXT SSL certificate file
|
||||
--ssl-keyfile-password TEXT SSL keyfile password
|
||||
--ssl-version INTEGER SSL version to use (see stdlib ssl module's)
|
||||
[default: 17]
|
||||
--ssl-cert-reqs INTEGER Whether client certificate is required (see
|
||||
stdlib ssl module's) [default: 0]
|
||||
--ssl-ca-certs TEXT CA certificates file
|
||||
--ssl-ciphers TEXT Ciphers to use (see stdlib ssl module's)
|
||||
[default: TLSv1]
|
||||
--header TEXT Specify custom default HTTP response headers
|
||||
as a Name:Value pair
|
||||
--version Display the uvicorn version and exit.
|
||||
--app-dir TEXT Look for APP in the specified directory, by
|
||||
adding this to the PYTHONPATH. Defaults to
|
||||
the current working directory.
|
||||
--h11-max-incomplete-event-size INTEGER
|
||||
For h11, the maximum number of bytes to
|
||||
buffer of an incomplete event.
|
||||
--factory Treat APP as an application factory, i.e. a
|
||||
() -> <ASGI app> callable.
|
||||
--help Show this message and exit.
|
||||
```
|
||||
|
||||
|
||||
See the [settings documentation](settings.md) for more details on the supported options for running uvicorn.
|
||||
|
||||
## Running programmatically
|
||||
|
||||
To run directly from within a Python program, you should use `uvicorn.run(app, **config)`. For example:
|
||||
|
||||
```py title="main.py"
|
||||
import uvicorn
|
||||
|
||||
class App:
|
||||
...
|
||||
|
||||
app = App()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", host="127.0.0.1", port=5000, log_level="info")
|
||||
```
|
||||
|
||||
The set of configuration options is the same as for the command line tool.
|
||||
|
||||
Note that the application instance itself *can* be passed instead of the app
|
||||
import string.
|
||||
|
||||
```python
|
||||
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info")
|
||||
```
|
||||
|
||||
However, this style only works if you are not using multiprocessing (`workers=NUM`)
|
||||
or reloading (`reload=True`), so we recommend using the import string style.
|
||||
|
||||
Also note that in this case, you should put `uvicorn.run` into `if __name__ == '__main__'` clause in the main module.
|
||||
|
||||
!!! note
|
||||
The `reload` and `workers` parameters are **mutually exclusive**.
|
||||
|
||||
## Using a process manager
|
||||
|
||||
Running Uvicorn using a process manager ensures that you can run multiple processes in a resilient manner, and allows you to perform server upgrades without dropping requests.
|
||||
|
||||
A process manager will handle the socket setup, start-up multiple server processes, monitor process aliveness, and listen for signals to provide for processes restarts, shutdowns, or dialing up and down the number of running processes.
|
||||
|
||||
Uvicorn provides a lightweight way to run multiple worker processes, for example `--workers 4`, but does not provide any process monitoring.
|
||||
|
||||
### Gunicorn
|
||||
|
||||
Gunicorn is probably the simplest way to run and manage Uvicorn in a production setting. Uvicorn includes a gunicorn worker class that means you can get set up with very little configuration.
|
||||
|
||||
The following will start Gunicorn with four worker processes:
|
||||
|
||||
`gunicorn -w 4 -k uvicorn.workers.UvicornWorker`
|
||||
|
||||
The `UvicornWorker` implementation uses the `uvloop` and `httptools` implementations. To run under PyPy you'll want to use pure-python implementation instead. You can do this by using the `UvicornH11Worker` class.
|
||||
|
||||
`gunicorn -w 4 -k uvicorn.workers.UvicornH11Worker`
|
||||
|
||||
Gunicorn provides a different set of configuration options to Uvicorn, so some options such as `--limit-concurrency` are not yet supported when running with Gunicorn.
|
||||
|
||||
If you need to pass uvicorn's config arguments to gunicorn workers then you'll have to subclass `UvicornWorker`:
|
||||
|
||||
```python
|
||||
from uvicorn.workers import UvicornWorker
|
||||
|
||||
class MyUvicornWorker(UvicornWorker):
|
||||
CONFIG_KWARGS = {"loop": "asyncio", "http": "h11", "lifespan": "off"}
|
||||
```
|
||||
|
||||
### Supervisor
|
||||
|
||||
To use `supervisor` as a process manager you should either:
|
||||
|
||||
* Hand over the socket to uvicorn using its file descriptor, which supervisor always makes available as `0`, and which must be set in the `fcgi-program` section.
|
||||
* Or use a UNIX domain socket for each `uvicorn` process.
|
||||
|
||||
A simple supervisor configuration might look something like this:
|
||||
|
||||
```ini title="supervisord.conf"
|
||||
[supervisord]
|
||||
|
||||
[fcgi-program:uvicorn]
|
||||
socket=tcp://localhost:8000
|
||||
command=venv/bin/uvicorn --fd 0 main:App
|
||||
numprocs=4
|
||||
process_name=uvicorn-%(process_num)d
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
```
|
||||
|
||||
Then run with `supervisord -n`.
|
||||
|
||||
### Circus
|
||||
|
||||
To use `circus` as a process manager, you should either:
|
||||
|
||||
* Hand over the socket to uvicorn using its file descriptor, which circus makes available as `$(circus.sockets.web)`.
|
||||
* Or use a UNIX domain socket for each `uvicorn` process.
|
||||
|
||||
A simple circus configuration might look something like this:
|
||||
|
||||
```ini title="circus.ini"
|
||||
[watcher:web]
|
||||
cmd = venv/bin/uvicorn --fd $(circus.sockets.web) main:App
|
||||
use_sockets = True
|
||||
numprocesses = 4
|
||||
|
||||
[socket:web]
|
||||
host = 0.0.0.0
|
||||
port = 8000
|
||||
```
|
||||
|
||||
Then run `circusd circus.ini`.
|
||||
|
||||
## Running behind Nginx
|
||||
|
||||
Using Nginx as a proxy in front of your Uvicorn processes may not be necessary, but is recommended for additional resilience. Nginx can deal with serving your static media and buffering slow requests, leaving your application servers free from load as much as possible.
|
||||
|
||||
In managed environments such as `Heroku`, you won't typically need to configure Nginx, as your server processes will already be running behind load balancing proxies.
|
||||
|
||||
The recommended configuration for proxying from Nginx is to use a UNIX domain socket between Nginx and whatever the process manager that is being used to run Uvicorn.
|
||||
Note that when doing this you will need to run Uvicorn with `--forwarded-allow-ips='*'` to ensure that the domain socket is trusted as a source from which to proxy headers.
|
||||
|
||||
When fronting the application with a proxy server you want to make sure that the proxy sets headers to ensure that the application can properly determine the client address of the incoming connection, and if the connection was over `http` or `https`.
|
||||
|
||||
You should ensure that the `X-Forwarded-For` and `X-Forwarded-Proto` headers are set by the proxy, and that Uvicorn is run using the `--proxy-headers` setting. This ensures that the ASGI scope includes correct `client` and `scheme` information.
|
||||
|
||||
Here's how a simple Nginx configuration might look. This example includes setting proxy headers, and using a UNIX domain socket to communicate with the application server.
|
||||
|
||||
It also includes some basic configuration to forward websocket connections. For more info on this, check [Nginx recommendations][nginx_websocket].
|
||||
|
||||
```conf
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
client_max_body_size 4G;
|
||||
|
||||
server_name example.com;
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
proxy_pass http://uvicorn;
|
||||
}
|
||||
|
||||
location /static {
|
||||
# path for static files
|
||||
root /path/to/app/static;
|
||||
}
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
upstream uvicorn {
|
||||
server unix:/tmp/uvicorn.sock;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Uvicorn's `--proxy-headers` behavior may not be sufficient for more complex proxy configurations that use different combinations of headers, or where the application is running behind more than one intermediary proxying service.
|
||||
|
||||
In those cases, you might want to use an ASGI middleware to set the `client` and `scheme` dependant on the request headers.
|
||||
|
||||
## Running behind a CDN
|
||||
|
||||
Running behind a content delivery network, such as Cloudflare or Cloud Front, provides a serious layer of protection against DDOS attacks. Your service will be running behind huge clusters of proxies and load balancers that are designed for handling huge amounts of traffic, and have capabilities for detecting and closing off connections from DDOS attacks.
|
||||
|
||||
Proper usage of cache control headers can mean that a CDN is able to serve large amounts of data without always having to forward the request on to your server.
|
||||
|
||||
Content Delivery Networks can also be a low-effort way to provide HTTPS termination.
|
||||
|
||||
## Running with HTTPS
|
||||
|
||||
To run uvicorn with https, a certificate and a private key are required.
|
||||
The recommended way to get them is using [Let's Encrypt][letsencrypt].
|
||||
|
||||
For local development with https, it's possible to use [mkcert][mkcert]
|
||||
to generate a valid certificate and private key.
|
||||
|
||||
```bash
|
||||
$ uvicorn main:app --port 5000 --ssl-keyfile=./key.pem --ssl-certfile=./cert.pem
|
||||
```
|
||||
|
||||
### Running gunicorn worker
|
||||
|
||||
It's also possible to use certificates with uvicorn's worker for gunicorn.
|
||||
|
||||
```bash
|
||||
$ gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornWorker main:app
|
||||
```
|
||||
|
||||
[nginx_websocket]: https://nginx.org/en/docs/http/websocket.html
|
||||
[letsencrypt]: https://letsencrypt.org/
|
||||
[mkcert]: https://github.com/FiloSottile/mkcert
|
||||
153
docs/deployment/docker.md
Normal file
153
docs/deployment/docker.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Dockerfile
|
||||
|
||||
**Docker** is a popular choice for modern application deployment. However, creating a good Dockerfile from scratch can be challenging. This guide provides a **solid foundation** that works well for most Python projects.
|
||||
|
||||
While the example below won't fit every use case, it offers an excellent starting point that you can adapt to your specific needs.
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
For this example, we'll need to install [`docker`](https://docs.docker.com/get-docker/),
|
||||
[docker-compose](https://docs.docker.com/compose/install/) and
|
||||
[`uv`](https://docs.astral.sh/uv/getting-started/installation/).
|
||||
|
||||
Then, let's create a new project with `uv`:
|
||||
|
||||
```bash
|
||||
uv init app
|
||||
```
|
||||
|
||||
This will create a new project with a basic structure:
|
||||
|
||||
```bash
|
||||
app/
|
||||
├── main.py
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
On `main.py`, let's create a simple ASGI application:
|
||||
|
||||
```python title="main.py"
|
||||
async def app(scope, receive, send):
|
||||
body = "Hello, world!"
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": [
|
||||
[b"content-type", b"text/plain"],
|
||||
[b"content-length", len(body)],
|
||||
],
|
||||
}
|
||||
)
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.body",
|
||||
"body": body.encode("utf-8"),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
We need to include `uvicorn` in the dependencies:
|
||||
|
||||
```bash
|
||||
uv add uvicorn
|
||||
```
|
||||
|
||||
This will also create a `uv.lock` file. :sunglasses:
|
||||
|
||||
??? tip "What is `uv.lock`?"
|
||||
|
||||
`uv.lock` is a `uv` specific lockfile. A lockfile is a file that contains the exact versions of the dependencies
|
||||
that were installed when the `uv.lock` file was created.
|
||||
|
||||
This allows for deterministic builds and consistent deployments.
|
||||
|
||||
Just to make sure everything is working, let's run the application:
|
||||
|
||||
```bash
|
||||
uv run uvicorn main:app
|
||||
```
|
||||
|
||||
You should see the following output:
|
||||
|
||||
```bash
|
||||
INFO: Started server process [62727]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: ASGI 'lifespan' protocol appears unsupported.
|
||||
INFO: Application startup complete.
|
||||
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
|
||||
```
|
||||
|
||||
## Dockerfile
|
||||
|
||||
We'll create a **cache-aware Dockerfile** that optimizes build times. The key strategy is to install dependencies first, then copy the project files. This approach leverages Docker's caching mechanism to significantly speed up rebuilds.
|
||||
|
||||
```dockerfile title="Dockerfile"
|
||||
FROM python:3.12-slim
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
# Change the working directory to the `app` directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project
|
||||
|
||||
# Copy the project into the image
|
||||
ADD . /app
|
||||
|
||||
# Sync the project
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen
|
||||
|
||||
# Run with uvicorn
|
||||
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
```
|
||||
|
||||
A common question is **"how many workers should I run?"**. The image above uses a single Uvicorn worker.
|
||||
The recommended approach is to let your orchestration system manage the number of deployed containers rather than
|
||||
relying on the process manager inside the container.
|
||||
|
||||
You can read more about this in the
|
||||
[Decouple applications](https://docs.docker.com/build/building/best-practices/#decouple-applications) section
|
||||
of the Docker documentation.
|
||||
|
||||
!!! warning "For production, create a non-root user!"
|
||||
When running in production, you should create a non-root user and run the container as that user.
|
||||
|
||||
To make sure it works, let's build the image and run it:
|
||||
|
||||
```bash
|
||||
docker build -t my-app .
|
||||
docker run -p 8000:8000 my-app
|
||||
```
|
||||
|
||||
For more information on using uv with Docker, refer to the
|
||||
[official uv Docker integration guide](https://docs.astral.sh/uv/guides/integration/docker/).
|
||||
|
||||
## Docker Compose
|
||||
|
||||
When running in development, it's often useful to have a way to hot-reload the application when code changes.
|
||||
|
||||
Let's create a `docker-compose.yml` file to run the application:
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
backend:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- UVICORN_RELOAD=true
|
||||
volumes:
|
||||
- .:/app
|
||||
tty: true
|
||||
```
|
||||
|
||||
You can run the application with `docker compose up` and it will automatically rebuild the image when code changes.
|
||||
|
||||
Now you have a fully working development environment! :tada:
|
||||
297
docs/deployment/index.md
Normal file
297
docs/deployment/index.md
Normal file
@ -0,0 +1,297 @@
|
||||
Server deployment is a complex area, that will depend on what kind of service you're deploying Uvicorn onto.
|
||||
|
||||
As a general rule, you probably want to:
|
||||
|
||||
* Run `uvicorn --reload` from the command line for local development.
|
||||
* Run `gunicorn -k uvicorn.workers.UvicornWorker` for production.
|
||||
* Additionally run behind Nginx for self-hosted deployments.
|
||||
* Finally, run everything behind a CDN for caching support, and serious DDOS protection.
|
||||
|
||||
## Running from the command line
|
||||
|
||||
Typically you'll run `uvicorn` from the command line.
|
||||
|
||||
```bash
|
||||
$ uvicorn main:app --reload --port 5000
|
||||
```
|
||||
|
||||
The ASGI application should be specified in the form `path.to.module:instance.path`.
|
||||
|
||||
When running locally, use `--reload` to turn on auto-reloading.
|
||||
|
||||
The `--reload` and `--workers` arguments are **mutually exclusive**.
|
||||
|
||||
To see the complete set of available options, use `uvicorn --help`:
|
||||
|
||||
```bash
|
||||
{{ uvicorn_help }}
|
||||
```
|
||||
|
||||
See the [settings documentation](../settings.md) for more details on the supported options for running uvicorn.
|
||||
|
||||
## Running programmatically
|
||||
|
||||
To run directly from within a Python program, you should use `uvicorn.run(app, **config)`. For example:
|
||||
|
||||
```py title="main.py"
|
||||
import uvicorn
|
||||
|
||||
class App:
|
||||
...
|
||||
|
||||
app = App()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("main:app", host="127.0.0.1", port=5000, log_level="info")
|
||||
```
|
||||
|
||||
The set of configuration options is the same as for the command line tool.
|
||||
|
||||
Note that the application instance itself *can* be passed instead of the app
|
||||
import string.
|
||||
|
||||
```python
|
||||
uvicorn.run(app, host="127.0.0.1", port=5000, log_level="info")
|
||||
```
|
||||
|
||||
However, this style only works if you are not using multiprocessing (`workers=NUM`)
|
||||
or reloading (`reload=True`), so we recommend using the import string style.
|
||||
|
||||
Also note that in this case, you should put `uvicorn.run` into `if __name__ == '__main__'` clause in the main module.
|
||||
|
||||
!!! note
|
||||
The `reload` and `workers` parameters are **mutually exclusive**.
|
||||
|
||||
## Using a process manager
|
||||
|
||||
Running Uvicorn using a process manager ensures that you can run multiple processes in a resilient manner, and allows you to perform server upgrades without dropping requests.
|
||||
|
||||
A process manager will handle the socket setup, start-up multiple server processes, monitor process aliveness, and listen for signals to provide for processes restarts, shutdowns, or dialing up and down the number of running processes.
|
||||
|
||||
### Built-in
|
||||
|
||||
Uvicorn includes a `--workers` option that allows you to run multiple worker processes.
|
||||
|
||||
```bash
|
||||
$ uvicorn main:app --workers 4
|
||||
```
|
||||
|
||||
Unlike gunicorn, uvicorn does not use pre-fork, but uses [`spawn`](https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods), which allows uvicorn's multiprocess manager to still work well on Windows.
|
||||
|
||||
The default process manager monitors the status of child processes and automatically restarts child processes that die unexpectedly. Not only that, it will also monitor the status of the child process through the pipeline. When the child process is accidentally stuck, the corresponding child process will be killed through an unstoppable system signal or interface.
|
||||
|
||||
You can also manage child processes by sending specific signals to the main process. (Not supported on Windows.)
|
||||
|
||||
- `SIGHUP`: Work processes are graceful restarted one after another. If you update the code, the new worker process will use the new code.
|
||||
- `SIGTTIN`: Increase the number of worker processes by one.
|
||||
- `SIGTTOU`: Decrease the number of worker processes by one.
|
||||
|
||||
### Gunicorn
|
||||
|
||||
!!! warning
|
||||
The `uvicorn.workers` module is deprecated and will be removed in a future release.
|
||||
|
||||
You should use the [`uvicorn-worker`](https://github.com/Kludex/uvicorn-worker) package instead.
|
||||
|
||||
```bash
|
||||
python -m pip install uvicorn-worker
|
||||
```
|
||||
|
||||
Gunicorn is probably the simplest way to run and manage Uvicorn in a production setting. Uvicorn includes a gunicorn worker class that means you can get set up with very little configuration.
|
||||
|
||||
The following will start Gunicorn with four worker processes:
|
||||
|
||||
`gunicorn -w 4 -k uvicorn.workers.UvicornWorker`
|
||||
|
||||
The `UvicornWorker` implementation uses the `uvloop` and `httptools` implementations. To run under PyPy you'll want to use pure-python implementation instead. You can do this by using the `UvicornH11Worker` class.
|
||||
|
||||
`gunicorn -w 4 -k uvicorn.workers.UvicornH11Worker`
|
||||
|
||||
Gunicorn provides a different set of configuration options to Uvicorn, so some options such as `--limit-concurrency` are not yet supported when running with Gunicorn.
|
||||
|
||||
If you need to pass uvicorn's config arguments to gunicorn workers then you'll have to subclass `UvicornWorker`:
|
||||
|
||||
```python
|
||||
from uvicorn.workers import UvicornWorker
|
||||
|
||||
class MyUvicornWorker(UvicornWorker):
|
||||
CONFIG_KWARGS = {"loop": "asyncio", "http": "h11", "lifespan": "off"}
|
||||
```
|
||||
|
||||
### Supervisor
|
||||
|
||||
To use `supervisor` as a process manager you should either:
|
||||
|
||||
* Hand over the socket to uvicorn using its file descriptor, which supervisor always makes available as `0`, and which must be set in the `fcgi-program` section.
|
||||
* Or use a UNIX domain socket for each `uvicorn` process.
|
||||
|
||||
A simple supervisor configuration might look something like this:
|
||||
|
||||
```ini title="supervisord.conf"
|
||||
[supervisord]
|
||||
|
||||
[fcgi-program:uvicorn]
|
||||
socket=tcp://localhost:8000
|
||||
command=venv/bin/uvicorn --fd 0 main:App
|
||||
numprocs=4
|
||||
process_name=uvicorn-%(process_num)d
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
```
|
||||
|
||||
Then run with `supervisord -n`.
|
||||
|
||||
## Running behind Nginx
|
||||
|
||||
Using Nginx as a proxy in front of your Uvicorn processes may not be necessary, but is recommended for additional resilience. Nginx can deal with serving your static media and buffering slow requests, leaving your application servers free from load as much as possible.
|
||||
|
||||
In managed environments such as `Heroku`, you won't typically need to configure Nginx, as your server processes will already be running behind load balancing proxies.
|
||||
|
||||
The recommended configuration for proxying from Nginx is to use a UNIX domain socket between Nginx and whatever the process manager that is being used to run Uvicorn. If using Uvicorn directly you can bind it to a UNIX domain socket using `uvicorn --uds /path/to/socket.sock <...>`.
|
||||
|
||||
When running your application behind one or more proxies you will want to make sure that each proxy sets appropriate headers to ensure that your application can properly determine the client address of the incoming connection, and if the connection was over `http` or `https`. For more information see [Proxies and Forwarded Headers](#proxies-and-forwarded-headers) below.
|
||||
|
||||
Here's how a simple Nginx configuration might look. This example includes setting proxy headers, and using a UNIX domain socket to communicate with the application server.
|
||||
|
||||
It also includes some basic configuration to forward websocket connections.
|
||||
For more info on this, check [Nginx recommendations](https://nginx.org/en/docs/http/websocket.html).
|
||||
|
||||
```conf
|
||||
http {
|
||||
server {
|
||||
listen 80;
|
||||
client_max_body_size 4G;
|
||||
|
||||
server_name example.com;
|
||||
|
||||
location / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
proxy_pass http://uvicorn;
|
||||
}
|
||||
|
||||
location /static {
|
||||
# path for static files
|
||||
root /path/to/app/static;
|
||||
}
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
upstream uvicorn {
|
||||
server unix:/tmp/uvicorn.sock;
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
Uvicorn's `--proxy-headers` behavior may not be sufficient for more complex proxy configurations that use different combinations of headers, or where the application is running behind more than one intermediary proxying service.
|
||||
|
||||
In those cases, you might want to use an ASGI middleware to set the `client` and `scheme` dependant on the request headers.
|
||||
|
||||
## Running behind a CDN
|
||||
|
||||
Running behind a content delivery network, such as Cloudflare or Cloud Front, provides a serious layer of protection against DDoS attacks. Your service will be running behind huge clusters of proxies and load balancers that are designed for handling huge amounts of traffic, and have capabilities for detecting and closing off connections from DDoS attacks.
|
||||
|
||||
Proper usage of cache control headers can mean that a CDN is able to serve large amounts of data without always having to forward the request on to your server.
|
||||
|
||||
Content Delivery Networks can also be a low-effort way to provide HTTPS termination.
|
||||
|
||||
## Running with HTTPS
|
||||
|
||||
To run uvicorn with https, a certificate and a private key are required.
|
||||
The recommended way to get them is using [Let's Encrypt](https://letsencrypt.org/).
|
||||
|
||||
For local development with https, it's possible to use [mkcert](https://github.com/FiloSottile/mkcert)
|
||||
to generate a valid certificate and private key.
|
||||
|
||||
```bash
|
||||
$ uvicorn main:app --port 5000 --ssl-keyfile=./key.pem --ssl-certfile=./cert.pem
|
||||
```
|
||||
|
||||
### Running gunicorn worker
|
||||
|
||||
It's also possible to use certificates with uvicorn's worker for gunicorn.
|
||||
|
||||
```bash
|
||||
$ gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornWorker main:app
|
||||
```
|
||||
|
||||
### Customizing the SSL context
|
||||
|
||||
For TLS scenarios that the `--ssl-*` flags don't cover (e.g., mutual TLS, custom `SSLContext.options`, bumping `minimum_version`, loading certificates from memory), pass an `ssl_context_factory` to `uvicorn.run()` or `Config`.
|
||||
|
||||
The factory receives the `Config` instance and a `default_ssl_context_factory` callable that builds the standard context from the `ssl_*` settings on `Config`. Use it to start from uvicorn's default and mutate it, or ignore it and build your own context from scratch - the `ssl_*` settings are only consumed by the default factory, so if you don't call it they're effectively unused.
|
||||
|
||||
```python
|
||||
import ssl
|
||||
from collections.abc import Callable
|
||||
|
||||
import uvicorn
|
||||
from uvicorn.config import Config
|
||||
|
||||
|
||||
def ssl_context_factory(config: Config, default_ssl_context_factory: Callable[[], ssl.SSLContext]) -> ssl.SSLContext:
|
||||
context = default_ssl_context_factory()
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
return context
|
||||
|
||||
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
ssl_keyfile="key.pem",
|
||||
ssl_certfile="cert.pem",
|
||||
ssl_context_factory=ssl_context_factory,
|
||||
)
|
||||
```
|
||||
|
||||
The factory is called inside each worker process, so it works with `--reload` and `--workers > 1`. The factory itself must be picklable in those modes (a top-level function is fine; lambdas and local closures are not). The `ssl_*` settings on `Config` are only consumed by `default_ssl_context_factory()`; if you build the context yourself without calling it, those settings are ignored.
|
||||
|
||||
## Proxies and Forwarded Headers
|
||||
|
||||
When running an application behind one or more proxies, certain information about the request is lost.
|
||||
To avoid this most proxies will add headers containing this information for downstream servers to read.
|
||||
|
||||
Uvicorn currently supports the following headers:
|
||||
|
||||
- `X-Forwarded-For` ([MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For))
|
||||
- `X-Forwarded-Proto`([MDN Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto))
|
||||
|
||||
Uvicorn can use these headers to correctly set the client and protocol in the request.
|
||||
However as anyone can set these headers you must configure which "clients" you will trust to have set them correctly.
|
||||
|
||||
Uvicorn can be configured to trust IP Addresses (e.g. `127.0.0.1`), IP Networks (e.g. `10.100.0.0/16`),
|
||||
or Literals (e.g. `/path/to/socket.sock`). When running from CLI these are configured using `--forwarded-allow-ips`.
|
||||
|
||||
!!! Warning "Only trust clients you can actually trust!"
|
||||
Incorrectly trusting other clients can lead to malicious actors spoofing their apparent client address to your application.
|
||||
|
||||
For more information, check [`ProxyHeadersMiddleware`](https://github.com/Kludex/uvicorn/blob/main/uvicorn/middleware/proxy_headers.py).
|
||||
|
||||
### Client Port
|
||||
|
||||
Currently if the `ProxyHeadersMiddleware` is able to retrieve a trusted client value then the client's port will be set to `0`.
|
||||
This is because port information is lost when using these headers.
|
||||
|
||||
### UNIX Domain Sockets (UDS)
|
||||
|
||||
Although it is common for UNIX Domain Sockets to be used for communicating between various HTTP servers, they can mess with some of the expected received values as they will be various non-address strings or missing values.
|
||||
|
||||
For example:
|
||||
|
||||
- when NGINX itself is running behind a UDS it will add the literal `unix:` as the client in the `X-Forwarded-For` header.
|
||||
- When Uvicorn is running behind a UDS the initial client will be `None`.
|
||||
|
||||
### Trust Everything
|
||||
|
||||
Rather than specifying what to trust, you can instruct Uvicorn to trust all clients using the literal `"*"`.
|
||||
You should only set this when you know you can trust all values within the forwarded headers (e.g. because
|
||||
your proxies remove the existing headers before setting their own).
|
||||
BIN
docs/img/fastapi-logo.png
Normal file
BIN
docs/img/fastapi-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
496
docs/index.md
496
docs/index.md
@ -1,3 +1,10 @@
|
||||
<style>
|
||||
.md-typeset h1,
|
||||
.md-content__button {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p align="center">
|
||||
<img width="320" height="320" src="../../uvicorn.png" alt='uvicorn'>
|
||||
</p>
|
||||
@ -7,8 +14,8 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/encode/uvicorn/actions">
|
||||
<img src="https://github.com/encode/uvicorn/workflows/Test%20Suite/badge.svg" alt="Test Suite">
|
||||
<a href="https://github.com/Kludex/uvicorn/actions">
|
||||
<img src="https://github.com/Kludex/uvicorn/workflows/Test%20Suite/badge.svg" alt="Test Suite">
|
||||
</a>
|
||||
<a href="https://pypi.org/project/uvicorn/">
|
||||
<img src="https://badge.fury.io/py/uvicorn.svg" alt="Package version">
|
||||
@ -16,53 +23,58 @@
|
||||
<a href="https://pypi.org/project/uvicorn" target="_blank">
|
||||
<img src="https://img.shields.io/pypi/pyversions/uvicorn.svg?color=%2334D058" alt="Supported Python versions">
|
||||
</a>
|
||||
<a href="https://discord.gg/RxKUF5JuHs">
|
||||
<img src="https://img.shields.io/discord/1051468649518616576?logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2" alt="Discord">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
# Introduction
|
||||
**Documentation**: [https://uvicorn.dev](https://uvicorn.dev)
|
||||
|
||||
Uvicorn is an ASGI web server implementation for Python.
|
||||
**Source Code**: [https://www.github.com/Kludex/uvicorn](https://www.github.com/Kludex/uvicorn)
|
||||
|
||||
---
|
||||
|
||||
**Uvicorn** is an [ASGI](concepts/asgi.md) web server implementation for Python.
|
||||
|
||||
Until recently Python has lacked a minimal low-level server/application interface for
|
||||
async frameworks. The [ASGI specification][asgi] fills this gap, and means we're now able to
|
||||
start building a common set of tooling usable across all async frameworks.
|
||||
async frameworks. The [ASGI specification](https://asgi.readthedocs.io/en/latest/) fills this gap,
|
||||
and means we're now able to start building a common set of tooling usable across all async frameworks.
|
||||
|
||||
Uvicorn currently supports **HTTP/1.1** and **WebSockets**.
|
||||
|
||||
## Sponsorship
|
||||
|
||||
Help us keep Uvicorn maintained and sustainable by [becoming a sponsor](https://github.com/sponsors/Kludex).
|
||||
|
||||
**Current sponsors:**
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 2rem; align-items: center; margin: 1rem 0;">
|
||||
<a href="https://fastapi.tiangolo.com">
|
||||
<img src="img/fastapi-logo.png" alt="FastAPI" style="height: 80px;">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Quickstart
|
||||
|
||||
Install using `pip`:
|
||||
**Uvicorn** is available on [PyPI](https://pypi.org/project/uvicorn/) so installation is as simple as:
|
||||
|
||||
```shell
|
||||
$ pip install uvicorn
|
||||
```
|
||||
=== "pip"
|
||||
```bash
|
||||
pip install uvicorn
|
||||
```
|
||||
|
||||
This will install uvicorn with minimal (pure Python) dependencies.
|
||||
=== "uv"
|
||||
```bash
|
||||
uv add uvicorn
|
||||
```
|
||||
|
||||
```shell
|
||||
$ pip install 'uvicorn[standard]'
|
||||
```
|
||||
See the [installation documentation](installation.md) for more information.
|
||||
|
||||
This will install uvicorn with "Cython-based" dependencies (where possible) and other "optional extras".
|
||||
---
|
||||
|
||||
In this context, "Cython-based" means the following:
|
||||
|
||||
- the event loop `uvloop` will be installed and used if possible.
|
||||
- `uvloop` is a fast, drop-in replacement of the built-in asyncio event loop. It is implemented in Cython. Read more [here][uvloop_docs].
|
||||
- The built-in asyncio event loop serves as an easy-to-read reference implementation and is there for easy debugging as it's pure-python based.
|
||||
- the http protocol will be handled by `httptools` if possible.
|
||||
- Read more about comparison with `h11` [here][httptools_vs_h11].
|
||||
|
||||
Moreover, "optional extras" means that:
|
||||
|
||||
- the websocket protocol will be handled by `websockets` (should you want to use `wsproto` you'd need to install it manually) if possible.
|
||||
- the `--reload` flag in development mode will use `watchfiles`.
|
||||
- windows users will have `colorama` installed for the colored logs.
|
||||
- `python-dotenv` will be installed should you want to use the `--env-file` option.
|
||||
- `PyYAML` will be installed to allow you to provide a `.yaml` file to `--log-config`, if desired.
|
||||
|
||||
Create an application:
|
||||
Let's create a simple ASGI application to run with Uvicorn:
|
||||
|
||||
```python title="main.py"
|
||||
async def app(scope, receive, send):
|
||||
@ -72,7 +84,8 @@ async def app(scope, receive, send):
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
[b'content-type', b'text/plain'],
|
||||
(b'content-type', b'text/plain'),
|
||||
(b'content-length', b'13'),
|
||||
],
|
||||
})
|
||||
await send({
|
||||
@ -81,10 +94,10 @@ async def app(scope, receive, send):
|
||||
})
|
||||
```
|
||||
|
||||
Run the server:
|
||||
Then we can run it with Uvicorn:
|
||||
|
||||
```shell
|
||||
$ uvicorn main:app
|
||||
uvicorn main:app
|
||||
```
|
||||
|
||||
---
|
||||
@ -95,115 +108,8 @@ The uvicorn command line tool is the easiest way to run your application.
|
||||
|
||||
### Command line options
|
||||
|
||||
<!-- :cli_usage: -->
|
||||
```
|
||||
$ uvicorn --help
|
||||
Usage: uvicorn [OPTIONS] APP
|
||||
|
||||
Options:
|
||||
--host TEXT Bind socket to this host. [default:
|
||||
127.0.0.1]
|
||||
--port INTEGER Bind socket to this port. If 0, an available
|
||||
port will be picked. [default: 8000]
|
||||
--uds TEXT Bind to a UNIX domain socket.
|
||||
--fd INTEGER Bind to socket from this file descriptor.
|
||||
--reload Enable auto-reload.
|
||||
--reload-dir PATH Set reload directories explicitly, instead
|
||||
of using the current working directory.
|
||||
--reload-include TEXT Set glob patterns to include while watching
|
||||
for files. Includes '*.py' by default; these
|
||||
defaults can be overridden with `--reload-
|
||||
exclude`. This option has no effect unless
|
||||
watchfiles is installed.
|
||||
--reload-exclude TEXT Set glob patterns to exclude while watching
|
||||
for files. Includes '.*, .py[cod], .sw.*,
|
||||
~*' by default; these defaults can be
|
||||
overridden with `--reload-include`. This
|
||||
option has no effect unless watchfiles is
|
||||
installed.
|
||||
--reload-delay FLOAT Delay between previous and next check if
|
||||
application needs to be. Defaults to 0.25s.
|
||||
[default: 0.25]
|
||||
--workers INTEGER Number of worker processes. Defaults to the
|
||||
$WEB_CONCURRENCY environment variable if
|
||||
available, or 1. Not valid with --reload.
|
||||
--loop [auto|asyncio|uvloop] Event loop implementation. [default: auto]
|
||||
--http [auto|h11|httptools] HTTP protocol implementation. [default:
|
||||
auto]
|
||||
--ws [auto|none|websockets|wsproto]
|
||||
WebSocket protocol implementation.
|
||||
[default: auto]
|
||||
--ws-max-size INTEGER WebSocket max size message in bytes
|
||||
[default: 16777216]
|
||||
--ws-max-queue INTEGER The maximum length of the WebSocket message
|
||||
queue. [default: 32]
|
||||
--ws-ping-interval FLOAT WebSocket ping interval in seconds.
|
||||
[default: 20.0]
|
||||
--ws-ping-timeout FLOAT WebSocket ping timeout in seconds.
|
||||
[default: 20.0]
|
||||
--ws-per-message-deflate BOOLEAN
|
||||
WebSocket per-message-deflate compression
|
||||
[default: True]
|
||||
--lifespan [auto|on|off] Lifespan implementation. [default: auto]
|
||||
--interface [auto|asgi3|asgi2|wsgi]
|
||||
Select ASGI3, ASGI2, or WSGI as the
|
||||
application interface. [default: auto]
|
||||
--env-file PATH Environment configuration file.
|
||||
--log-config PATH Logging configuration file. Supported
|
||||
formats: .ini, .json, .yaml.
|
||||
--log-level [critical|error|warning|info|debug|trace]
|
||||
Log level. [default: info]
|
||||
--access-log / --no-access-log Enable/Disable access log.
|
||||
--use-colors / --no-use-colors Enable/Disable colorized logging.
|
||||
--proxy-headers / --no-proxy-headers
|
||||
Enable/Disable X-Forwarded-Proto,
|
||||
X-Forwarded-For, X-Forwarded-Port to
|
||||
populate remote address info.
|
||||
--server-header / --no-server-header
|
||||
Enable/Disable default Server header.
|
||||
--date-header / --no-date-header
|
||||
Enable/Disable default Date header.
|
||||
--forwarded-allow-ips TEXT Comma separated list of IPs to trust with
|
||||
proxy headers. Defaults to the
|
||||
$FORWARDED_ALLOW_IPS environment variable if
|
||||
available, or '127.0.0.1'.
|
||||
--root-path TEXT Set the ASGI 'root_path' for applications
|
||||
submounted below a given URL path.
|
||||
--limit-concurrency INTEGER Maximum number of concurrent connections or
|
||||
tasks to allow, before issuing HTTP 503
|
||||
responses.
|
||||
--backlog INTEGER Maximum number of connections to hold in
|
||||
backlog
|
||||
--limit-max-requests INTEGER Maximum number of requests to service before
|
||||
terminating the process.
|
||||
--timeout-keep-alive INTEGER Close Keep-Alive connections if no new data
|
||||
is received within this timeout. [default:
|
||||
5]
|
||||
--timeout-graceful-shutdown INTEGER
|
||||
Maximum number of seconds to wait for
|
||||
graceful shutdown.
|
||||
--ssl-keyfile TEXT SSL key file
|
||||
--ssl-certfile TEXT SSL certificate file
|
||||
--ssl-keyfile-password TEXT SSL keyfile password
|
||||
--ssl-version INTEGER SSL version to use (see stdlib ssl module's)
|
||||
[default: 17]
|
||||
--ssl-cert-reqs INTEGER Whether client certificate is required (see
|
||||
stdlib ssl module's) [default: 0]
|
||||
--ssl-ca-certs TEXT CA certificates file
|
||||
--ssl-ciphers TEXT Ciphers to use (see stdlib ssl module's)
|
||||
[default: TLSv1]
|
||||
--header TEXT Specify custom default HTTP response headers
|
||||
as a Name:Value pair
|
||||
--version Display the uvicorn version and exit.
|
||||
--app-dir TEXT Look for APP in the specified directory, by
|
||||
adding this to the PYTHONPATH. Defaults to
|
||||
the current working directory.
|
||||
--h11-max-incomplete-event-size INTEGER
|
||||
For h11, the maximum number of bytes to
|
||||
buffer of an incomplete event.
|
||||
--factory Treat APP as an application factory, i.e. a
|
||||
() -> <ASGI app> callable.
|
||||
--help Show this message and exit.
|
||||
```bash
|
||||
{{ uvicorn_help }}
|
||||
```
|
||||
|
||||
For more information, see the [settings documentation](settings.md).
|
||||
@ -262,7 +168,16 @@ if __name__ == "__main__":
|
||||
|
||||
### Running with Gunicorn
|
||||
|
||||
[Gunicorn][gunicorn] is a mature, fully featured server and process manager.
|
||||
!!! warning
|
||||
The `uvicorn.workers` module is deprecated and will be removed in a future release.
|
||||
|
||||
You should use the [`uvicorn-worker`](https://github.com/Kludex/uvicorn-worker) package instead.
|
||||
|
||||
```bash
|
||||
python -m pip install uvicorn-worker
|
||||
```
|
||||
|
||||
[Gunicorn](https://gunicorn.org/) is a mature, fully featured server and process manager.
|
||||
|
||||
Uvicorn includes a Gunicorn worker class allowing you to run ASGI applications,
|
||||
with all of Uvicorn's performance benefits, while also giving you Gunicorn's
|
||||
@ -277,9 +192,9 @@ For production deployments we recommend using gunicorn with the uvicorn worker c
|
||||
gunicorn example:app -w 4 -k uvicorn.workers.UvicornWorker
|
||||
```
|
||||
|
||||
For a [PyPy][pypy] compatible configuration use `uvicorn.workers.UvicornH11Worker`.
|
||||
For a [PyPy](https://pypy.org/) compatible configuration use `uvicorn.workers.UvicornH11Worker`.
|
||||
|
||||
For more information, see the [deployment documentation](deployment.md).
|
||||
For more information, see the [deployment documentation](deployment/index.md).
|
||||
|
||||
### Application factories
|
||||
|
||||
@ -292,290 +207,5 @@ def create_app():
|
||||
```
|
||||
|
||||
```shell
|
||||
$ uvicorn --factory main:create_app
|
||||
uvicorn --factory main:create_app
|
||||
```
|
||||
|
||||
## The ASGI interface
|
||||
|
||||
Uvicorn uses the [ASGI specification][asgi] for interacting with an application.
|
||||
|
||||
The application should expose an async callable which takes three arguments:
|
||||
|
||||
* `scope` - A dictionary containing information about the incoming connection.
|
||||
* `receive` - A channel on which to receive incoming messages from the server.
|
||||
* `send` - A channel on which to send outgoing messages to the server.
|
||||
|
||||
Two common patterns you might use are either function-based applications:
|
||||
|
||||
```python
|
||||
async def app(scope, receive, send):
|
||||
assert scope['type'] == 'http'
|
||||
...
|
||||
```
|
||||
|
||||
Or instance-based applications:
|
||||
|
||||
```python
|
||||
class App:
|
||||
async def __call__(self, scope, receive, send):
|
||||
assert scope['type'] == 'http'
|
||||
...
|
||||
|
||||
app = App()
|
||||
```
|
||||
|
||||
It's good practice for applications to raise an exception on scope types
|
||||
that they do not handle.
|
||||
|
||||
The content of the `scope` argument, and the messages expected by `receive` and `send` depend on the protocol being used.
|
||||
|
||||
The format for HTTP messages is described in the [ASGI HTTP Message format][asgi-http].
|
||||
|
||||
### HTTP Scope
|
||||
|
||||
An incoming HTTP request might have a connection `scope` like this:
|
||||
|
||||
```python
|
||||
{
|
||||
'type': 'http',
|
||||
'scheme': 'http',
|
||||
'root_path': '',
|
||||
'server': ('127.0.0.1', 8000),
|
||||
'http_version': '1.1',
|
||||
'method': 'GET',
|
||||
'path': '/',
|
||||
'headers': [
|
||||
(b'host', b'127.0.0.1:8000'),
|
||||
(b'user-agent', b'curl/7.51.0'),
|
||||
(b'accept', b'*/*')
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Messages
|
||||
|
||||
The instance coroutine communicates back to the server by sending messages to the `send` coroutine.
|
||||
|
||||
```python
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
[b'content-type', b'text/plain'],
|
||||
]
|
||||
})
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': b'Hello, world!',
|
||||
})
|
||||
```
|
||||
|
||||
### Requests & responses
|
||||
|
||||
Here's an example that displays the method and path used in the incoming request:
|
||||
|
||||
```python
|
||||
async def app(scope, receive, send):
|
||||
"""
|
||||
Echo the method and path back in an HTTP response.
|
||||
"""
|
||||
assert scope['type'] == 'http'
|
||||
|
||||
body = f'Received {scope["method"]} request to {scope["path"]}'
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
[b'content-type', b'text/plain'],
|
||||
]
|
||||
})
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': body.encode('utf-8'),
|
||||
})
|
||||
```
|
||||
|
||||
### Reading the request body
|
||||
|
||||
You can stream the request body without blocking the asyncio task pool,
|
||||
by fetching messages from the `receive` coroutine.
|
||||
|
||||
```python
|
||||
async def read_body(receive):
|
||||
"""
|
||||
Read and return the entire body from an incoming ASGI message.
|
||||
"""
|
||||
body = b''
|
||||
more_body = True
|
||||
|
||||
while more_body:
|
||||
message = await receive()
|
||||
body += message.get('body', b'')
|
||||
more_body = message.get('more_body', False)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
"""
|
||||
Echo the request body back in an HTTP response.
|
||||
"""
|
||||
body = await read_body(receive)
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
(b'content-type', b'text/plain'),
|
||||
(b'content-length', str(len(body)).encode())
|
||||
]
|
||||
})
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': body,
|
||||
})
|
||||
```
|
||||
|
||||
### Streaming responses
|
||||
|
||||
You can stream responses by sending multiple `http.response.body` messages to
|
||||
the `send` coroutine.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
"""
|
||||
Send a slowly streaming HTTP response back to the client.
|
||||
"""
|
||||
await send({
|
||||
'type': 'http.response.start',
|
||||
'status': 200,
|
||||
'headers': [
|
||||
[b'content-type', b'text/plain'],
|
||||
]
|
||||
})
|
||||
for chunk in [b'Hello', b', ', b'world!']:
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': chunk,
|
||||
'more_body': True
|
||||
})
|
||||
await asyncio.sleep(1)
|
||||
await send({
|
||||
'type': 'http.response.body',
|
||||
'body': b'',
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why ASGI?
|
||||
|
||||
Most well established Python Web frameworks started out as WSGI-based frameworks.
|
||||
|
||||
WSGI applications are a single, synchronous callable that takes a request and returns a response.
|
||||
This doesn’t allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections,
|
||||
which WSGI doesn't support well.
|
||||
|
||||
Having an async concurrency model also allows for options such as lightweight background tasks,
|
||||
and can be less of a limiting factor for endpoints that have long periods being blocked on network
|
||||
I/O such as dealing with slow HTTP requests.
|
||||
|
||||
---
|
||||
|
||||
## Alternative ASGI servers
|
||||
|
||||
A strength of the ASGI protocol is that it decouples the server implementation
|
||||
from the application framework. This allows for an ecosystem of interoperating
|
||||
webservers and application frameworks.
|
||||
|
||||
### Daphne
|
||||
|
||||
The first ASGI server implementation, originally developed to power Django Channels, is [the Daphne webserver][daphne].
|
||||
|
||||
It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets.
|
||||
|
||||
Any of the example applications given here can equally well be run using `daphne` instead.
|
||||
|
||||
```
|
||||
$ pip install daphne
|
||||
$ daphne app:App
|
||||
```
|
||||
|
||||
### Hypercorn
|
||||
|
||||
[Hypercorn][hypercorn] was initially part of the Quart web framework, before
|
||||
being separated out into a standalone ASGI server.
|
||||
|
||||
Hypercorn supports HTTP/1.1, HTTP/2, HTTP/3 and WebSockets.
|
||||
|
||||
```
|
||||
$ pip install hypercorn
|
||||
$ hypercorn app:App
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ASGI frameworks
|
||||
|
||||
You can use Uvicorn, Daphne, or Hypercorn to run any ASGI framework.
|
||||
|
||||
For small services you can also write ASGI applications directly.
|
||||
|
||||
### Starlette
|
||||
|
||||
[Starlette](https://github.com/encode/starlette) is a lightweight ASGI framework/toolkit.
|
||||
|
||||
It is ideal for building high performance asyncio services, and supports both HTTP and WebSockets.
|
||||
|
||||
### Django Channels
|
||||
|
||||
The ASGI specification was originally designed for use with [Django Channels](https://channels.readthedocs.io/en/latest/).
|
||||
|
||||
Channels is a little different to other ASGI frameworks in that it provides
|
||||
an asynchronous frontend onto a threaded-framework backend. It allows Django
|
||||
to support WebSockets, background tasks, and long-running connections,
|
||||
with application code still running in a standard threaded context.
|
||||
|
||||
### Quart
|
||||
|
||||
[Quart](https://pgjones.gitlab.io/quart/) is a Flask-like ASGI web framework.
|
||||
|
||||
### FastAPI
|
||||
|
||||
[**FastAPI**](https://github.com/tiangolo/fastapi) is an API framework based on **Starlette** and **Pydantic**, heavily inspired by previous server versions of **APIStar**.
|
||||
|
||||
You write your API function parameters with Python 3.6+ type declarations and get automatic data conversion, data validation, OpenAPI schemas (with JSON Schemas) and interactive API documentation UIs.
|
||||
|
||||
### BlackSheep
|
||||
|
||||
[BlackSheep](https://www.neoteroi.dev/blacksheep/) is a web framework based on ASGI, inspired by Flask and ASP.NET Core.
|
||||
|
||||
Its most distinctive features are built-in support for dependency injection, automatic binding of parameters by request handler's type annotations, and automatic generation of OpenAPI documentation and Swagger UI.
|
||||
|
||||
### Falcon
|
||||
|
||||
[Falcon](https://falconframework.org) is a minimalist REST and app backend framework for Python, with a focus on reliability, correctness, and performance at scale.
|
||||
|
||||
### Muffin
|
||||
|
||||
[Muffin](https://github.com/klen/muffin) is a fast, lightweight and asynchronous ASGI web-framework for Python 3.
|
||||
|
||||
### Litestar
|
||||
|
||||
[**Litestar**](https://litestar.dev) is a powerful, lightweight and flexible ASGI framework.
|
||||
|
||||
It includes everything that's needed to build modern APIs - from data serialization and validation to websockets, ORM integration, session management, authentication and more.
|
||||
|
||||
|
||||
[uvloop]: https://github.com/MagicStack/uvloop
|
||||
[httptools]: https://github.com/MagicStack/httptools
|
||||
[gunicorn]: https://gunicorn.org/
|
||||
[pypy]: https://pypy.org/
|
||||
[asgi]: https://asgi.readthedocs.io/en/latest/
|
||||
[asgi-http]: https://asgi.readthedocs.io/en/latest/specs/www.html
|
||||
[daphne]: https://github.com/django/daphne
|
||||
[hypercorn]: https://gitlab.com/pgjones/hypercorn
|
||||
[uvloop_docs]: https://uvloop.readthedocs.io/
|
||||
[httptools_vs_h11]: https://github.com/python-hyper/h11/issues/9
|
||||
|
||||
75
docs/installation.md
Normal file
75
docs/installation.md
Normal file
@ -0,0 +1,75 @@
|
||||
**Uvicorn** is available on [PyPI](https://pypi.org/project/uvicorn/) so installation is as simple as:
|
||||
|
||||
=== "pip"
|
||||
|
||||
```bash
|
||||
pip install uvicorn
|
||||
```
|
||||
|
||||
=== "uv"
|
||||
|
||||
```bash
|
||||
uv add uvicorn
|
||||
```
|
||||
|
||||
The above will install Uvicorn with the minimal set of dependencies:
|
||||
|
||||
- [`h11`](https://github.com/python-hyper/h11) — Pure Python sans-io HTTP/1.1 implementation.
|
||||
- [`click`](https://github.com/pallets/click) — Command line interface library.
|
||||
|
||||
If you are running on Python 3.10 or early versions,
|
||||
[`typing_extensions`](https://github.com/python/typing_extensions) will also be installed.
|
||||
|
||||
## Optional Dependencies
|
||||
|
||||
There are many optional dependencies that can be installed to add support for various features.
|
||||
|
||||
If you just want to install all of them at once, you can use the `standard` extra:
|
||||
|
||||
=== "pip"
|
||||
```bash
|
||||
pip install 'uvicorn[standard]'
|
||||
```
|
||||
|
||||
=== "uv"
|
||||
```bash
|
||||
uv add 'uvicorn[standard]'
|
||||
```
|
||||
|
||||
The `standard` extra installs the following dependencies:
|
||||
|
||||
- **[`uvloop`](https://github.com/MagicStack/uvloop) — Fast, drop-in replacement of the built-in asyncio event loop.**
|
||||
|
||||
When `uvloop` is installed, Uvicorn will use it by default.
|
||||
|
||||
- **[`httptools`](https://github.com/MagicStack/httptools) — Python binding for the Node.js HTTP parser.**
|
||||
|
||||
When `httptools` is installed, Uvicorn will use it by default for HTTP/1.1 parsing.
|
||||
|
||||
You can read this issue to understand how it compares with `h11`: [h11/issues/9](https://github.com/python-hyper/h11/issues/9).
|
||||
|
||||
- **[`websockets`](https://websockets.readthedocs.io/en/stable/) — WebSocket library for Python.**
|
||||
|
||||
When `websockets` is installed, Uvicorn will use it by default for WebSocket handling.
|
||||
|
||||
You can alternatively install **[`wsproto`](https://github.com/python-hyper/wsproto)** and set the `--ws`
|
||||
option to `wsproto` to use it instead.
|
||||
|
||||
- **[`watchfiles`](https://github.com/samuelcolvin/watchfiles) — Simple, modern and high performance file
|
||||
watching and code reload in python.**
|
||||
|
||||
When `watchfiles` is installed, Uvicorn will use it by default for the `--reload` option.
|
||||
|
||||
- **[`colorama`](https://github.com/tartley/colorama) — Cross-platform support for ANSI terminal
|
||||
colors.**
|
||||
|
||||
This is installed only on Windows, to provide colored logs.
|
||||
|
||||
- **[`python-dotenv`](https://github.com/theskumar/python-dotenv) — Reads key-value pairs from a `.env` file
|
||||
and adds them to the environment.**
|
||||
|
||||
This is installed to allow you to use the `--env-file` option.
|
||||
|
||||
- **[`PyYAML`](https://github.com/yaml/pyyaml) — YAML parser and emitter for Python.**
|
||||
|
||||
This is installed to allow you to provide a `.yaml` file to the `--log-config` option.
|
||||
12
docs/overrides/main.html
Normal file
12
docs/overrides/main.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block extrahead %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
// Redirect starlette.io to starlette.dev
|
||||
if (window.location.hostname === 'www.uvicorn.org' || window.location.hostname === 'uvicorn.org') {
|
||||
const newUrl = window.location.href.replace(/^https?:\/\/(www\.)?uvicorn\.org/, 'https://uvicorn.dev');
|
||||
window.location.replace(newUrl);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -1,3 +1,5 @@
|
||||
{% import "partials/nav-item.html" as item with context %}
|
||||
|
||||
<!-- Determine class according to configuration -->
|
||||
{% set class = "md-nav md-nav--primary" %}
|
||||
{% if "navigation.tabs" in features %}
|
||||
@ -35,18 +37,22 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Render item list -->
|
||||
<!-- Navigation list -->
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
{% for nav_item in nav %}
|
||||
{% set path = "__nav_" ~ loop.index %}
|
||||
{% set level = 1 %}
|
||||
{% include "partials/nav-item.html" %}
|
||||
{{ item.render(nav_item, path, 1) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<ul class="md-nav__list" data-md-scrollfix style="padding-top: 15px; padding-left: 10px">
|
||||
<div>
|
||||
<a href="https://fastapi.tiangolo.com"><img src="/sponsors/fastapi.png" width=150px style=></img></a>
|
||||
</div>
|
||||
</ul>
|
||||
<!-- Sponsors -->
|
||||
<div class="md-nav__sponsors">
|
||||
<p class="md-nav__sponsors-title">Sponsors</p>
|
||||
<a href="https://fastapi.tiangolo.com" title="FastAPI" class="md-nav__sponsor">
|
||||
<img src="{{ 'img/fastapi-logo.png' | url }}" alt="FastAPI">
|
||||
</a>
|
||||
<a href="https://github.com/sponsors/Kludex" class="md-nav__sponsor-cta">
|
||||
Become a sponsor! ❤️
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
18
docs/overrides/partials/toc-item.html
Normal file
18
docs/overrides/partials/toc-item.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!-- Copied from https://github.com/squidfunk/mkdocs-material/issues/4827#issuecomment-1869812019 -->
|
||||
<li class="md-nav__item"></li>
|
||||
<a href="{{ toc_item.url }}" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
{{ toc_item.title }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<!-- Table of contents list -->
|
||||
{% if toc_item.children %}
|
||||
<nav class="md-nav" aria-label="{{ toc_item.title | striptags }}">
|
||||
<ul class="md-nav__list">
|
||||
{% for toc_item in toc_item.children %}
|
||||
{% if not page.meta.toc_depth or toc_item.level <= page.meta.toc_depth %} {% include "partials/toc-item.html" %}
|
||||
{% endif %} {% endfor %} </ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</li>
|
||||
42
docs/plugins/main.py
Normal file
42
docs/plugins/main.py
Normal file
@ -0,0 +1,42 @@
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
from functools import lru_cache
|
||||
|
||||
from mkdocs.config import Config
|
||||
from mkdocs.structure.files import Files
|
||||
from mkdocs.structure.pages import Page
|
||||
|
||||
|
||||
def on_page_content(html: str, page: Page, config: Config, files: Files) -> str:
|
||||
"""Called on each page after the markdown is converted to HTML."""
|
||||
html = add_hyperlink_to_pull_request(html, page, config, files)
|
||||
return html
|
||||
|
||||
|
||||
def add_hyperlink_to_pull_request(html: str, page: Page, config: Config, files: Files) -> str:
|
||||
"""Add hyperlink on PRs mentioned on the release notes page.
|
||||
|
||||
If we find "(#\\d+)" it will be added an hyperlink to https://github.com/Kludex/uvicorn/pull/$1.
|
||||
"""
|
||||
if not page.file.name == "release-notes":
|
||||
return html
|
||||
|
||||
return re.sub(r"\(#(\d+)\)", r"(<a href='https://github.com/Kludex/uvicorn/pull/\1'>#\1</a>)", html)
|
||||
|
||||
|
||||
def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str:
|
||||
"""Called on each file after it is read and before it is converted to HTML."""
|
||||
markdown = uvicorn_print_help(markdown, page)
|
||||
return markdown
|
||||
|
||||
|
||||
def uvicorn_print_help(markdown: str, page: Page) -> str:
|
||||
return re.sub(r"{{ *uvicorn_help *}}", get_uvicorn_help(), markdown)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_uvicorn_help():
|
||||
output = subprocess.run(["uvicorn", "--help"], capture_output=True, check=True)
|
||||
return output.stdout.decode()
|
||||
806
docs/release-notes.md
Normal file
806
docs/release-notes.md
Normal file
@ -0,0 +1,806 @@
|
||||
---
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
## 0.47.0 (May 14, 2026)
|
||||
|
||||
### Added
|
||||
|
||||
* Add `ssl_context_factory` for custom `SSLContext` configuration (#2920)
|
||||
|
||||
### Changed
|
||||
|
||||
* Eagerly import the ASGI app in the parent process (#2919)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Treat `fd=0` as a valid file descriptor with reload/workers (#2927)
|
||||
|
||||
## 0.46.0 (April 23, 2026)
|
||||
|
||||
### Added
|
||||
|
||||
* Support `ws_max_size` in `wsproto` implementation (#2915)
|
||||
* Support `ws_ping_interval` and `ws_ping_timeout` in `wsproto` implementation (#2916)
|
||||
|
||||
### Changed
|
||||
|
||||
* Use `bytearray` for incoming WebSocket message buffer in `websockets-sansio` (#2917)
|
||||
|
||||
## 0.45.0 (April 21, 2026)
|
||||
|
||||
### Added
|
||||
|
||||
* Add `--reset-contextvars` flag to isolate ASGI request context (#2912)
|
||||
* Accept `os.PathLike` for `log_config` (#2905)
|
||||
* Accept `log_level` strings case-insensitively (#2907)
|
||||
|
||||
### Changed
|
||||
|
||||
* Revert "Emit `http.disconnect` on server shutdown for streaming responses" (#2913)
|
||||
* Revert "Explicitly start ASGI run with empty context" (#2911)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Preserve forwarded client ports in proxy headers middleware (#2903)
|
||||
* Raise helpful `ImportError` when PyYAML is missing for YAML log config (#2906)
|
||||
|
||||
## 0.44.0 (April 6, 2026)
|
||||
|
||||
### Added
|
||||
|
||||
* Implement websocket keepalive pings for websockets-sansio (#2888)
|
||||
|
||||
## 0.43.0 (April 3, 2026)
|
||||
|
||||
You can quit Uvicorn now. We heard you, @pamelafox - all 47 of your Ctrl+C's (thanks for flagging it, and thanks to @tiangolo for the fix 🙏). [See the tweet](https://x.com/pamelafox/status/2039097686155227623).
|
||||
|
||||
### Changed
|
||||
|
||||
* Emit `http.disconnect` ASGI `receive()` event on server shutting down for streaming responses (#2829)
|
||||
* Use native `context` parameter for `create_task` on Python 3.11+ (#2859)
|
||||
* Drop cast in ASGI types (#2875)
|
||||
|
||||
## 0.42.0 (March 16, 2026)
|
||||
|
||||
### Changed
|
||||
|
||||
* Use `bytearray` for request body accumulation to avoid O(n^2) allocation on fragmented bodies (#2845)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Escape brackets and backslash in httptools `HEADER_RE` regex (#2824)
|
||||
* Fix multiple issues in websockets sans-io implementation (#2825)
|
||||
|
||||
## 0.41.0 (February 16, 2026)
|
||||
|
||||
### Added
|
||||
|
||||
* Add `--limit-max-requests-jitter` to stagger worker restarts (#2707)
|
||||
* Add socket path to `scope["server"]` (#2561)
|
||||
|
||||
### Changed
|
||||
|
||||
* Rename `LifespanOn.error_occured` to `error_occurred` (#2776)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Ignore permission denied errors in watchfiles reloader (#2817)
|
||||
* Ensure lifespan shutdown runs when `should_exit` is set during startup (#2812)
|
||||
* Reduce the log level of 'request limit exceeded' messages (#2788)
|
||||
|
||||
## 0.40.0 (December 21, 2025)
|
||||
|
||||
### Remove
|
||||
|
||||
* Drop support for Python 3.9 (#2772)
|
||||
|
||||
## 0.39.0 (December 21, 2025)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Send close frame on ASGI return for WebSockets (#2769)
|
||||
* Explicitly start ASGI run with empty context (#2742)
|
||||
|
||||
## 0.38.0 (October 18, 2025)
|
||||
|
||||
### Added
|
||||
|
||||
* Support Python 3.14 (#2723)
|
||||
|
||||
## 0.37.0 (September 23, 2025)
|
||||
|
||||
### Added
|
||||
|
||||
* Add `--timeout-worker-healthcheck` option (#2711)
|
||||
* Add `os.PathLike[str]` type to `ssl_ca_certs` (#2676)
|
||||
|
||||
## 0.36.1 (September 23, 2025)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Raise an exception when calling removed `Config.setup_event_loop()` (#2709)
|
||||
|
||||
## 0.36.0 (September 20, 2025)
|
||||
|
||||
### Added
|
||||
|
||||
* Support custom IOLOOPs (#2435)
|
||||
* Allow to provide importable string in `--http`, `--ws` and `--loop` (#2658)
|
||||
|
||||
## 0.35.0 (June 28, 2025)
|
||||
|
||||
### Added
|
||||
|
||||
* Add `WebSocketsSansIOProtocol` (#2540)
|
||||
|
||||
### Changed
|
||||
|
||||
* Refine help message for option `--proxy-headers` (#2653)
|
||||
|
||||
## 0.34.3 (June 1, 2025)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Don't include `cwd()` when non-empty `--reload-dirs` is passed (#2598)
|
||||
* Apply `get_client_addr` formatting to WebSocket logging (#2636)
|
||||
|
||||
## 0.34.2 (April 19, 2025)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Flush stdout buffer on Windows to trigger reload (#2604)
|
||||
|
||||
## 0.34.1 (April 13, 2025)
|
||||
|
||||
### Deprecated
|
||||
|
||||
* Deprecate `ServerState` in the main module (#2581)
|
||||
|
||||
## 0.34.0 (December 15, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
* Add `content-length` to 500 response in `wsproto` implementation (#2542)
|
||||
|
||||
### Removed
|
||||
|
||||
* Drop support for Python 3.8 (#2543)
|
||||
|
||||
## 0.33.0 (December 14, 2024)
|
||||
|
||||
### Removed
|
||||
|
||||
* Remove `WatchGod` support for `--reload` (#2536)
|
||||
|
||||
## 0.32.1 (November 20, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Drop ASGI spec version to 2.3 on HTTP scope (#2513)
|
||||
* Enable httptools lenient data on `httptools >= 0.6.3` (#2488)
|
||||
|
||||
## 0.32.0 (October 15, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
* Officially support Python 3.13 (#2482)
|
||||
* Warn when `max_request_limit` is exceeded (#2430)
|
||||
|
||||
## 0.31.1 (October 9, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
* Support WebSockets 0.13.1 (#2471)
|
||||
* Restore support for `[*]` in trusted hosts (#2480)
|
||||
* Add `PathLike[str]` type hint for `ssl_keyfile` (#2481)
|
||||
|
||||
## 0.31.0 (September 27, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
Improve `ProxyHeadersMiddleware` (#2468) and (#2231):
|
||||
|
||||
- Fix the host for requests from clients running on the proxy server itself.
|
||||
- Fallback to host that was already set for empty x-forwarded-for headers.
|
||||
- Also allow to specify IP Networks as trusted hosts. This greatly simplifies deployments
|
||||
on docker swarm/kubernetes, where the reverse proxy might have a dynamic IP.
|
||||
- This includes support for IPv6 Address/Networks.
|
||||
|
||||
## 0.30.6 (August 13, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't warn when upgrade is not WebSocket and dependencies are installed (#2360)
|
||||
|
||||
## 0.30.5 (August 2, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't close connection before receiving body on H11 (#2408)
|
||||
|
||||
## 0.30.4 (July 31, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Close connection when `h11` sets client state to `MUST_CLOSE` (#2375)
|
||||
|
||||
## 0.30.3 (July 20, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Suppress `KeyboardInterrupt` from CLI and programmatic usage (#2384)
|
||||
- `ClientDisconnect` inherits from `OSError` instead of `IOError` (#2393)
|
||||
|
||||
## 0.30.2 (July 20, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `reason` support to [`websocket.disconnect`](https://asgi.readthedocs.io/en/latest/specs/www.html#disconnect-receive-event-ws) event (#2324)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Iterate subprocesses in-place on the process manager (#2373)
|
||||
|
||||
## 0.30.1 (June 2, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow horizontal tabs `\t` in response header values (#2345)
|
||||
|
||||
## 0.30.0 (May 28, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
- New multiprocess manager (#2183)
|
||||
- Allow `ConfigParser` or a `io.IO[Any]` on `log_config` (#1976)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Suppress side-effects of signal propagation (#2317)
|
||||
- Send `content-length` header on 5xx (#2304)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Deprecate the `uvicorn.workers` module (#2302)
|
||||
|
||||
## 0.29.0 (March 19, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
- Cooperative signal handling (#1600)
|
||||
|
||||
## 0.28.1 (March 19, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Revert raise `ClientDisconnected` on HTTP (#2276)
|
||||
|
||||
## 0.28.0 (March 9, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
- Raise `ClientDisconnected` on `send()` when client disconnected (#2220)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Except `AttributeError` on `sys.stdin.fileno()` for Windows IIS10 (#1947)
|
||||
- Use `X-Forwarded-Proto` for WebSockets scheme when the proxy provides it (#2258)
|
||||
|
||||
## 0.27.1 (February 10, 2024)
|
||||
|
||||
- Fix spurious LocalProtocolError errors when processing pipelined requests (#2243)
|
||||
|
||||
## 0.27.0.post1 (January 29, 2024)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix nav overrides for newer version of Mkdocs Material (#2233)
|
||||
|
||||
## 0.27.0 (January 22, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
- Raise `ClientDisconnect(IOError)` on `send()` when client disconnected (#2218)
|
||||
- Bump ASGI WebSocket spec version to 2.4 (#2221)
|
||||
|
||||
## 0.26.0 (January 16, 2024)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update `--root-path` to include the root path prefix in the full ASGI `path` as per the ASGI spec (#2213)
|
||||
- Use `__future__.annotations` on some internal modules (#2199)
|
||||
|
||||
## 0.25.0 (December 17, 2023)
|
||||
|
||||
### Added
|
||||
|
||||
- Support the WebSocket Denial Response ASGI extension (#1916)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow explicit hidden file paths on `--reload-include` (#2176)
|
||||
- Properly annotate `uvicorn.run()` (#2158)
|
||||
|
||||
## 0.24.0.post1 (November 6, 2023)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Revert mkdocs-material from 9.1.21 to 9.2.6 (#2148)
|
||||
|
||||
## 0.24.0 (November 4, 2023)
|
||||
|
||||
### Added
|
||||
|
||||
- Support Python 3.12 (#2145)
|
||||
- Allow setting `app` via environment variable `UVICORN_APP` (#2106)
|
||||
|
||||
## 0.23.2 (July 31, 2023)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Maintain the same behavior of `websockets` from 10.4 on 11.0 (#2061)
|
||||
|
||||
## 0.23.1 (July 18, 2023)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add `typing_extensions` for Python 3.10 and lower (#2053)
|
||||
|
||||
## 0.23.0 (July 10, 2023)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--ws-max-queue` parameter WebSockets (#2033)
|
||||
|
||||
### Removed
|
||||
|
||||
- Drop support for Python 3.7 (#1996)
|
||||
- Remove `asgiref` as typing dependency (#1999)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Set `scope["scheme"]` to `ws` or `wss` instead of `http` or `https` on `ProxyHeadersMiddleware` for WebSockets (#2043)
|
||||
|
||||
### Changed
|
||||
|
||||
- Raise `ImportError` on circular import (#2040)
|
||||
- Use `logger.getEffectiveLevel()` instead of `logger.level` to check if log level is `TRACE` (#1966)
|
||||
|
||||
## 0.22.0 (April 28, 2023)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--timeout-graceful-shutdown` parameter (#1950)
|
||||
- Handle `SIGBREAK` on Windows (#1909)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Shutdown event is now being triggered on Windows when using hot reload (#1584)
|
||||
- `--reload-delay` is effectively used on the `watchfiles` reloader (#1930)
|
||||
|
||||
## 0.21.1 (March 16, 2023)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reset lifespan state on each request (#1903)
|
||||
|
||||
## 0.21.0 (March 9, 2023)
|
||||
|
||||
### Added
|
||||
|
||||
- Introduce lifespan state (#1818)
|
||||
- Allow headers to be sent as iterables on H11 implementation (#1782)
|
||||
- Improve discoverability when --port=0 is used (#1890)
|
||||
|
||||
### Changed
|
||||
|
||||
- Avoid importing `h11` and `pyyaml` when not needed to improve import time (#1846)
|
||||
- Replace current native `WSGIMiddleware` implementation by `a2wsgi` (#1825)
|
||||
- Change default `--app-dir` from "." (dot) to "" (empty string) (#1835)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Send code 1012 on shutdown for WebSockets (#1816)
|
||||
- Use `surrogateescape` to encode headers on `websockets` implementation (#1005)
|
||||
- Fix warning message on reload failure (#1784)
|
||||
|
||||
## 0.20.0 (November 20, 2022)
|
||||
|
||||
### Added
|
||||
|
||||
- Check if handshake is completed before sending frame on `wsproto` shutdown (#1737)
|
||||
- Add default headers to WebSockets implementations (#1606 & #1747)
|
||||
- Warn user when `reload` and `workers` flag are used together (#1731)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use correct `WebSocket` error codes on `close` (#1753)
|
||||
- Send disconnect event on connection lost for `wsproto` (#996)
|
||||
- Add `SIGQUIT` handler to `UvicornWorker` (#1710)
|
||||
- Fix crash on exist with "--uds" if socket doesn't exist (#1725)
|
||||
- Annotate `CONFIG_KWARGS` in `UvicornWorker` class (#1746)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove conditional on `RemoteProtocolError.event_hint` on `wsproto` (#1486)
|
||||
- Remove unused `handle_no_connect` on `wsproto` implementation (#1759)
|
||||
|
||||
## 0.19.0 (October 19, 2022)
|
||||
|
||||
### Added
|
||||
|
||||
- Support Python 3.11 (#1652)
|
||||
- Bump minimal `httptools` version to `0.5.0` (#1645)
|
||||
- Ignore HTTP/2 upgrade and optionally ignore WebSocket upgrade (#1661)
|
||||
- Add `py.typed` to comply with PEP 561 (#1687)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Set `propagate` to `False` on "uvicorn" logger (#1288)
|
||||
- USR1 signal is now handled correctly on `UvicornWorker`. (#1565)
|
||||
- Use path with query string on `WebSockets` logs (#1385)
|
||||
- Fix behavior on which "Date" headers were not updated on the same connection (#1706)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the `--debug` flag (#1640)
|
||||
- Remove the `DebugMiddleware` (#1697)
|
||||
|
||||
## 0.18.3 (August 24, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove cyclic references on HTTP implementations. (#1604)
|
||||
|
||||
### Changed
|
||||
|
||||
- `reload_delay` default changed from `None` to `0.25` on `uvicorn.run()` and `Config`. `None` is not an acceptable value anymore. (#1545)
|
||||
|
||||
## 0.18.2 (June 27, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add default `log_config` on `uvicorn.run()` (#1541)
|
||||
- Revert `logging` file name modification (#1543)
|
||||
|
||||
## 0.18.1 (June 23, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Use `DEFAULT_MAX_INCOMPLETE_EVENT_SIZE` as default to `h11_max_incomplete_event_size` on the CLI (#1534)
|
||||
|
||||
## 0.18.0 (June 23, 2022)
|
||||
|
||||
### Added
|
||||
|
||||
- The `reload` flag prioritizes `watchfiles` instead of the deprecated `watchgod` (#1437)
|
||||
- Annotate `uvicorn.run()` function (#1423)
|
||||
- Allow configuring `max_incomplete_event_size` for `h11` implementation (#1514)
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove `asgiref` dependency (#1532)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Turn `raw_path` into bytes on both websockets implementations (#1487)
|
||||
- Revert log exception traceback in case of invalid HTTP request (#1518)
|
||||
- Set `asyncio.WindowsSelectorEventLoopPolicy()` when using multiple workers to avoid "WinError 87" (#1454)
|
||||
|
||||
## 0.17.6 (March 11, 2022)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change `httptools` range to `>=0.4.0` (#1400)
|
||||
|
||||
## 0.17.5 (February 16, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix case where url is fragmented in httptools protocol (#1263)
|
||||
- Fix WSGI middleware not to explode quadratically in the case of a larger body (#1329)
|
||||
|
||||
### Changed
|
||||
|
||||
- Send HTTP 400 response for invalid request (#1352)
|
||||
|
||||
## 0.17.4 (February 4, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Replace `create_server` by `create_unix_server` (#1362)
|
||||
|
||||
## 0.17.3 (February 3, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Drop wsproto version checking. (#1359)
|
||||
|
||||
## 0.17.2 (February 3, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Revert #1332. While trying to solve the memory leak, it introduced an issue (#1345) when the server receives big chunks of data using the `httptools` implementation. (#1354)
|
||||
- Revert stream interface changes. This was introduced on 0.14.0, and caused an issue (#1226), which caused a memory leak when sending TCP pings. (#1355)
|
||||
- Fix wsproto version check expression (#1342)
|
||||
|
||||
## 0.17.1 (January 28, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Move all data handling logic to protocol and ensure connection is closed. (#1332)
|
||||
- Change `spec_version` field from "2.1" to "2.3", as Uvicorn is compliant with that version of the ASGI specifications. (#1337)
|
||||
|
||||
## 0.17.0.post1 (January 24, 2022)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Add the `python_requires` version specifier (#1328)
|
||||
|
||||
## 0.17.0 (January 14, 2022)
|
||||
|
||||
### Added
|
||||
|
||||
- Allow configurable websocket per-message-deflate setting (#1300)
|
||||
- Support extra_headers for WS accept message (#1293)
|
||||
- Add missing http version on websockets scope (#1309)
|
||||
|
||||
### Fixed/Removed
|
||||
|
||||
- Drop Python 3.6 support (#1261)
|
||||
- Fix reload process behavior when exception is raised (#1313)
|
||||
- Remove `root_path` from logs (#1294)
|
||||
|
||||
## 0.16.0 (December 8, 2021)
|
||||
|
||||
### Added
|
||||
|
||||
- Enable read of uvicorn settings from environment variables (#1279)
|
||||
- Bump `websockets` to 10.0. (#1180)
|
||||
- Ensure non-zero exit code when startup fails (#1278)
|
||||
- Increase `httptools` version range from "==0.2.*" to ">=0.2.0,<0.4.0". (#1243)
|
||||
- Override default asyncio event loop with reload only on Windows (#1257)
|
||||
- Replace `HttpToolsProtocol.pipeline` type from `list` to `deque`. (#1213)
|
||||
- Replace `WSGIResponder.send_queue` type from `list` to `deque`. (#1214)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Main process exit after startup failure on reloader classes (#1177)
|
||||
- Fix the need of `httptools` on minimal installation (#1135)
|
||||
- Fix ping parameters annotation in Config class (#1127)
|
||||
|
||||
## 0.15.0 (August 13, 2021)
|
||||
|
||||
### Added
|
||||
|
||||
- Change reload to be configurable with glob patterns. Currently only `.py` files are watched, which is different from the previous default behavior. (#820)
|
||||
- Add Python 3.10-rc.1 support. Now the server uses `asyncio.run` which will: start a fresh asyncio event loop, on shutdown cancel any background tasks rather than aborting them, `aexit` any remaining async generators, and shutdown the default `ThreadPoolExecutor`. (#1070)
|
||||
- Exit with status 3 when worker starts failed (#1077)
|
||||
- Add option to set websocket ping interval and timeout (#1048)
|
||||
- Adapt bind_socket to make it usable with multiple processes (#1009)
|
||||
- Add existence check to the reload directory(ies) (#1089)
|
||||
- Add missing trace log for websocket protocols (#1083)
|
||||
- Support disabling default Server and Date headers (#818)
|
||||
|
||||
### Changed
|
||||
|
||||
- Add PEP440 compliant version of click (#1099)
|
||||
- Bump asgiref to 3.4.0 (#1100)
|
||||
|
||||
### Fixed
|
||||
|
||||
- When receiving a `SIGTERM` supervisors now terminate their processes before joining them (#1069)
|
||||
- Fix `httptools` range to `>=0.4.0` (#1400)
|
||||
|
||||
## 0.14.0 (June 1, 2021)
|
||||
|
||||
### Added
|
||||
|
||||
- Defaults ws max_size on server to 16MB (#995)
|
||||
- Improve user feedback if no ws library installed (#926 and #1023)
|
||||
- Support 'reason' field in 'websocket.close' messages (#957)
|
||||
- Implemented lifespan.shutdown.failed (#755)
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded websockets requirements (#1065)
|
||||
- Switch to asyncio streams API (#869)
|
||||
- Update httptools from 0.1.* to 0.2.* (#1024)
|
||||
- Allow Click 8.0, refs #1016 (#1042)
|
||||
- Add search for a trusted host in ProxyHeadersMiddleware (#591)
|
||||
- Up wsproto to 1.0.0 (#892)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Force reload_dirs to be a list (#978)
|
||||
- Fix gunicorn worker not running if extras not installed (#901)
|
||||
- Fix socket port 0 (#975)
|
||||
- Prevent garbage collection of main lifespan task (#972)
|
||||
|
||||
## 0.13.4 (February 20, 2021)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed wsgi middleware PATH_INFO encoding (#962)
|
||||
- Fixed uvloop dependency (#952) then (#959)
|
||||
- Relax watchgod up bound (#946)
|
||||
- Return 'connection: close' header in response (#721)
|
||||
|
||||
### Added
|
||||
|
||||
- Docs: Nginx + websockets (#948)
|
||||
- Document the default value of 1 for workers (#940) (#943)
|
||||
- Enabled permessage-deflate extension in websockets (#764)
|
||||
|
||||
## 0.13.3 (December 29, 2020)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent swallowing of return codes from `subprocess` when running with Gunicorn by properly resetting signals. (#895)
|
||||
- Tweak detection of app factories to be more robust. A warning is now logged when passing a factory without the `--factory` flag. (#914)
|
||||
- Properly clean tasks when handshake is aborted when running with `--ws websockets`. (#921)
|
||||
|
||||
## 0.13.2 (December 12, 2020)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Log full exception traceback in case of invalid HTTP request. (#886 and #888)
|
||||
|
||||
## 0.13.1 (December 12, 2020)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Prevent exceptions when the ASGI application rejects a connection during the WebSocket handshake, when running on both `--ws wsproto` or `--ws websockets`. (#704 and #881)
|
||||
- Ensure connection `scope` doesn't leak in logs when using JSON log formatters. (#859 and #884)
|
||||
|
||||
## 0.13.0 (December 8, 2020)
|
||||
|
||||
### Added
|
||||
|
||||
- Add `--factory` flag to support factory-style application imports. (#875)
|
||||
- Skip installation of signal handlers when not in the main thread. Allows using `Server` in multithreaded contexts without having to override `.install_signal_handlers()`. (#871)
|
||||
|
||||
## 0.12.3 (November 21, 2020)
|
||||
|
||||
### Fixed
|
||||
- Fix race condition that leads Quart to hang with uvicorn (#848)
|
||||
- Use latin1 when decoding X-Forwarded-* headers (#701)
|
||||
- Rework IPv6 support (#837)
|
||||
- Cancel old keepalive-trigger before setting new one. (#832)
|
||||
|
||||
## 0.12.2 (October 19, 2020)
|
||||
|
||||
### Added
|
||||
- Adding ability to decrypt ssl key file (#808)
|
||||
- Support .yml log config files (#799)
|
||||
- Added python 3.9 support (#804)
|
||||
|
||||
### Fixed
|
||||
- Fixes watchgod with common prefixes (#817)
|
||||
- Fix reload with ipv6 host (#803)
|
||||
- Added cli support for headers containing colon (#813)
|
||||
- Sharing socket across workers on windows (#802)
|
||||
- Note the need to configure trusted "ips" when using unix sockets (#796)
|
||||
|
||||
## 0.12.1 (September 30, 2020)
|
||||
|
||||
### Changed
|
||||
- Pinning h11 and python-dotenv to min versions (#789)
|
||||
- Get docs/index.md in sync with README.md (#784)
|
||||
|
||||
### Fixed
|
||||
- Improve changelog by pointing out breaking changes (#792)
|
||||
|
||||
## 0.12.0 (September 28, 2020)
|
||||
|
||||
### Added
|
||||
- Make reload delay configurable (#774)
|
||||
- Upgrade maximum h11 dependency version to 0.10 (#772)
|
||||
- Allow .json or .yaml --log-config files (#665)
|
||||
- Add ASGI dict to the lifespan scope (#754)
|
||||
- Upgrade wsproto to 0.15.0 (#750)
|
||||
- Use optional package installs (#666)
|
||||
|
||||
### Changed
|
||||
- Don't set log level for root logger (#767) 8/28/20 df81b168
|
||||
- Uvicorn no longer ships extra dependencies `uvloop`, `websockets` and `httptools` as default.
|
||||
To install these dependencies use `uvicorn[standard]`.
|
||||
|
||||
### Fixed
|
||||
- Revert "Improve shutdown robustness when using `--reload` or multiprocessing (#620)" (#756)
|
||||
- Fix terminate error in windows (#744)
|
||||
- Fix bug where --log-config disables uvicorn loggers (#512)
|
||||
|
||||
## 0.11.8 (July 30, 2020)
|
||||
|
||||
* Fix a regression that caused Uvicorn to crash when using `--interface=wsgi`. (#730)
|
||||
* Fix a regression that caused Uvicorn to crash when using unix domain sockets. (#729)
|
||||
|
||||
## 0.11.7 (July 28, 2020)
|
||||
|
||||
* SECURITY FIX: Prevent sending invalid HTTP header names and values. (#725)
|
||||
* SECURITY FIX: Ensure path value is escaped before logging to the console. (#724)
|
||||
* Fix `--proxy-headers` client IP and host when using a Unix socket. (#636)
|
||||
|
||||
## 0.11.6 (July 17, 2020)
|
||||
|
||||
* Fix overriding the root logger.
|
||||
|
||||
## 0.11.5 (April 29, 2020)
|
||||
|
||||
* Revert "Watch all files, not just .py" due to unexpected side effects.
|
||||
* Revert "Pass through gunicorn timeout config." due to unexpected side effects.
|
||||
|
||||
## 0.11.4 (April 28, 2020)
|
||||
|
||||
* Use `watchgod`, if installed, for watching code changes.
|
||||
* Watch all files, not just .py.
|
||||
* Pass through gunicorn timeout config.
|
||||
|
||||
## 0.11.3 (February 17, 2020)
|
||||
|
||||
* Update dependencies.
|
||||
|
||||
## 0.11.2 (January 20, 2020)
|
||||
|
||||
* Don't open socket until after application startup.
|
||||
* Support `--backlog`.
|
||||
|
||||
## 0.11.1 (December 20, 2019)
|
||||
|
||||
* Use a more liberal `h11` dependency. Either `0.8.*` or `0.9.*``.
|
||||
|
||||
## 0.11.0 (December 20, 2019)
|
||||
|
||||
* Fix reload/multiprocessing on Windows with Python 3.8.
|
||||
* Drop IOCP support. (Required for fix above.)
|
||||
* Add `uvicorn --version` flag.
|
||||
* Add `--use-colors` and `--no-use-colors` flags.
|
||||
* Display port correctly, when auto port selection isused with `--port=0`.
|
||||
|
||||
## 0.10.8 (November 12, 2019)
|
||||
|
||||
* Fix reload/multiprocessing error.
|
||||
|
||||
## 0.10.7 (November 12, 2019)
|
||||
|
||||
* Use resource_sharer.DupSocket to resolve socket sharing on Windows.
|
||||
|
||||
## 0.10.6 (November 12, 2019)
|
||||
|
||||
* Exit if `workers` or `reload` are use without an app import string style.
|
||||
* Reorganise supervisor processes to properly hand over sockets on windows.
|
||||
|
||||
## 0.10.5 (November 12, 2019)
|
||||
|
||||
* Update uvloop dependency to 0.14+
|
||||
|
||||
## 0.10.4 (November 9, 2019)
|
||||
|
||||
* Error clearly when `workers=<NUM>` is used with app instance, instead of an app import string.
|
||||
* Switch `--reload-dir` to current working directory by default.
|
||||
|
||||
## 0.10.3 (November 1, 2019)
|
||||
|
||||
* Add ``--log-level trace`
|
||||
|
||||
## 0.10.2 (October 31, 2019)
|
||||
|
||||
* Enable --proxy-headers by default.
|
||||
|
||||
## 0.10.1 (October 31, 2019)
|
||||
|
||||
* Resolve issues with logging when using `--reload` or `--workers`.
|
||||
* Setup up root logger to capture output for all logger instances, not just `uvicorn.error` and `uvicorn.access`.
|
||||
|
||||
## 0.10.0 (October 29, 2019)
|
||||
|
||||
* Support for Python 3.8
|
||||
* Separated out `uvicorn.error` and `uvicorn.access` logs.
|
||||
* Coloured log output when connected to a terminal.
|
||||
* Dropped `logger=` config setting.
|
||||
* Added `--log-config [FILE]` and `log_config=[str|dict]`. May either be a Python logging config dictionary or the file name of a logging configuration.
|
||||
* Added `--forwarded_allow_ips` and `forwarded_allow_ips`. Defaults to the value of the `$FORWARDED_ALLOW_IPS` environment variable or "127.0.0.1". The `--proxy-headers` flag now defaults to `True`, but only trusted IPs are used to populate forwarding info.
|
||||
* The `--workers` setting now defaults to the value of the `$WEB_CONCURRENCY` environment variable.
|
||||
* Added support for `--env-file`. Requires `python-dotenv`.
|
||||
@ -83,6 +83,9 @@ Server errors will be logged at the `error` log level. All logging defaults to b
|
||||
|
||||
If an exception is raised by an ASGI application, and a response has not yet been sent on the connection, then a `500 Server Error` HTTP response will be sent.
|
||||
|
||||
Uvicorn sends the headers and the status code as soon as it receives from the ASGI application. This means that if the application sends a [Response Start](https://asgi.readthedocs.io/en/latest/specs/www.html#response-start-send-event)
|
||||
message with a status code of `200 OK`, and then an exception is raised, the response will still be sent with a status code of `200 OK`.
|
||||
|
||||
### Invalid responses
|
||||
|
||||
Uvicorn will ensure that ASGI applications send the correct sequence of messages, and will raise errors otherwise. This includes checking for no response sent, partial response sent, or invalid message sequences being sent.
|
||||
|
||||
@ -2,35 +2,57 @@
|
||||
|
||||
Use the following options to configure Uvicorn, when running from the command line.
|
||||
|
||||
If you're running programmatically, using `uvicorn.run(...)`, then use
|
||||
equivalent keyword arguments, eg. `uvicorn.run("example:app", port=5000, reload=True, access_log=False)`.
|
||||
Please note that in this case, if you use `reload=True` or `workers=NUM`,
|
||||
you should put `uvicorn.run` into `if __name__ == '__main__'` clause in the main module.
|
||||
## Configuration Methods
|
||||
|
||||
You can also configure Uvicorn using environment variables with the prefix `UVICORN_`.
|
||||
For example, in case you want to run the app on port `5000`, just set the environment variable `UVICORN_PORT` to `5000`.
|
||||
There are three ways to configure Uvicorn:
|
||||
|
||||
!!! note
|
||||
CLI options and the arguments for `uvicorn.run()` take precedence over environment variables.
|
||||
1. **Command Line**: Use command line options when running Uvicorn directly.
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
Also note that `UVICORN_*` prefixed settings cannot be used from within an environment configuration file. Using an environment configuration file with the `--env-file` flag is intended for configuring the ASGI application that uvicorn runs, rather than configuring uvicorn itself.
|
||||
2. **Programmatic**: Use keyword arguments when running programmatically with `uvicorn.run()`.
|
||||
```python
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
!!! note
|
||||
When using `reload=True` or `workers=NUM`, you should put `uvicorn.run` into
|
||||
an `if __name__ == '__main__'` clause in the main module.
|
||||
|
||||
3. **Environment Variables**: Use environment variables with the prefix `UVICORN_`.
|
||||
```bash
|
||||
export UVICORN_HOST="0.0.0.0"
|
||||
export UVICORN_PORT="8000"
|
||||
uvicorn main:app
|
||||
```
|
||||
|
||||
CLI options and the arguments for `uvicorn.run()` take precedence over environment variables.
|
||||
|
||||
Also note that `UVICORN_*` prefixed settings cannot be used from within an environment
|
||||
configuration file. Using an environment configuration file with the `--env-file` flag is
|
||||
intended for configuring the ASGI application that uvicorn runs, rather than configuring
|
||||
uvicorn itself.
|
||||
|
||||
## Application
|
||||
|
||||
* `APP` - The ASGI application to run, in the format `"<module>:<attribute>"`.
|
||||
* `--factory` - Treat `APP` as an application factory, i.e. a `() -> <ASGI app>` callable.
|
||||
* `--app-dir <path>` - Look for APP in the specified directory by adding it to the PYTHONPATH. **Default:** *Current working directory*.
|
||||
* `--reset-contextvars` - Run each ASGI request in a fresh `contextvars.Context`. Workaround for a [context leak in asyncio](https://github.com/python/cpython/issues/140947); only relevant when using the `asyncio` event loop (uvloop is not affected). Enabling this hides any context set in the lifespan or by external instrumentation from ASGI handlers. **Default:** *False*.
|
||||
|
||||
## Socket Binding
|
||||
|
||||
* `--host <str>` - Bind socket to this host. Use `--host 0.0.0.0` to make the application available on your local network. IPv6 addresses are supported, for example: `--host '::'`. **Default:** *'127.0.0.1'*.
|
||||
* `--port <int>` - Bind to a socket with this port. **Default:** *8000*.
|
||||
* `--port <int>` - Bind to a socket with this port. If set to 0, an available port will be picked. **Default:** *8000*.
|
||||
* `--uds <path>` - Bind to a UNIX domain socket, for example `--uds /tmp/uvicorn.sock`. Useful if you want to run Uvicorn behind a reverse proxy.
|
||||
* `--fd <int>` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager.
|
||||
|
||||
## Development
|
||||
|
||||
* `--reload` - Enable auto-reload. Uvicorn supports two versions of auto-reloading behavior enabled by this option. There are important differences between them.
|
||||
* `--reload` - Enable auto-reload. Uvicorn supports two versions of auto-reloading behavior enabled by this option. **Default:** *False*.
|
||||
* `--reload-dir <path>` - Specify which directories to watch for python file changes. May be used multiple times. If unused, then by default the whole current directory will be watched. If you are running programmatically use `reload_dirs=[]` and pass a list of strings.
|
||||
* `--reload-delay <float>` - Delay between previous and next check if application needs to be reloaded. **Default:** *0.25*.
|
||||
|
||||
### Reloading without watchfiles
|
||||
|
||||
@ -40,7 +62,7 @@ If Uvicorn _cannot_ load [watchfiles](https://pypi.org/project/watchfiles/) at r
|
||||
|
||||
For more nuanced control over which file modifications trigger reloads, install `uvicorn[standard]`, which includes watchfiles as a dependency. Alternatively, install [watchfiles](https://pypi.org/project/watchfiles/) where Uvicorn can see it.
|
||||
|
||||
Using Uvicorn with watchfiles will enable the following options (which are otherwise ignored).
|
||||
Using Uvicorn with watchfiles will enable the following options (which are otherwise ignored):
|
||||
|
||||
* `--reload-include <glob-pattern>` - Specify a glob pattern to match files or directories which will be watched. May be used multiple times. By default the following patterns are included: `*.py`. These defaults can be overwritten by including them in `--reload-exclude`.
|
||||
* `--reload-exclude <glob-pattern>` - Specify a glob pattern to match files or directories which will excluded from watching. May be used multiple times. By default the following patterns are excluded: `.*, .py[cod], .sw.*, ~*`. These defaults can be overwritten by including them in `--reload-include`.
|
||||
@ -52,7 +74,12 @@ Using Uvicorn with watchfiles will enable the following options (which are other
|
||||
|
||||
## Production
|
||||
|
||||
* `--workers <int>` - Use multiple worker processes. Defaults to the `$WEB_CONCURRENCY` environment variable if available, or 1.
|
||||
* `--workers <int>` - Number of worker processes. Defaults to the `$WEB_CONCURRENCY` environment variable if available, or 1. Not valid with `--reload`.
|
||||
* `--env-file <path>` - Environment configuration file for the ASGI application. **Default:** *None*.
|
||||
* `--timeout-worker-healthcheck <int>` - Maximum number of seconds to wait for a worker to respond to a healthcheck. **Default:** *5*.
|
||||
|
||||
!!! note
|
||||
The `--reload` and `--workers` arguments are mutually exclusive. You cannot use both at the same time.
|
||||
|
||||
## Logging
|
||||
|
||||
@ -60,24 +87,24 @@ Using Uvicorn with watchfiles will enable the following options (which are other
|
||||
* If you wish to use a YAML file for your logging config, you will need to include PyYAML as a dependency for your project or install uvicorn with the `[standard]` optional extras.
|
||||
* `--log-level <str>` - Set the log level. **Options:** *'critical', 'error', 'warning', 'info', 'debug', 'trace'.* **Default:** *'info'*.
|
||||
* `--no-access-log` - Disable access log only, without changing log level.
|
||||
* `--use-colors / --no-use-colors` - Enable / disable colorized formatting of the log records, in case this is not set it will be auto-detected. This option is ignored if the `--log-config` CLI option is used.
|
||||
|
||||
* `--use-colors / --no-use-colors` - Enable / disable colorized formatting of the log records. If not set, colors will be auto-detected. This option is ignored if the `--log-config` CLI option is used.
|
||||
|
||||
## Implementation
|
||||
|
||||
* `--loop <str>` - Set the event loop implementation. The uvloop implementation provides greater performance, but is not compatible with Windows or PyPy. **Options:** *'auto', 'asyncio', 'uvloop'.* **Default:** *'auto'*.
|
||||
* `--http <str>` - Set the HTTP protocol implementation. The httptools implementation provides greater performance, but it not compatible with PyPy. **Options:** *'auto', 'h11', 'httptools'.* **Default:** *'auto'*.
|
||||
* `--ws <str>` - Set the WebSockets protocol implementation. Either of the `websockets` and `wsproto` packages are supported. Use `'none'` to ignore all websocket requests. **Options:** *'auto', 'none', 'websockets', 'wsproto'.* **Default:** *'auto'*.
|
||||
* `--ws-max-size <int>` - Set the WebSockets max message size, in bytes. Please note that this can be used only with the default `websockets` protocol.
|
||||
* `--ws-max-queue <int>` - Set the maximum length of the WebSocket incoming message queue. Please note that this can be used only with the default `websockets` protocol.
|
||||
* `--ws-ping-interval <float>` - Set the WebSockets ping interval, in seconds. Please note that this can be used only with the default `websockets` protocol. **Default:** *20.0*
|
||||
* `--ws-ping-timeout <float>` - Set the WebSockets ping timeout, in seconds. Please note that this can be used only with the default `websockets` protocol. **Default:** *20.0*
|
||||
* `--ws <str>` - Set the WebSockets protocol implementation. Either of the `websockets` and `wsproto` packages are supported. There are two versions of `websockets` supported: `websockets` and `websockets-sansio`. Use `'none'` to ignore all websocket requests. **Options:** *'auto', 'none', 'websockets', 'websockets-sansio', 'wsproto'.* **Default:** *'auto'*.
|
||||
* `--ws-max-size <int>` - Set the WebSockets max message size, in bytes. **Default:** *16777216* (16 MB).
|
||||
* `--ws-max-queue <int>` - Set the maximum length of the WebSocket incoming message queue. Only available with the `websockets` protocol. **Default:** *32*.
|
||||
* `--ws-ping-interval <float>` - Set the WebSockets ping interval, in seconds. **Default:** *20.0*.
|
||||
* `--ws-ping-timeout <float>` - Set the WebSockets ping timeout, in seconds. **Default:** *20.0*.
|
||||
* `--ws-per-message-deflate <bool>` - Enable/disable WebSocket per-message-deflate compression. Only available with the `websockets` protocol. **Default:** *True*.
|
||||
* `--lifespan <str>` - Set the Lifespan protocol implementation. **Options:** *'auto', 'on', 'off'.* **Default:** *'auto'*.
|
||||
* `--h11-max-incomplete-event-size <int>` - Set the maximum number of bytes to buffer of an incomplete event. Only available for `h11` HTTP protocol implementation. **Default:** *'16384'* (16 KB).
|
||||
* `--h11-max-incomplete-event-size <int>` - Set the maximum number of bytes to buffer of an incomplete event. Only available for `h11` HTTP protocol implementation. **Default:** *16384* (16 KB).
|
||||
|
||||
## Application Interface
|
||||
|
||||
* `--interface` - Select ASGI3, ASGI2, or WSGI as the application interface.
|
||||
* `--interface <str>` - Select ASGI3, ASGI2, or WSGI as the application interface.
|
||||
Note that WSGI mode always disables WebSocket support, as it is not supported by the WSGI interface.
|
||||
**Options:** *'auto', 'asgi3', 'asgi2', 'wsgi'.* **Default:** *'auto'*.
|
||||
|
||||
@ -87,12 +114,12 @@ Note that WSGI mode always disables WebSocket support, as it is not supported by
|
||||
|
||||
## HTTP
|
||||
|
||||
* `--root-path <str>` - Set the ASGI `root_path` for applications submounted below a given URL path.
|
||||
* `--proxy-headers` / `--no-proxy-headers` - Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to populate remote address info. Defaults to enabled, but is restricted to only trusting
|
||||
connecting IPs in the `forwarded-allow-ips` configuration.
|
||||
* `--forwarded-allow-ips` <comma-separated-list> Comma separated list of IPs to trust with proxy headers. Defaults to the `$FORWARDED_ALLOW_IPS` environment variable if available, or '127.0.0.1'. A wildcard '*' means always trust.
|
||||
* `--server-header` / `--no-server-header` - Enable/Disable default `Server` header.
|
||||
* `--date-header` / `--no-date-header` - Enable/Disable default `Date` header.
|
||||
* `--root-path <str>` - Set the ASGI `root_path` for applications submounted below a given URL path. **Default:** *""*.
|
||||
* `--proxy-headers / --no-proxy-headers` - Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate remote address info. Defaults to enabled, but is restricted to only trusting connecting IPs in the `forwarded-allow-ips` configuration.
|
||||
* `--forwarded-allow-ips <comma-separated-list>` - Comma separated list of IP Addresses, IP Networks, or literals (e.g. UNIX Socket path) to trust with proxy headers. Defaults to the `$FORWARDED_ALLOW_IPS` environment variable if available, or '127.0.0.1'. The literal `'*'` means trust everything.
|
||||
* `--server-header / --no-server-header` - Enable/Disable default `Server` header. **Default:** *True*.
|
||||
* `--date-header / --no-date-header` - Enable/Disable default `Date` header. **Default:** *True*.
|
||||
* `--header <name:value>` - Specify custom default HTTP response headers as a Name:Value pair. May be used multiple times.
|
||||
|
||||
!!! note
|
||||
The `--no-date-header` flag doesn't have effect on the `websockets` implementation.
|
||||
@ -104,20 +131,23 @@ The [SSL context](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) can
|
||||
* `--ssl-keyfile <path>` - The SSL key file.
|
||||
* `--ssl-keyfile-password <str>` - The password to decrypt the ssl key.
|
||||
* `--ssl-certfile <path>` - The SSL certificate file.
|
||||
* `--ssl-version <int>` - The SSL version to use.
|
||||
* `--ssl-cert-reqs <int>` - Whether client certificate is required.
|
||||
* `--ssl-version <int>` - The SSL version to use. **Default:** *ssl.PROTOCOL_TLS_SERVER*.
|
||||
* `--ssl-cert-reqs <int>` - Whether client certificate is required. **Default:** *ssl.CERT_NONE*.
|
||||
* `--ssl-ca-certs <str>` - The CA certificates file.
|
||||
* `--ssl-ciphers <str>` - The ciphers to use.
|
||||
* `--ssl-ciphers <str>` - The ciphers to use. **Default:** *"TLSv1"*.
|
||||
|
||||
To understand more about the SSL context options, please refer to the [Python documentation](https://docs.python.org/3/library/ssl.html).
|
||||
|
||||
For advanced TLS scenarios that the flags above don't cover (e.g., mutual TLS, certificate pinning, custom `SSLContext.options`), pass an `ssl_context_factory` to `uvicorn.run()` or `Config`. See [Running with HTTPS](deployment/index.md#customizing-the-ssl-context) for details.
|
||||
|
||||
## Resource Limits
|
||||
|
||||
* `--limit-concurrency <int>` - Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses. Useful for ensuring known memory usage patterns even under over-resourced loads.
|
||||
* `--limit-max-requests <int>` - Maximum number of requests to service before terminating the process. Useful when running together with a process manager, for preventing memory leaks from impacting long-running processes.
|
||||
* `--backlog <int>` - Maximum number of connections to hold in backlog. Relevant for heavy incoming traffic. **Default:** *2048*
|
||||
* `--limit-max-requests-jitter <int>` - Maximum jitter to add to `limit-max-requests`. Each worker adds a random number in the range `[0, jitter]`, staggering restarts to avoid all workers restarting simultaneously. **Default:** *0*.
|
||||
* `--backlog <int>` - Maximum number of connections to hold in backlog. Relevant for heavy incoming traffic. **Default:** *2048*.
|
||||
|
||||
## Timeouts
|
||||
|
||||
* `--timeout-keep-alive <int>` - Close Keep-Alive connections if no new data is received within this timeout. **Default:** *5*.
|
||||
* `--timeout-keep-alive <int>` - Close Keep-Alive connections if no new data is received within this timeout (in seconds). **Default:** *5*.
|
||||
* `--timeout-graceful-shutdown <int>` - Maximum number of seconds to wait for graceful shutdown. After this timeout, the server will start terminating requests.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
111
mkdocs.yml
111
mkdocs.yml
@ -1,9 +1,18 @@
|
||||
site_name: Uvicorn
|
||||
site_description: The lightning-fast ASGI server.
|
||||
site_url: https://uvicorn.dev
|
||||
|
||||
repo_name: Kludex/uvicorn
|
||||
repo_url: https://github.com/Kludex/uvicorn
|
||||
edit_uri: edit/main/docs/
|
||||
|
||||
strict: true
|
||||
|
||||
theme:
|
||||
name: "material"
|
||||
name: material
|
||||
custom_dir: docs/overrides
|
||||
logo: uvicorn.png
|
||||
favicon: uvicorn.png
|
||||
palette:
|
||||
- scheme: "default"
|
||||
media: "(prefers-color-scheme: light)"
|
||||
@ -16,28 +25,108 @@ theme:
|
||||
toggle:
|
||||
icon: "material/lightbulb-outline"
|
||||
name: "Switch to light mode"
|
||||
icon:
|
||||
repo: fontawesome/brands/github
|
||||
features:
|
||||
- content.code.copy
|
||||
- content.code.annotate
|
||||
- content.code.copy # https://squidfunk.github.io/mkdocs-material/upgrade/?h=content+copy#contentcodecopy
|
||||
- content.tabs.link
|
||||
- navigation.footer # https://squidfunk.github.io/mkdocs-material/upgrade/?h=content+copy#navigationfooter
|
||||
- navigation.path
|
||||
- navigation.sections # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation
|
||||
- navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#back-to-top-button
|
||||
- navigation.tracking
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#anchor-following
|
||||
|
||||
|
||||
repo_name: encode/uvicorn
|
||||
repo_url: https://github.com/encode/uvicorn
|
||||
edit_uri: ""
|
||||
# https://www.mkdocs.org/user-guide/configuration/#validation
|
||||
validation:
|
||||
omitted_files: warn
|
||||
absolute_links: warn
|
||||
unrecognized_links: warn
|
||||
|
||||
nav:
|
||||
- Introduction: "index.md"
|
||||
- Settings: "settings.md"
|
||||
- Deployment: "deployment.md"
|
||||
- Server Behavior: "server-behavior.md"
|
||||
- Contributing: "contributing.md"
|
||||
- Welcome: index.md
|
||||
- Installation: installation.md
|
||||
- Settings: settings.md
|
||||
- Server Behavior: server-behavior.md
|
||||
- Concepts:
|
||||
- ASGI: concepts/asgi.md
|
||||
- Lifespan: concepts/lifespan.md
|
||||
- Logging: concepts/logging.md
|
||||
- WebSockets: concepts/websockets.md
|
||||
- Event Loop: concepts/event-loop.md
|
||||
- Deployment:
|
||||
- Deployment: deployment/index.md
|
||||
- Docker: deployment/docker.md
|
||||
- Release Notes: release-notes.md
|
||||
- Contributing: contributing.md
|
||||
|
||||
extra:
|
||||
analytics:
|
||||
provider: google
|
||||
property: G-KTS6TXPD85
|
||||
social:
|
||||
- icon: fontawesome/brands/github-alt
|
||||
link: https://github.com/Kludex/uvicorn
|
||||
- icon: fontawesome/brands/discord
|
||||
link: https://discord.com/invite/RxKUF5JuHs
|
||||
- icon: fontawesome/brands/twitter
|
||||
link: https://x.com/marcelotryle
|
||||
- icon: fontawesome/brands/linkedin
|
||||
link: https://www.linkedin.com/in/marcelotryle
|
||||
- icon: fontawesome/solid/globe
|
||||
link: https://fastapiexpert.com
|
||||
|
||||
extra_css:
|
||||
- css/extra.css
|
||||
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
- admonition
|
||||
- codehilite:
|
||||
css_class: highlight
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.details
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
- pymdownx.extra:
|
||||
pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
inventories:
|
||||
- https://docs.python.org/3/objects.inv
|
||||
- llmstxt:
|
||||
full_output: llms-full.txt
|
||||
markdown_description: |-
|
||||
Uvicorn is a lightning-fast ASGI server implementation, designed to run asynchronous web applications.
|
||||
It supports the ASGI specification, which allows for both HTTP/1.1 and WebSocket protocols.
|
||||
sections:
|
||||
Sections:
|
||||
- index.md
|
||||
- settings.md
|
||||
- deployment/*.md
|
||||
- server-behavior.md
|
||||
Concepts:
|
||||
- concepts/*.md
|
||||
|
||||
hooks:
|
||||
- docs/plugins/main.py
|
||||
|
||||
109
pyproject.toml
109
pyproject.toml
@ -8,22 +8,25 @@ dynamic = ["version"]
|
||||
description = "The lightning-fast ASGI server."
|
||||
readme = "README.md"
|
||||
license = "BSD-3-Clause"
|
||||
requires-python = ">=3.8"
|
||||
license-files = ["LICENSE.md"]
|
||||
requires-python = ">=3.10"
|
||||
authors = [
|
||||
{ name = "Tom Christie", email = "tom@tomchristie.com" },
|
||||
]
|
||||
maintainers = [
|
||||
{ name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
@ -36,33 +39,73 @@ dependencies = [
|
||||
|
||||
[project.optional-dependencies]
|
||||
standard = [
|
||||
"colorama>=0.4;sys_platform == 'win32'",
|
||||
"httptools>=0.5.0",
|
||||
"colorama>=0.4; sys_platform == 'win32'",
|
||||
"httptools>=0.6.3",
|
||||
"python-dotenv>=0.13",
|
||||
"PyYAML>=5.1",
|
||||
"uvloop>=0.14.0,!=0.15.0,!=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')",
|
||||
"watchfiles>=0.13",
|
||||
"uvloop>=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')",
|
||||
"watchfiles>=0.20",
|
||||
"websockets>=10.4",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
# We add uvicorn[standard] so `uv sync` considers the extras.
|
||||
"uvicorn[standard]",
|
||||
"ruff==0.15.1",
|
||||
"pytest==9.0.3",
|
||||
"pytest-mock==3.15.1",
|
||||
"pytest-xdist[psutil]==3.8.0",
|
||||
"pytest-codspeed>=4.1.1",
|
||||
"mypy==1.19.1",
|
||||
"types-click==7.1.8",
|
||||
"types-pyyaml==6.0.12.20250915",
|
||||
"trustme==1.2.1",
|
||||
"cryptography>=44.0.3",
|
||||
"coverage==7.13.4",
|
||||
"coverage-conditional-plugin==0.9.0",
|
||||
"coverage-enable-subprocess==1.0",
|
||||
"httpx==0.28.1",
|
||||
# check dist
|
||||
"twine==6.2.0",
|
||||
# Explicit optionals,
|
||||
"a2wsgi==1.10.10",
|
||||
"wsproto==1.3.2",
|
||||
"websockets==13.1",
|
||||
]
|
||||
docs = [
|
||||
"mkdocs==1.6.1",
|
||||
"mkdocs-material==9.7.1",
|
||||
"mkdocstrings-python==2.0.2",
|
||||
"mkdocs-llmstxt==0.5.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
default-groups = ["dev", "docs"]
|
||||
required-version = ">=0.9.17"
|
||||
exclude-newer = "7 days"
|
||||
|
||||
[project.scripts]
|
||||
uvicorn = "uvicorn.main:main"
|
||||
|
||||
[project.urls]
|
||||
Changelog = "https://github.com/encode/uvicorn/blob/master/CHANGELOG.md"
|
||||
Changelog = "https://uvicorn.dev/release-notes"
|
||||
Funding = "https://github.com/sponsors/encode"
|
||||
Homepage = "https://www.uvicorn.org/"
|
||||
Source = "https://github.com/encode/uvicorn"
|
||||
Homepage = "https://uvicorn.dev/"
|
||||
Source = "https://github.com/Kludex/uvicorn"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "uvicorn/__init__.py"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["/uvicorn"]
|
||||
include = ["/uvicorn", "/tests"]
|
||||
|
||||
[tool.ruff]
|
||||
select = ["E", "F", "I"]
|
||||
ignore = ["B904", "B028"]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "FA", "UP"]
|
||||
ignore = ["B904", "B028", "UP031"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
combine-as-imports = true
|
||||
@ -71,58 +114,62 @@ combine-as-imports = true
|
||||
warn_unused_ignores = true
|
||||
warn_redundant_casts = true
|
||||
show_error_codes = true
|
||||
disallow_untyped_defs = true
|
||||
disallow_untyped_defs = false
|
||||
ignore_missing_imports = true
|
||||
follow_imports = "silent"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "tests.*"
|
||||
disallow_untyped_defs = false
|
||||
check_untyped_defs = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-rxXs --strict-config --strict-markers"
|
||||
addopts = "-rxXs --strict-config --strict-markers -n 8"
|
||||
xfail_strict = true
|
||||
filterwarnings = [
|
||||
"error",
|
||||
'ignore: \"watchgod\" is deprecated\, you should switch to watchfiles \(`pip install watchfiles`\)\.:DeprecationWarning',
|
||||
"ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
|
||||
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning"
|
||||
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
|
||||
"ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
|
||||
"ignore: websockets.legacy is deprecated.*:DeprecationWarning",
|
||||
"ignore: websockets.server.WebSocketServerProtocol is deprecated.*:DeprecationWarning",
|
||||
"ignore: websockets.client.connect is deprecated.*:DeprecationWarning",
|
||||
# httptools in Python 3.14t needs the `PYTHON_GIL=0` environment variable, or raises a RuntimeWarning.
|
||||
"ignore: The global interpreter lock (GIL)*:RuntimeWarning"
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
parallel = true
|
||||
source_pkgs = ["uvicorn", "tests"]
|
||||
plugins = ["coverage_conditional_plugin"]
|
||||
omit = [
|
||||
"uvicorn/workers.py",
|
||||
"uvicorn/__main__.py",
|
||||
]
|
||||
omit = ["uvicorn/workers.py", "uvicorn/__main__.py", "uvicorn/_compat.py", "tests/benchmarks/*"]
|
||||
|
||||
[tool.coverage.report]
|
||||
precision = 2
|
||||
fail_under = 98.35
|
||||
fail_under = 100
|
||||
show_missing = true
|
||||
skip_covered = true
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"pragma: nocover",
|
||||
"pragma: full coverage",
|
||||
"if TYPE_CHECKING:",
|
||||
"if typing.TYPE_CHECKING:",
|
||||
"raise NotImplementedError",
|
||||
]
|
||||
|
||||
[tool.coverage.coverage_conditional_plugin.omit]
|
||||
"sys_platform == 'win32'" = ["uvicorn/loops/uvloop.py"]
|
||||
"sys_platform == 'win32'" = [
|
||||
"uvicorn/loops/uvloop.py",
|
||||
"uvicorn/supervisors/multiprocess.py",
|
||||
"tests/supervisors/test_multiprocess.py",
|
||||
]
|
||||
"sys_platform != 'win32'" = ["uvicorn/loops/asyncio.py"]
|
||||
|
||||
[tool.coverage.coverage_conditional_plugin.rules]
|
||||
py-win32 = "sys_platform == 'win32'"
|
||||
py-not-win32 = "sys_platform != 'win32'"
|
||||
py-linux = "sys_platform == 'linux'"
|
||||
py-not-linux = "sys_platform != 'linux'"
|
||||
py-darwin = "sys_platform == 'darwin'"
|
||||
py-gte-38 = "sys_version_info >= (3, 8)"
|
||||
py-lt-38 = "sys_version_info < (3, 8)"
|
||||
py-gte-39 = "sys_version_info >= (3, 9)"
|
||||
py-lt-39 = "sys_version_info < (3, 9)"
|
||||
py-gte-310 = "sys_version_info >= (3, 10)"
|
||||
py-lt-310 = "sys_version_info < (3, 10)"
|
||||
py-gte-311 = "sys_version_info >= (3, 11)"
|
||||
py-lt-311 = "sys_version_info < (3, 11)"
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
-e .[standard]
|
||||
|
||||
# TODO: Remove this after h11 makes a release. By this writing, h11 was on 0.14.0.
|
||||
# Core dependencies
|
||||
h11 @ git+https://github.com/python-hyper/h11.git@master
|
||||
|
||||
# Explicit optionals
|
||||
a2wsgi==1.8.0
|
||||
wsproto==1.2.0
|
||||
websockets==12.0
|
||||
|
||||
# Packaging
|
||||
build==1.0.3
|
||||
twine==4.0.2
|
||||
|
||||
# Testing
|
||||
ruff==0.1.6
|
||||
pytest==7.4.3
|
||||
pytest-mock==3.11.1
|
||||
mypy==1.7.1
|
||||
types-click==7.1.8
|
||||
types-pyyaml==6.0.12.12
|
||||
trustme==1.1.0
|
||||
cryptography==41.0.6
|
||||
coverage==7.3.1
|
||||
coverage-conditional-plugin==0.9.0
|
||||
httpx==0.25.2
|
||||
watchgod==0.8.2
|
||||
|
||||
# Documentation
|
||||
mkdocs==1.5.2
|
||||
mkdocs-material==9.1.21
|
||||
@ -1,13 +1,7 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
if [ -d 'venv' ] ; then
|
||||
PREFIX="venv/bin/"
|
||||
else
|
||||
PREFIX=""
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
${PREFIX}python -m build
|
||||
${PREFIX}twine check dist/*
|
||||
${PREFIX}mkdocs build
|
||||
uv build
|
||||
uv run twine check dist/*
|
||||
uv run mkdocs build
|
||||
|
||||
@ -1,16 +1,10 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
export PREFIX=""
|
||||
if [ -d 'venv' ] ; then
|
||||
export PREFIX="venv/bin/"
|
||||
export PATH=${PREFIX}:${PATH}
|
||||
fi
|
||||
export SOURCE_FILES="uvicorn tests"
|
||||
|
||||
set -x
|
||||
|
||||
./scripts/sync-version
|
||||
${PREFIX}ruff format --check --diff $SOURCE_FILES
|
||||
${PREFIX}mypy $SOURCE_FILES
|
||||
${PREFIX}ruff check $SOURCE_FILES
|
||||
${PREFIX}python -m tools.cli_usage --check
|
||||
uv run ruff format --check --diff $SOURCE_FILES
|
||||
uv run mypy $SOURCE_FILES
|
||||
uv run ruff check $SOURCE_FILES
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
export PREFIX=""
|
||||
if [ -d 'venv' ]; then
|
||||
export PREFIX="venv/bin/"
|
||||
fi
|
||||
export SOURCE_FILES="uvicorn tests"
|
||||
|
||||
set -x
|
||||
|
||||
${PREFIX}coverage report
|
||||
uv run coverage combine
|
||||
uv run coverage report
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
PREFIX=""
|
||||
if [ -d "venv" ] ; then
|
||||
PREFIX="venv/bin/"
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
${PREFIX}mkdocs "$@"
|
||||
uv run mkdocs "$@"
|
||||
|
||||
@ -1,19 +1,5 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
# Use the Python executable provided from the `-p` option, or a default.
|
||||
[ "$1" = "-p" ] && PYTHON=$2 || PYTHON="python3"
|
||||
|
||||
REQUIREMENTS="requirements.txt"
|
||||
VENV="venv"
|
||||
|
||||
set -x
|
||||
|
||||
if [ -z "$GITHUB_ACTIONS" ]; then
|
||||
"$PYTHON" -m venv "$VENV"
|
||||
PIP="$VENV/bin/pip"
|
||||
else
|
||||
PIP="$PYTHON -m pip"
|
||||
fi
|
||||
|
||||
${PIP} install -U pip
|
||||
${PIP} install -r "$REQUIREMENTS"
|
||||
uv sync --frozen
|
||||
|
||||
10
scripts/lint
10
scripts/lint
@ -1,14 +1,8 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
export PREFIX=""
|
||||
if [ -d 'venv' ] ; then
|
||||
export PREFIX="venv/bin/"
|
||||
export PATH=${PREFIX}:${PATH}
|
||||
fi
|
||||
export SOURCE_FILES="uvicorn tests"
|
||||
|
||||
set -x
|
||||
|
||||
${PREFIX}ruff format $SOURCE_FILES
|
||||
${PREFIX}ruff --fix $SOURCE_FILES
|
||||
${PREFIX}python -m tools.cli_usage
|
||||
uv run ruff format $SOURCE_FILES
|
||||
uv run ruff check --fix $SOURCE_FILES
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
VERSION_FILE="uvicorn/__init__.py"
|
||||
|
||||
if [ -d 'venv' ] ; then
|
||||
PREFIX="venv/bin/"
|
||||
else
|
||||
PREFIX=""
|
||||
fi
|
||||
|
||||
if [ ! -z "$GITHUB_ACTIONS" ]; then
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
VERSION=`grep __version__ ${VERSION_FILE} | grep -o '[0-9][^"]*'`
|
||||
|
||||
if [ "refs/tags/${VERSION}" != "${GITHUB_REF}" ] ; then
|
||||
echo "GitHub Ref '${GITHUB_REF}' did not match package version '${VERSION}'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
set -x
|
||||
|
||||
${PREFIX}twine upload dist/*
|
||||
${PREFIX}mkdocs gh-deploy --force
|
||||
@ -1,7 +1,7 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
SEMVER_REGEX="([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?"
|
||||
CHANGELOG_VERSION=$(grep -o -E $SEMVER_REGEX CHANGELOG.md | head -1)
|
||||
SEMVER_REGEX="([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?(\+[0-9A-Za-z-]+)?"
|
||||
CHANGELOG_VERSION=$(grep -o -E $SEMVER_REGEX docs/release-notes.md | head -1)
|
||||
VERSION=$(grep -o -E $SEMVER_REGEX uvicorn/__init__.py | head -1)
|
||||
if [ "$CHANGELOG_VERSION" != "$VERSION" ]; then
|
||||
echo "Version in changelog does not match version in uvicorn/__init__.py!"
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
export PREFIX=""
|
||||
if [ -d 'venv' ]; then
|
||||
export PREFIX="venv/bin/"
|
||||
fi
|
||||
|
||||
set -ex
|
||||
|
||||
if [ -z $GITHUB_ACTIONS ]; then
|
||||
scripts/check
|
||||
fi
|
||||
|
||||
${PREFIX}coverage run --debug config -m pytest "$@"
|
||||
export COVERAGE_PROCESS_START=$(pwd)/pyproject.toml
|
||||
|
||||
uv run coverage run --debug config -m pytest "$@"
|
||||
|
||||
if [ -z $GITHUB_ACTIONS ]; then
|
||||
scripts/coverage
|
||||
|
||||
0
tests/benchmarks/__init__.py
Normal file
0
tests/benchmarks/__init__.py
Normal file
174
tests/benchmarks/http.py
Normal file
174
tests/benchmarks/http.py
Normal file
@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
|
||||
from uvicorn._types import ASGIApplication, Scope
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.lifespan.off import LifespanOff
|
||||
from uvicorn.protocols.http.h11_impl import H11Protocol
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
||||
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
||||
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol
|
||||
|
||||
WSProtocol: TypeAlias = WebSocketProtocol | _WSProtocol
|
||||
HTTPProtocol: TypeAlias = H11Protocol | HttpToolsProtocol
|
||||
|
||||
|
||||
SIMPLE_GET_REQUEST = b"\r\n".join([b"GET / HTTP/1.1", b"Host: example.org", b"", b""])
|
||||
|
||||
SIMPLE_POST_REQUEST = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: application/json",
|
||||
b"Content-Length: 18",
|
||||
b"",
|
||||
b'{"hello": "world"}',
|
||||
]
|
||||
)
|
||||
|
||||
LARGE_POST_REQUEST = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: text/plain",
|
||||
b"Content-Length: 100000",
|
||||
b"",
|
||||
b"x" * 100000,
|
||||
]
|
||||
)
|
||||
|
||||
HTTP10_GET_REQUEST = b"\r\n".join([b"GET / HTTP/1.0", b"Host: example.org", b"", b""])
|
||||
|
||||
CONNECTION_CLOSE_REQUEST = b"\r\n".join([b"GET / HTTP/1.1", b"Host: example.org", b"Connection: close", b"", b""])
|
||||
|
||||
START_POST_REQUEST = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: application/json",
|
||||
b"Content-Length: 18",
|
||||
b"",
|
||||
b"",
|
||||
]
|
||||
)
|
||||
|
||||
FINISH_POST_REQUEST = b'{"hello": "world"}'
|
||||
|
||||
BODY_CHUNK_SIZE = 256
|
||||
FRAGMENTED_BODY_SIZE = 100_000
|
||||
FRAGMENTED_POST_HEADERS = b"\r\n".join(
|
||||
[
|
||||
b"POST / HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: application/octet-stream",
|
||||
b"Content-Length: " + str(FRAGMENTED_BODY_SIZE).encode(),
|
||||
b"",
|
||||
b"",
|
||||
]
|
||||
)
|
||||
FRAGMENTED_BODY_CHUNKS = [b"x" * BODY_CHUNK_SIZE] * (FRAGMENTED_BODY_SIZE // BODY_CHUNK_SIZE)
|
||||
|
||||
|
||||
class MockTransport:
|
||||
def __init__(self) -> None:
|
||||
self.buffer = b""
|
||||
self.closed = False
|
||||
self.read_paused = False
|
||||
|
||||
def get_extra_info(self, key: Any) -> Any:
|
||||
return {
|
||||
"sockname": ("127.0.0.1", 8000),
|
||||
"peername": ("127.0.0.1", 8001),
|
||||
"sslcontext": False,
|
||||
}.get(key)
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
self.buffer += data
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
def pause_reading(self) -> None:
|
||||
self.read_paused = True
|
||||
|
||||
def resume_reading(self) -> None:
|
||||
self.read_paused = False
|
||||
|
||||
def is_closing(self) -> bool:
|
||||
return self.closed
|
||||
|
||||
def clear_buffer(self) -> None:
|
||||
self.buffer = b""
|
||||
|
||||
def set_protocol(self, protocol: asyncio.Protocol) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class MockTimerHandle:
|
||||
def __init__(
|
||||
self, loop_later_list: list[MockTimerHandle], delay: float, callback: Callable[[], None], args: tuple[Any, ...]
|
||||
) -> None:
|
||||
self.loop_later_list = loop_later_list
|
||||
self.delay = delay
|
||||
self.callback = callback
|
||||
self.args = args
|
||||
self.cancelled = False
|
||||
|
||||
def cancel(self) -> None:
|
||||
if not self.cancelled:
|
||||
self.cancelled = True
|
||||
self.loop_later_list.remove(self)
|
||||
|
||||
|
||||
class MockLoop:
|
||||
def __init__(self) -> None:
|
||||
self._tasks: list[asyncio.Task[Any]] = []
|
||||
self._later: list[MockTimerHandle] = []
|
||||
|
||||
def create_task(self, coroutine: Any) -> Any:
|
||||
self._tasks.insert(0, coroutine)
|
||||
return MockTask()
|
||||
|
||||
def call_later(self, delay: float, callback: Callable[[], None], *args: Any) -> MockTimerHandle:
|
||||
handle = MockTimerHandle(self._later, delay, callback, args)
|
||||
self._later.insert(0, handle)
|
||||
return handle
|
||||
|
||||
async def run_one(self) -> Any:
|
||||
return await self._tasks.pop()
|
||||
|
||||
|
||||
class MockTask:
|
||||
def add_done_callback(self, callback: Callable[[], None]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class MockProtocol(asyncio.Protocol):
|
||||
loop: MockLoop
|
||||
transport: MockTransport
|
||||
timeout_keep_alive_task: asyncio.TimerHandle | None
|
||||
ws_protocol_class: type[WSProtocol] | None
|
||||
scope: Scope
|
||||
|
||||
|
||||
def make_config(app: ASGIApplication, **kwargs: Any) -> Config:
|
||||
return Config(app=app, **kwargs)
|
||||
|
||||
|
||||
def get_connected_protocol(
|
||||
config: Config,
|
||||
http_protocol_cls: type[HTTPProtocol],
|
||||
) -> MockProtocol:
|
||||
loop = MockLoop()
|
||||
transport = MockTransport()
|
||||
lifespan = LifespanOff(config)
|
||||
server_state = ServerState()
|
||||
protocol = http_protocol_cls(config=config, server_state=server_state, app_state=lifespan.state, _loop=loop) # type: ignore
|
||||
protocol.connection_made(transport) # type: ignore[arg-type]
|
||||
return protocol # type: ignore[return-value]
|
||||
115
tests/benchmarks/test_http.py
Normal file
115
tests/benchmarks/test_http.py
Normal file
@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.benchmarks.http import (
|
||||
CONNECTION_CLOSE_REQUEST,
|
||||
FINISH_POST_REQUEST,
|
||||
FRAGMENTED_BODY_CHUNKS,
|
||||
FRAGMENTED_POST_HEADERS,
|
||||
HTTP10_GET_REQUEST,
|
||||
LARGE_POST_REQUEST,
|
||||
SIMPLE_GET_REQUEST,
|
||||
SIMPLE_POST_REQUEST,
|
||||
START_POST_REQUEST,
|
||||
get_connected_protocol,
|
||||
make_config,
|
||||
)
|
||||
from tests.response import Response
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tests.benchmarks.http import HTTPProtocol
|
||||
|
||||
pytestmark = [pytest.mark.anyio, pytest.mark.benchmark]
|
||||
|
||||
_plain_text_app = Response("Hello, world", media_type="text/plain")
|
||||
_no_content_app = Response(b"", status_code=204)
|
||||
_chunked_app = Response(b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"})
|
||||
|
||||
_plain_text_config = make_config(_plain_text_app)
|
||||
_chunked_config = make_config(_chunked_app)
|
||||
|
||||
|
||||
async def _body_echo_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
body = b""
|
||||
while True:
|
||||
message = await receive()
|
||||
body += message.get("body", b"") # type: ignore[operator]
|
||||
if not message.get("more_body", False):
|
||||
break
|
||||
headers = [(b"content-length", str(len(body)).encode())]
|
||||
await send({"type": "http.response.start", "status": 200, "headers": headers})
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
|
||||
|
||||
_body_echo_config = make_config(_body_echo_app)
|
||||
|
||||
|
||||
async def test_bench_simple_get(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_simple_post(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_POST_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_large_post(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(LARGE_POST_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_pipelined_requests(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST * 3)
|
||||
await protocol.loop.run_one()
|
||||
await protocol.loop.run_one()
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_keepalive_reuse(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_chunked_response(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_chunked_config, http_protocol_cls)
|
||||
protocol.data_received(SIMPLE_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_http10(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(HTTP10_GET_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_connection_close(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(CONNECTION_CLOSE_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_fragmented_body(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_plain_text_config, http_protocol_cls)
|
||||
protocol.data_received(FRAGMENTED_POST_HEADERS)
|
||||
for chunk in FRAGMENTED_BODY_CHUNKS:
|
||||
protocol.data_received(chunk)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_post_body_receive(http_protocol_cls: type[HTTPProtocol]) -> None:
|
||||
protocol = get_connected_protocol(_body_echo_config, http_protocol_cls)
|
||||
protocol.data_received(START_POST_REQUEST)
|
||||
protocol.data_received(FINISH_POST_REQUEST)
|
||||
await protocol.loop.run_one()
|
||||
64
tests/benchmarks/test_ws.py
Normal file
64
tests/benchmarks/test_ws.py
Normal file
@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.benchmarks.http import make_config
|
||||
from tests.benchmarks.ws import WS_UPGRADE, get_connected_ws_protocol
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tests.benchmarks.ws import WSProtocolClass
|
||||
|
||||
pytestmark = [pytest.mark.anyio, pytest.mark.benchmark]
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
pytest.param(
|
||||
"wsproto",
|
||||
marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."),
|
||||
id="wsproto",
|
||||
),
|
||||
pytest.param("websockets-sansio", id="websockets-sansio"),
|
||||
]
|
||||
)
|
||||
def ws_cls(request: pytest.FixtureRequest) -> WSProtocolClass:
|
||||
if request.param == "wsproto":
|
||||
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
|
||||
|
||||
return WSProtocol
|
||||
from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
|
||||
|
||||
return WebSocketsSansIOProtocol
|
||||
|
||||
|
||||
async def _ws_accept_close_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
await receive()
|
||||
await send({"type": "websocket.accept"})
|
||||
await send({"type": "websocket.close", "code": 1000})
|
||||
|
||||
|
||||
async def _ws_send_text_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
await receive()
|
||||
await send({"type": "websocket.accept"})
|
||||
await send({"type": "websocket.send", "text": "Hello, world!"})
|
||||
await send({"type": "websocket.close", "code": 1000})
|
||||
|
||||
|
||||
_ws_accept_close_config = make_config(_ws_accept_close_app, access_log=False)
|
||||
_ws_send_text_config = make_config(_ws_send_text_app, access_log=False)
|
||||
|
||||
|
||||
async def test_bench_ws_handshake(ws_cls: WSProtocolClass) -> None:
|
||||
protocol = get_connected_ws_protocol(_ws_accept_close_config, ws_cls)
|
||||
protocol.data_received(WS_UPGRADE)
|
||||
await protocol.loop.run_one()
|
||||
|
||||
|
||||
async def test_bench_ws_send_text(ws_cls: WSProtocolClass) -> None:
|
||||
protocol = get_connected_ws_protocol(_ws_send_text_config, ws_cls)
|
||||
protocol.data_received(WS_UPGRADE)
|
||||
await protocol.loop.run_one()
|
||||
40
tests/benchmarks/ws.py
Normal file
40
tests/benchmarks/ws.py
Normal file
@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
|
||||
from tests.benchmarks.http import MockLoop, MockTransport
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.lifespan.off import LifespanOff
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
|
||||
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
|
||||
|
||||
WSProtocolClass: TypeAlias = type[WSProtocol] | type[WebSocketsSansIOProtocol]
|
||||
|
||||
WS_UPGRADE = (
|
||||
b"GET / HTTP/1.1\r\n"
|
||||
b"Host: example.org\r\n"
|
||||
b"Upgrade: websocket\r\n"
|
||||
b"Connection: Upgrade\r\n"
|
||||
b"Sec-WebSocket-Key: YmVuY2htYXJra2V5MTIzNA==\r\n"
|
||||
b"Sec-WebSocket-Version: 13\r\n"
|
||||
b"\r\n"
|
||||
)
|
||||
|
||||
# Masked text frame: "Hello, world!" (13 bytes) with zero mask key
|
||||
WS_TEXT_FRAME = b"\x81\x8d\x00\x00\x00\x00Hello, world!"
|
||||
|
||||
# Masked close frame: code 1000 with zero mask key
|
||||
WS_CLOSE_FRAME = b"\x88\x82\x00\x00\x00\x00\x03\xe8"
|
||||
|
||||
|
||||
def get_connected_ws_protocol(config: Config, ws_protocol_cls: WSProtocolClass) -> Any:
|
||||
loop = MockLoop()
|
||||
transport = MockTransport()
|
||||
lifespan = LifespanOff(config)
|
||||
server_state = ServerState()
|
||||
protocol = ws_protocol_cls(config=config, server_state=server_state, app_state=lifespan.state, _loop=loop) # type: ignore[arg-type]
|
||||
protocol.connection_made(transport) # type: ignore[arg-type]
|
||||
return protocol
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import os
|
||||
@ -7,8 +9,7 @@ from copy import deepcopy
|
||||
from hashlib import md5
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@ -38,14 +39,14 @@ LOGGING_CONFIG["loggers"]["uvicorn"]["propagate"] = True
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate_authority() -> "trustme.CA":
|
||||
def tls_certificate_authority() -> trustme.CA:
|
||||
if not HAVE_TRUSTME:
|
||||
pytest.skip("trustme not installed") # pragma: no cover
|
||||
return trustme.CA()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate(tls_certificate_authority: "trustme.CA") -> "trustme.LeafCert":
|
||||
def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert:
|
||||
return tls_certificate_authority.issue_cert(
|
||||
"localhost",
|
||||
"127.0.0.1",
|
||||
@ -54,13 +55,13 @@ def tls_certificate(tls_certificate_authority: "trustme.CA") -> "trustme.LeafCer
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_ca_certificate_pem_path(tls_certificate_authority: "trustme.CA"):
|
||||
def tls_ca_certificate_pem_path(tls_certificate_authority: trustme.CA):
|
||||
with tls_certificate_authority.cert_pem.tempfile() as ca_cert_pem:
|
||||
yield ca_cert_pem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_ca_certificate_private_key_path(tls_certificate_authority: "trustme.CA"):
|
||||
def tls_ca_certificate_private_key_path(tls_certificate_authority: trustme.CA):
|
||||
with tls_certificate_authority.private_key_pem.tempfile() as private_key:
|
||||
yield private_key
|
||||
|
||||
@ -82,25 +83,25 @@ def tls_certificate_private_key_encrypted_path(tls_certificate):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate_private_key_path(tls_certificate: "trustme.CA"):
|
||||
def tls_certificate_private_key_path(tls_certificate: trustme.CA):
|
||||
with tls_certificate.private_key_pem.tempfile() as private_key:
|
||||
yield private_key
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate_key_and_chain_path(tls_certificate: "trustme.LeafCert"):
|
||||
def tls_certificate_key_and_chain_path(tls_certificate: trustme.LeafCert):
|
||||
with tls_certificate.private_key_and_cert_chain_pem.tempfile() as cert_pem:
|
||||
yield cert_pem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate_server_cert_path(tls_certificate: "trustme.LeafCert"):
|
||||
def tls_certificate_server_cert_path(tls_certificate: trustme.LeafCert):
|
||||
with tls_certificate.cert_chain_pems[0].tempfile() as cert_pem:
|
||||
yield cert_pem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_ca_ssl_context(tls_certificate_authority: "trustme.CA") -> ssl.SSLContext:
|
||||
def tls_ca_ssl_context(tls_certificate_authority: trustme.CA) -> ssl.SSLContext:
|
||||
ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
|
||||
tls_certificate_authority.configure_trust(ssl_ctx)
|
||||
return ssl_ctx
|
||||
@ -124,6 +125,9 @@ def reload_directory_structure(tmp_path_factory: pytest.TempPathFactory):
|
||||
│ └── sub.py
|
||||
├── ext
|
||||
│ └── ext.jpg
|
||||
├── .dotted
|
||||
├── .dotted_dir
|
||||
│ └── file.txt
|
||||
└── main.py
|
||||
"""
|
||||
root = tmp_path_factory.mktemp("reload_directory")
|
||||
@ -169,7 +173,7 @@ def anyio_backend() -> str:
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def logging_config() -> dict:
|
||||
def logging_config() -> dict[str, Any]:
|
||||
return deepcopy(LOGGING_CONFIG)
|
||||
|
||||
|
||||
@ -208,27 +212,6 @@ def short_socket_name(tmp_path, tmp_path_factory): # pragma: py-win32
|
||||
return
|
||||
|
||||
|
||||
def sleep_touch(*paths: Path):
|
||||
sleep(0.1)
|
||||
for p in paths:
|
||||
p.touch()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def touch_soon():
|
||||
threads = []
|
||||
|
||||
def start(*paths: Path):
|
||||
thread = Thread(target=sleep_touch, args=paths)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
|
||||
yield start
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
|
||||
def _unused_port(socket_type: int) -> int:
|
||||
"""Find an unused localhost port from 1024-65535 and return it."""
|
||||
with contextlib.closing(socket.socket(type=socket_type)) as sock:
|
||||
@ -247,12 +230,31 @@ def unused_tcp_port() -> int:
|
||||
params=[
|
||||
pytest.param(
|
||||
"uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
|
||||
marks=pytest.mark.skipif(
|
||||
not importlib.util.find_spec("wsproto"), reason="wsproto not installed."
|
||||
),
|
||||
marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."),
|
||||
id="wsproto",
|
||||
),
|
||||
pytest.param("uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", id="websockets"),
|
||||
pytest.param(
|
||||
"uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", id="websockets-sansio"
|
||||
),
|
||||
"uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
|
||||
]
|
||||
)
|
||||
def ws_protocol_cls(request: pytest.FixtureRequest):
|
||||
return import_from_string(request.param)
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
pytest.param(
|
||||
"uvicorn.protocols.http.httptools_impl:HttpToolsProtocol",
|
||||
marks=pytest.mark.skipif(
|
||||
not importlib.util.find_spec("httptools"),
|
||||
reason="httptools not installed.",
|
||||
),
|
||||
id="httptools",
|
||||
),
|
||||
pytest.param("uvicorn.protocols.http.h11_impl:H11Protocol", id="h11"),
|
||||
]
|
||||
)
|
||||
def http_protocol_cls(request: pytest.FixtureRequest):
|
||||
return import_from_string(request.param)
|
||||
|
||||
7
tests/custom_loop_utils.py
Normal file
7
tests/custom_loop_utils.py
Normal file
@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
class CustomLoop(asyncio.SelectorEventLoop):
|
||||
pass
|
||||
0
tests/middleware/__init__.py
Normal file
0
tests/middleware/__init__.py
Normal file
@ -1,32 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import typing
|
||||
from collections.abc import Iterator
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import websockets
|
||||
import websockets.client
|
||||
from websockets.protocol import State
|
||||
|
||||
from tests.utils import run_server
|
||||
from uvicorn import Config
|
||||
from uvicorn.protocols.http.h11_impl import H11Protocol
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
|
||||
try:
|
||||
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
||||
if TYPE_CHECKING:
|
||||
import sys
|
||||
|
||||
HTTP_PROTOCOLS = [H11Protocol, HttpToolsProtocol]
|
||||
except ImportError: # pragma: nocover
|
||||
HTTP_PROTOCOLS = [H11Protocol]
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
||||
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
|
||||
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol
|
||||
|
||||
WSProtocol: TypeAlias = "type[WebSocketProtocol | _WSProtocol]"
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def caplog_for_logger(caplog, logger_name):
|
||||
def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> Iterator[pytest.LogCaptureFixture]:
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.propagate, old_propagate = False, logger.propagate
|
||||
logger.addHandler(caplog.handler)
|
||||
@ -37,14 +39,13 @@ def caplog_for_logger(caplog, logger_name):
|
||||
logger.propagate = old_propagate
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
||||
assert scope["type"] == "http"
|
||||
await send({"type": "http.response.start", "status": 204, "headers": []})
|
||||
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_trace_logging(caplog, logging_config, unused_tcp_port: int):
|
||||
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
log_level="trace",
|
||||
@ -57,9 +58,7 @@ async def test_trace_logging(caplog, logging_config, unused_tcp_port: int):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
messages = [
|
||||
record.message for record in caplog.records if record.name == "uvicorn.asgi"
|
||||
]
|
||||
messages = [record.message for record in caplog.records if record.name == "uvicorn.asgi"]
|
||||
assert "ASGI [1] Started scope=" in messages.pop(0)
|
||||
assert "ASGI [1] Raised exception" in messages.pop(0)
|
||||
assert "ASGI [2] Started scope=" in messages.pop(0)
|
||||
@ -68,15 +67,11 @@ async def test_trace_logging(caplog, logging_config, unused_tcp_port: int):
|
||||
assert "ASGI [2] Completed" in messages.pop(0)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("http_protocol", HTTP_PROTOCOLS)
|
||||
async def test_trace_logging_on_http_protocol(
|
||||
http_protocol, caplog, logging_config, unused_tcp_port: int
|
||||
):
|
||||
async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging_config, unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
log_level="trace",
|
||||
http=http_protocol,
|
||||
http=http_protocol_cls,
|
||||
log_config=logging_config,
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
@ -85,23 +80,18 @@ async def test_trace_logging_on_http_protocol(
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "uvicorn.error"
|
||||
]
|
||||
messages = [record.message for record in caplog.records if record.name == "uvicorn.error"]
|
||||
assert any(" - HTTP connection made" in message for message in messages)
|
||||
assert any(" - HTTP connection lost" in message for message in messages)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_trace_logging_on_ws_protocol(
|
||||
ws_protocol_cls: "typing.Type[WSProtocol | WebSocketProtocol]",
|
||||
caplog,
|
||||
logging_config,
|
||||
ws_protocol_cls: WSProtocol,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
logging_config: dict[str, Any],
|
||||
unused_tcp_port: int,
|
||||
):
|
||||
async def websocket_app(scope, receive, send):
|
||||
async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
||||
assert scope["type"] == "websocket"
|
||||
while True:
|
||||
message = await receive()
|
||||
@ -110,9 +100,9 @@ async def test_trace_logging_on_ws_protocol(
|
||||
elif message["type"] == "websocket.disconnect":
|
||||
break
|
||||
|
||||
async def open_connection(url):
|
||||
async def open_connection(url: str):
|
||||
async with websockets.client.connect(url) as websocket:
|
||||
return websocket.open
|
||||
return websocket.state is State.OPEN
|
||||
|
||||
config = Config(
|
||||
app=websocket_app,
|
||||
@ -125,52 +115,38 @@ async def test_trace_logging_on_ws_protocol(
|
||||
async with run_server(config):
|
||||
is_open = await open_connection(f"ws://127.0.0.1:{unused_tcp_port}")
|
||||
assert is_open
|
||||
messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "uvicorn.error"
|
||||
]
|
||||
messages = [record.message for record in caplog.records if record.name == "uvicorn.error"]
|
||||
assert any(" - Upgrading to WebSocket" in message for message in messages)
|
||||
assert any(" - WebSocket connection made" in message for message in messages)
|
||||
assert any(" - WebSocket connection lost" in message for message in messages)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("use_colors", [(True), (False), (None)])
|
||||
async def test_access_logging(use_colors, caplog, logging_config, unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port
|
||||
)
|
||||
async def test_access_logging(
|
||||
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
|
||||
):
|
||||
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
|
||||
with caplog_for_logger(caplog, "uvicorn.access"):
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
|
||||
assert response.status_code == 204
|
||||
messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "uvicorn.access"
|
||||
]
|
||||
messages = [record.message for record in caplog.records if record.name == "uvicorn.access"]
|
||||
assert '"GET / HTTP/1.1" 204' in messages.pop()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("use_colors", [(True), (False)])
|
||||
async def test_default_logging(
|
||||
use_colors, caplog, logging_config, unused_tcp_port: int
|
||||
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
|
||||
):
|
||||
config = Config(
|
||||
app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port
|
||||
)
|
||||
config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port)
|
||||
with caplog_for_logger(caplog, "uvicorn.access"):
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
messages = [
|
||||
record.message for record in caplog.records if "uvicorn" in record.name
|
||||
]
|
||||
messages = [record.message for record in caplog.records if "uvicorn" in record.name]
|
||||
assert "Started server process" in messages.pop(0)
|
||||
assert "Waiting for application startup" in messages.pop(0)
|
||||
assert "ASGI 'lifespan' protocol appears unsupported" in messages.pop(0)
|
||||
@ -180,10 +156,9 @@ async def test_default_logging(
|
||||
assert "Shutting down" in messages.pop(0)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system")
|
||||
async def test_running_log_using_uds(
|
||||
caplog, short_socket_name, unused_tcp_port: int
|
||||
caplog: pytest.LogCaptureFixture, short_socket_name: str, unused_tcp_port: int
|
||||
): # pragma: py-win32
|
||||
config = Config(app=app, uds=short_socket_name, port=unused_tcp_port)
|
||||
with caplog_for_logger(caplog, "uvicorn.access"):
|
||||
@ -191,15 +166,11 @@ async def test_running_log_using_uds(
|
||||
...
|
||||
|
||||
messages = [record.message for record in caplog.records if "uvicorn" in record.name]
|
||||
assert (
|
||||
f"Uvicorn running on unix socket {short_socket_name} (Press CTRL+C to quit)"
|
||||
in messages
|
||||
)
|
||||
assert f"Uvicorn running on unix socket {short_socket_name} (Press CTRL+C to quit)" in messages
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system")
|
||||
async def test_running_log_using_fd(caplog, unused_tcp_port: int): # pragma: py-win32
|
||||
async def test_running_log_using_fd(caplog: pytest.LogCaptureFixture, unused_tcp_port: int): # pragma: py-win32
|
||||
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
||||
fd = sock.fileno()
|
||||
config = Config(app=app, fd=fd, port=unused_tcp_port)
|
||||
@ -211,9 +182,8 @@ async def test_running_log_using_fd(caplog, unused_tcp_port: int): # pragma: py
|
||||
assert f"Uvicorn running on socket {sockname} (Press CTRL+C to quit)" in messages
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_unknown_status_code(caplog, unused_tcp_port: int):
|
||||
async def app(scope, receive, send):
|
||||
async def test_unknown_status_code(caplog: pytest.LogCaptureFixture, unused_tcp_port: int):
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
||||
assert scope["type"] == "http"
|
||||
await send({"type": "http.response.start", "status": 599, "headers": []})
|
||||
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
||||
@ -225,19 +195,14 @@ async def test_unknown_status_code(caplog, unused_tcp_port: int):
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
|
||||
assert response.status_code == 599
|
||||
messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "uvicorn.access"
|
||||
]
|
||||
messages = [record.message for record in caplog.records if record.name == "uvicorn.access"]
|
||||
assert '"GET / HTTP/1.1" 599' in messages.pop()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_server_start_with_port_zero(caplog: pytest.LogCaptureFixture):
|
||||
config = Config(app=app, port=0)
|
||||
async with run_server(config) as server:
|
||||
server = server.servers[0]
|
||||
async with run_server(config) as _server:
|
||||
server = _server.servers[0]
|
||||
sock = server.sockets[0]
|
||||
host, port = sock.getsockname()
|
||||
messages = [record.message for record in caplog.records if "uvicorn" in record.name]
|
||||
|
||||
@ -2,13 +2,14 @@ import httpx
|
||||
import pytest
|
||||
|
||||
from tests.middleware.test_logging import caplog_for_logger
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
from uvicorn.logging import TRACE_LOG_LEVEL
|
||||
from uvicorn.middleware.message_logger import MessageLoggerMiddleware
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_message_logger(caplog):
|
||||
async def app(scope, receive, send):
|
||||
async def test_message_logger(caplog: pytest.LogCaptureFixture) -> None:
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
await receive()
|
||||
await send({"type": "http.response.start", "status": 200, "headers": []})
|
||||
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
||||
@ -17,8 +18,8 @@ async def test_message_logger(caplog):
|
||||
caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi")
|
||||
caplog.set_level(TRACE_LOG_LEVEL)
|
||||
|
||||
app = MessageLoggerMiddleware(app)
|
||||
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
transport = httpx.ASGITransport(MessageLoggerMiddleware(app)) # type: ignore
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
response = await client.get("/")
|
||||
assert response.status_code == 200
|
||||
messages = [record.msg % record.args for record in caplog.records]
|
||||
@ -26,21 +27,19 @@ async def test_message_logger(caplog):
|
||||
assert sum(["ASGI [1] Send" in message for message in messages]) == 2
|
||||
assert sum(["ASGI [1] Receive" in message for message in messages]) == 1
|
||||
assert sum(["ASGI [1] Completed" in message for message in messages]) == 1
|
||||
assert (
|
||||
sum(["ASGI [1] Raised exception" in message for message in messages]) == 0
|
||||
)
|
||||
assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 0
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_message_logger_exc(caplog):
|
||||
async def app(scope, receive, send):
|
||||
async def test_message_logger_exc(caplog: pytest.LogCaptureFixture) -> None:
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
raise RuntimeError()
|
||||
|
||||
with caplog_for_logger(caplog, "uvicorn.asgi"):
|
||||
caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi")
|
||||
caplog.set_level(TRACE_LOG_LEVEL)
|
||||
app = MessageLoggerMiddleware(app)
|
||||
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
transport = httpx.ASGITransport(MessageLoggerMiddleware(app)) # type: ignore
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
with pytest.raises(RuntimeError):
|
||||
await client.get("/")
|
||||
messages = [record.msg % record.args for record in caplog.records]
|
||||
@ -48,6 +47,4 @@ async def test_message_logger_exc(caplog):
|
||||
assert sum(["ASGI [1] Send" in message for message in messages]) == 0
|
||||
assert sum(["ASGI [1] Receive" in message for message in messages]) == 0
|
||||
assert sum(["ASGI [1] Completed" in message for message in messages]) == 0
|
||||
assert (
|
||||
sum(["ASGI [1] Raised exception" in message for message in messages]) == 1
|
||||
)
|
||||
assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 1
|
||||
|
||||
@ -1,131 +1,532 @@
|
||||
from typing import TYPE_CHECKING, List, Type, Union
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import ipaddress
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import websockets.client
|
||||
|
||||
from tests.protocols.test_http import HTTP_PROTOCOLS
|
||||
from tests.response import Response
|
||||
from tests.utils import run_server
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||||
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware, _TrustedHosts
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from uvicorn.protocols.http.h11_impl import H11Protocol
|
||||
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
||||
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
||||
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
|
||||
|
||||
|
||||
async def app(
|
||||
scope: Scope,
|
||||
receive: ASGIReceiveCallable,
|
||||
send: ASGISendCallable,
|
||||
) -> None:
|
||||
X_FORWARDED_FOR = "X-Forwarded-For"
|
||||
X_FORWARDED_PROTO = "X-Forwarded-Proto"
|
||||
|
||||
|
||||
async def default_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
scheme = scope["scheme"] # type: ignore
|
||||
host, port = scope["client"] # type: ignore
|
||||
addr = "%s://%s:%d" % (scheme, host, port)
|
||||
response = Response("Remote: " + addr, media_type="text/plain")
|
||||
if (client := scope["client"]) is None: # type: ignore
|
||||
client_addr = "NONE" # pragma: no cover
|
||||
else:
|
||||
host, port = client
|
||||
with contextlib.suppress(ValueError):
|
||||
if ipaddress.ip_address(host).version == 6:
|
||||
host = f"[{host}]"
|
||||
client_addr = f"{host}:{port}"
|
||||
|
||||
response = Response(f"{scheme}://{client_addr}", media_type="text/plain")
|
||||
await response(scope, receive, send)
|
||||
|
||||
|
||||
def make_httpx_client(
|
||||
trusted_hosts: str | list[str],
|
||||
client: tuple[str, int] = ("127.0.0.1", 123),
|
||||
) -> httpx.AsyncClient:
|
||||
"""Create async client for use in test cases.
|
||||
|
||||
Args:
|
||||
trusted_hosts: trusted_hosts for proxy middleware
|
||||
client: transport client to use
|
||||
"""
|
||||
|
||||
app = ProxyHeadersMiddleware(default_app, trusted_hosts)
|
||||
transport = httpx.ASGITransport(app=app, client=client) # type: ignore
|
||||
return httpx.AsyncClient(transport=transport, base_url="http://testserver")
|
||||
|
||||
|
||||
# Note: we vary the format here to also test some of the functionality
|
||||
# of the _TrustedHosts.__init__ method.
|
||||
_TRUSTED_NOTHING: list[str] = []
|
||||
_TRUSTED_EVERYTHING = "*"
|
||||
_TRUSTED_EVERYTHING_LIST = ["*"]
|
||||
_TRUSTED_IPv4_ADDRESSES = "127.0.0.1, 10.0.0.1"
|
||||
_TRUSTED_IPv4_NETWORKS = ["127.0.0.0/8", "10.0.0.0/8"]
|
||||
_TRUSTED_IPv6_ADDRESSES = [
|
||||
"2001:db8::",
|
||||
"2001:0db8:0001:0000:0000:0ab9:C0A8:0102",
|
||||
"2001:db8:3333:4444:5555:6666:1.2.3.4", # This is a dual address
|
||||
"::11.22.33.44", # This is a dual address
|
||||
]
|
||||
_TRUSTED_IPv6_NETWORKS = "2001:db8:abcd:0012::0/64"
|
||||
_TRUSTED_LITERALS = "some-literal , unix:///foo/bar , /foo/bar, garba*gewith*"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("init_hosts", "test_host", "expected"),
|
||||
[
|
||||
## Never Trust trust
|
||||
## -----------------------------
|
||||
# Test IPv4 Addresses
|
||||
(_TRUSTED_NOTHING, "127.0.0.0", False),
|
||||
(_TRUSTED_NOTHING, "127.0.0.1", False),
|
||||
(_TRUSTED_NOTHING, "127.1.1.1", False),
|
||||
(_TRUSTED_NOTHING, "127.255.255.255", False),
|
||||
(_TRUSTED_NOTHING, "10.0.0.0", False),
|
||||
(_TRUSTED_NOTHING, "10.0.0.1", False),
|
||||
(_TRUSTED_NOTHING, "10.1.1.1", False),
|
||||
(_TRUSTED_NOTHING, "10.255.255.255", False),
|
||||
(_TRUSTED_NOTHING, "192.168.0.0", False),
|
||||
(_TRUSTED_NOTHING, "192.168.0.1", False),
|
||||
(_TRUSTED_NOTHING, "1.1.1.1", False),
|
||||
# Test IPv6 Addresses
|
||||
(_TRUSTED_NOTHING, "2001:db8::", False),
|
||||
(_TRUSTED_NOTHING, "2001:db8:abcd:0012::0", False),
|
||||
(_TRUSTED_NOTHING, "2001:db8:abcd:0012::1:1", False),
|
||||
(_TRUSTED_NOTHING, "::", False),
|
||||
(_TRUSTED_NOTHING, "::1", False),
|
||||
(
|
||||
_TRUSTED_NOTHING,
|
||||
"2001:db8:3333:4444:5555:6666:102:304",
|
||||
False,
|
||||
), # aka 2001:db8:3333:4444:5555:6666:1.2.3.4
|
||||
(_TRUSTED_NOTHING, "::b16:212c", False), # aka ::11.22.33.44
|
||||
(_TRUSTED_NOTHING, "a:b:c:d::", False),
|
||||
(_TRUSTED_NOTHING, "::a:b:c:d", False),
|
||||
# Test Literals
|
||||
(_TRUSTED_NOTHING, "some-literal", False),
|
||||
(_TRUSTED_NOTHING, "unix:///foo/bar", False),
|
||||
(_TRUSTED_NOTHING, "/foo/bar", False),
|
||||
(_TRUSTED_NOTHING, "*", False),
|
||||
(_TRUSTED_NOTHING, "another-literal", False),
|
||||
(_TRUSTED_NOTHING, "unix:///another/path", False),
|
||||
(_TRUSTED_NOTHING, "/another/path", False),
|
||||
(_TRUSTED_NOTHING, "", False),
|
||||
## Always trust
|
||||
## -----------------------------
|
||||
# Test IPv4 Addresses
|
||||
(_TRUSTED_EVERYTHING, "127.0.0.0", True),
|
||||
(_TRUSTED_EVERYTHING, "127.0.0.1", True),
|
||||
(_TRUSTED_EVERYTHING, "127.1.1.1", True),
|
||||
(_TRUSTED_EVERYTHING, "127.255.255.255", True),
|
||||
(_TRUSTED_EVERYTHING, "10.0.0.0", True),
|
||||
(_TRUSTED_EVERYTHING, "10.0.0.1", True),
|
||||
(_TRUSTED_EVERYTHING, "10.1.1.1", True),
|
||||
(_TRUSTED_EVERYTHING, "10.255.255.255", True),
|
||||
(_TRUSTED_EVERYTHING, "192.168.0.0", True),
|
||||
(_TRUSTED_EVERYTHING, "192.168.0.1", True),
|
||||
(_TRUSTED_EVERYTHING, "1.1.1.1", True),
|
||||
(_TRUSTED_EVERYTHING_LIST, "1.1.1.1", True),
|
||||
# Test IPv6 Addresses
|
||||
(_TRUSTED_EVERYTHING, "2001:db8::", True),
|
||||
(_TRUSTED_EVERYTHING, "2001:db8:abcd:0012::0", True),
|
||||
(_TRUSTED_EVERYTHING, "2001:db8:abcd:0012::1:1", True),
|
||||
(_TRUSTED_EVERYTHING, "::", True),
|
||||
(_TRUSTED_EVERYTHING, "::1", True),
|
||||
(
|
||||
_TRUSTED_EVERYTHING,
|
||||
"2001:db8:3333:4444:5555:6666:102:304",
|
||||
True,
|
||||
), # aka 2001:db8:3333:4444:5555:6666:1.2.3.4
|
||||
(_TRUSTED_EVERYTHING, "::b16:212c", True), # aka ::11.22.33.44
|
||||
(_TRUSTED_EVERYTHING, "a:b:c:d::", True),
|
||||
(_TRUSTED_EVERYTHING, "::a:b:c:d", True),
|
||||
(_TRUSTED_EVERYTHING_LIST, "::a:b:c:d", True),
|
||||
# Test Literals
|
||||
(_TRUSTED_EVERYTHING, "some-literal", True),
|
||||
(_TRUSTED_EVERYTHING, "unix:///foo/bar", True),
|
||||
(_TRUSTED_EVERYTHING, "/foo/bar", True),
|
||||
(_TRUSTED_EVERYTHING, "*", True),
|
||||
(_TRUSTED_EVERYTHING, "another-literal", True),
|
||||
(_TRUSTED_EVERYTHING, "unix:///another/path", True),
|
||||
(_TRUSTED_EVERYTHING, "/another/path", True),
|
||||
(_TRUSTED_EVERYTHING, "", True),
|
||||
(_TRUSTED_EVERYTHING_LIST, "", True),
|
||||
## Trust IPv4 Addresses
|
||||
## -----------------------------
|
||||
# Test IPv4 Addresses
|
||||
(_TRUSTED_IPv4_ADDRESSES, "127.0.0.0", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "127.0.0.1", True),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "127.1.1.1", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "127.255.255.255", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "10.0.0.0", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "10.0.0.1", True),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "10.1.1.1", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "10.255.255.255", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "192.168.0.0", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "192.168.0.1", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "1.1.1.1", False),
|
||||
# Test IPv6 Addresses
|
||||
(_TRUSTED_IPv4_ADDRESSES, "2001:db8::", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "2001:db8:abcd:0012::0", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "2001:db8:abcd:0012::1:1", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "::", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "::1", False),
|
||||
(
|
||||
_TRUSTED_IPv4_ADDRESSES,
|
||||
"2001:db8:3333:4444:5555:6666:102:304",
|
||||
False,
|
||||
), # aka 2001:db8:3333:4444:5555:6666:1.2.3.4
|
||||
(_TRUSTED_IPv4_ADDRESSES, "::b16:212c", False), # aka ::11.22.33.44
|
||||
(_TRUSTED_IPv4_ADDRESSES, "a:b:c:d::", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "::a:b:c:d", False),
|
||||
# Test Literals
|
||||
(_TRUSTED_IPv4_ADDRESSES, "some-literal", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "unix:///foo/bar", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "*", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "/foo/bar", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "another-literal", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "unix:///another/path", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "/another/path", False),
|
||||
(_TRUSTED_IPv4_ADDRESSES, "", False),
|
||||
## Trust IPv6 Addresses
|
||||
## -----------------------------
|
||||
# Test IPv4 Addresses
|
||||
(_TRUSTED_IPv6_ADDRESSES, "127.0.0.0", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "127.0.0.1", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "127.1.1.1", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "127.255.255.255", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "10.0.0.0", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "10.0.0.1", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "10.1.1.1", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "10.255.255.255", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "192.168.0.0", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "192.168.0.1", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "1.1.1.1", False),
|
||||
# Test IPv6 Addresses
|
||||
(_TRUSTED_IPv6_ADDRESSES, "2001:db8::", True),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "2001:db8:abcd:0012::0", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "2001:db8:abcd:0012::1:1", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "::", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "::1", False),
|
||||
(
|
||||
_TRUSTED_IPv6_ADDRESSES,
|
||||
"2001:db8:3333:4444:5555:6666:102:304",
|
||||
True,
|
||||
), # aka 2001:db8:3333:4444:5555:6666:1.2.3.4
|
||||
(_TRUSTED_IPv6_ADDRESSES, "::b16:212c", True), # aka ::11.22.33.44
|
||||
(_TRUSTED_IPv6_ADDRESSES, "a:b:c:d::", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "::a:b:c:d", False),
|
||||
# Test Literals
|
||||
(_TRUSTED_IPv6_ADDRESSES, "some-literal", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "unix:///foo/bar", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "*", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "/foo/bar", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "another-literal", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "unix:///another/path", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "/another/path", False),
|
||||
(_TRUSTED_IPv6_ADDRESSES, "", False),
|
||||
## Trust IPv4 Networks
|
||||
## -----------------------------
|
||||
# Test IPv4 Addresses
|
||||
(_TRUSTED_IPv4_NETWORKS, "127.0.0.0", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "127.0.0.1", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "127.1.1.1", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "127.255.255.255", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "10.0.0.0", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "10.0.0.1", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "10.1.1.1", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "10.255.255.255", True),
|
||||
(_TRUSTED_IPv4_NETWORKS, "192.168.0.0", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "192.168.0.1", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "1.1.1.1", False),
|
||||
# Test IPv6 Addresses
|
||||
(_TRUSTED_IPv4_NETWORKS, "2001:db8::", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "2001:db8:abcd:0012::0", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "2001:db8:abcd:0012::1:1", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "::", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "::1", False),
|
||||
(
|
||||
_TRUSTED_IPv4_NETWORKS,
|
||||
"2001:db8:3333:4444:5555:6666:102:304",
|
||||
False,
|
||||
), # aka 2001:db8:3333:4444:5555:6666:1.2.3.4
|
||||
(_TRUSTED_IPv4_NETWORKS, "::b16:212c", False), # aka ::11.22.33.44
|
||||
(_TRUSTED_IPv4_NETWORKS, "a:b:c:d::", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "::a:b:c:d", False),
|
||||
# Test Literals
|
||||
(_TRUSTED_IPv4_NETWORKS, "some-literal", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "unix:///foo/bar", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "*", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "/foo/bar", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "another-literal", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "unix:///another/path", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "/another/path", False),
|
||||
(_TRUSTED_IPv4_NETWORKS, "", False),
|
||||
## Trust IPv6 Networks
|
||||
## -----------------------------
|
||||
# Test IPv4 Addresses
|
||||
(_TRUSTED_IPv6_NETWORKS, "127.0.0.0", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "127.0.0.1", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "127.1.1.1", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "127.255.255.255", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "10.0.0.0", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "10.0.0.1", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "10.1.1.1", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "10.255.255.255", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "192.168.0.0", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "192.168.0.1", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "1.1.1.1", False),
|
||||
# Test IPv6 Addresses
|
||||
(_TRUSTED_IPv6_NETWORKS, "2001:db8::", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "2001:db8:abcd:0012::0", True),
|
||||
(_TRUSTED_IPv6_NETWORKS, "2001:db8:abcd:0012::1:1", True),
|
||||
(_TRUSTED_IPv6_NETWORKS, "::", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "::1", False),
|
||||
(
|
||||
_TRUSTED_IPv6_NETWORKS,
|
||||
"2001:db8:3333:4444:5555:6666:102:304",
|
||||
False,
|
||||
), # aka 2001:db8:3333:4444:5555:6666:1.2.3.4
|
||||
(_TRUSTED_IPv6_NETWORKS, "::b16:212c", False), # aka ::11.22.33.44
|
||||
(_TRUSTED_IPv6_NETWORKS, "a:b:c:d::", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "::a:b:c:d", False),
|
||||
# Test Literals
|
||||
(_TRUSTED_IPv6_NETWORKS, "some-literal", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "unix:///foo/bar", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "*", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "/foo/bar", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "another-literal", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "unix:///another/path", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "/another/path", False),
|
||||
(_TRUSTED_IPv6_NETWORKS, "", False),
|
||||
## Trust Literals
|
||||
## -----------------------------
|
||||
# Test IPv4 Addresses
|
||||
(_TRUSTED_LITERALS, "127.0.0.0", False),
|
||||
(_TRUSTED_LITERALS, "127.0.0.1", False),
|
||||
(_TRUSTED_LITERALS, "127.1.1.1", False),
|
||||
(_TRUSTED_LITERALS, "127.255.255.255", False),
|
||||
(_TRUSTED_LITERALS, "10.0.0.0", False),
|
||||
(_TRUSTED_LITERALS, "10.0.0.1", False),
|
||||
(_TRUSTED_LITERALS, "10.1.1.1", False),
|
||||
(_TRUSTED_LITERALS, "10.255.255.255", False),
|
||||
(_TRUSTED_LITERALS, "192.168.0.0", False),
|
||||
(_TRUSTED_LITERALS, "192.168.0.1", False),
|
||||
(_TRUSTED_LITERALS, "1.1.1.1", False),
|
||||
# Test IPv6 Addresses
|
||||
(_TRUSTED_LITERALS, "2001:db8::", False),
|
||||
(_TRUSTED_LITERALS, "2001:db8:abcd:0012::0", False),
|
||||
(_TRUSTED_LITERALS, "2001:db8:abcd:0012::1:1", False),
|
||||
(_TRUSTED_LITERALS, "::", False),
|
||||
(_TRUSTED_LITERALS, "::1", False),
|
||||
(
|
||||
_TRUSTED_LITERALS,
|
||||
"2001:db8:3333:4444:5555:6666:102:304",
|
||||
False,
|
||||
), # aka 2001:db8:3333:4444:5555:6666:1.2.3.4
|
||||
(_TRUSTED_LITERALS, "::b16:212c", False), # aka ::11.22.33.44
|
||||
(_TRUSTED_LITERALS, "a:b:c:d::", False),
|
||||
(_TRUSTED_LITERALS, "::a:b:c:d", False),
|
||||
# Test Literals
|
||||
(_TRUSTED_LITERALS, "some-literal", True),
|
||||
(_TRUSTED_LITERALS, "unix:///foo/bar", True),
|
||||
(_TRUSTED_LITERALS, "*", False),
|
||||
(_TRUSTED_LITERALS, "/foo/bar", True),
|
||||
(_TRUSTED_LITERALS, "another-literal", False),
|
||||
(_TRUSTED_LITERALS, "unix:///another/path", False),
|
||||
(_TRUSTED_LITERALS, "/another/path", False),
|
||||
(_TRUSTED_LITERALS, "", False),
|
||||
],
|
||||
)
|
||||
def test_forwarded_hosts(init_hosts: str | list[str], test_host: str, expected: bool) -> None:
|
||||
trusted_hosts = _TrustedHosts(init_hosts)
|
||||
assert (test_host in trusted_hosts) is expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("trusted_hosts", "response_text"),
|
||||
("trusted_hosts", "expected"),
|
||||
[
|
||||
# always trust
|
||||
("*", "Remote: https://1.2.3.4:0"),
|
||||
("*", "https://1.2.3.4:0"),
|
||||
# trusted proxy
|
||||
("127.0.0.1", "Remote: https://1.2.3.4:0"),
|
||||
(["127.0.0.1"], "Remote: https://1.2.3.4:0"),
|
||||
("127.0.0.1", "https://1.2.3.4:0"),
|
||||
(["127.0.0.1"], "https://1.2.3.4:0"),
|
||||
# trusted proxy list
|
||||
(["127.0.0.1", "10.0.0.1"], "Remote: https://1.2.3.4:0"),
|
||||
("127.0.0.1, 10.0.0.1", "Remote: https://1.2.3.4:0"),
|
||||
(["127.0.0.1", "10.0.0.1"], "https://1.2.3.4:0"),
|
||||
("127.0.0.1, 10.0.0.1", "https://1.2.3.4:0"),
|
||||
# trusted proxy network
|
||||
# https://github.com/Kludex/uvicorn/issues/1068#issuecomment-1004813267
|
||||
("127.0.0.0/24, 10.0.0.1", "https://1.2.3.4:0"),
|
||||
# request from untrusted proxy
|
||||
("192.168.0.1", "Remote: http://127.0.0.1:123"),
|
||||
("192.168.0.1", "http://127.0.0.1:123"),
|
||||
# request from untrusted proxy network
|
||||
("192.168.0.0/16", "http://127.0.0.1:123"),
|
||||
# request from client running on proxy server itself
|
||||
# https://github.com/Kludex/uvicorn/issues/1068#issuecomment-855371576
|
||||
(["127.0.0.1", "1.2.3.4"], "https://1.2.3.4:0"),
|
||||
],
|
||||
)
|
||||
async def test_proxy_headers_trusted_hosts(
|
||||
trusted_hosts: Union[List[str], str], response_text: str
|
||||
) -> None:
|
||||
app_with_middleware = ProxyHeadersMiddleware(app, trusted_hosts=trusted_hosts)
|
||||
async with httpx.AsyncClient(
|
||||
app=app_with_middleware, base_url="http://testserver"
|
||||
) as client:
|
||||
headers = {"X-Forwarded-Proto": "https", "X-Forwarded-For": "1.2.3.4"}
|
||||
async def test_proxy_headers_trusted_hosts(trusted_hosts: str | list[str], expected: str) -> None:
|
||||
async with make_httpx_client(trusted_hosts) as client:
|
||||
headers = {X_FORWARDED_FOR: "1.2.3.4", X_FORWARDED_PROTO: "https"}
|
||||
response = await client.get("/", headers=headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == response_text
|
||||
assert response.text == expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("trusted_hosts", "response_text"),
|
||||
("forwarded_for", "forwarded_proto", "expected"),
|
||||
[
|
||||
# always trust
|
||||
("*", "Remote: https://1.2.3.4:0"),
|
||||
# all proxies are trusted
|
||||
(
|
||||
["127.0.0.1", "10.0.2.1", "192.168.0.2"],
|
||||
"Remote: https://1.2.3.4:0",
|
||||
),
|
||||
# order doesn't matter
|
||||
(
|
||||
["10.0.2.1", "192.168.0.2", "127.0.0.1"],
|
||||
"Remote: https://1.2.3.4:0",
|
||||
),
|
||||
# should set first untrusted as remote address
|
||||
(["192.168.0.2", "127.0.0.1"], "Remote: https://10.0.2.1:0"),
|
||||
("", "", "http://127.0.0.1:123"),
|
||||
("", None, "http://127.0.0.1:123"),
|
||||
("", "asdf", "http://127.0.0.1:123"),
|
||||
(" , ", "https", "https://127.0.0.1:123"),
|
||||
(", , ", "https", "https://127.0.0.1:123"),
|
||||
(" , 10.0.0.1", "https", "https://127.0.0.1:123"),
|
||||
("9.9.9.9 , , , 10.0.0.1", "https", "https://127.0.0.1:123"),
|
||||
(", , 9.9.9.9", "https", "https://9.9.9.9:0"),
|
||||
(", , 9.9.9.9, , ", "https", "https://127.0.0.1:123"),
|
||||
],
|
||||
)
|
||||
async def test_proxy_headers_multiple_proxies(
|
||||
trusted_hosts: Union[List[str], str], response_text: str
|
||||
async def test_proxy_headers_trusted_hosts_malformed(
|
||||
forwarded_for: str,
|
||||
forwarded_proto: str | None,
|
||||
expected: str,
|
||||
) -> None:
|
||||
app_with_middleware = ProxyHeadersMiddleware(app, trusted_hosts=trusted_hosts)
|
||||
async with httpx.AsyncClient(
|
||||
app=app_with_middleware, base_url="http://testserver"
|
||||
) as client:
|
||||
async with make_httpx_client("127.0.0.1, 10.0.0.0/8") as client:
|
||||
headers = {X_FORWARDED_FOR: forwarded_for}
|
||||
if forwarded_proto is not None:
|
||||
headers[X_FORWARDED_PROTO] = forwarded_proto
|
||||
response = await client.get("/", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.text == expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("trusted_hosts", "expected"),
|
||||
[
|
||||
# always trust
|
||||
("*", "https://1.2.3.4:0"),
|
||||
# all proxies are trusted
|
||||
(["127.0.0.1", "10.0.2.1", "192.168.0.2"], "https://1.2.3.4:0"),
|
||||
# order doesn't matter
|
||||
(["10.0.2.1", "192.168.0.2", "127.0.0.1"], "https://1.2.3.4:0"),
|
||||
# should set first untrusted as remote address
|
||||
(["192.168.0.2", "127.0.0.1"], "https://10.0.2.1:0"),
|
||||
# Mixed literals and networks
|
||||
(["127.0.0.1", "10.0.0.0/8", "192.168.0.2"], "https://1.2.3.4:0"),
|
||||
],
|
||||
)
|
||||
async def test_proxy_headers_multiple_proxies(trusted_hosts: str | list[str], expected: str) -> None:
|
||||
async with make_httpx_client(trusted_hosts) as client:
|
||||
headers = {X_FORWARDED_FOR: "1.2.3.4, 10.0.2.1, 192.168.0.2", X_FORWARDED_PROTO: "https"}
|
||||
response = await client.get("/", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.text == expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
("trusted_hosts", "expected"),
|
||||
[
|
||||
# always trust
|
||||
("*", "https://1.2.3.4:1234"),
|
||||
# all proxies are trusted
|
||||
(["127.0.0.1", "2001:db8::1", "192.168.0.2"], "https://1.2.3.4:1234"),
|
||||
# should set first untrusted as remote address
|
||||
(["192.168.0.2", "127.0.0.1"], "https://[2001:db8::1]:8080"),
|
||||
# Mixed literals and networks
|
||||
(["127.0.0.1", "2001:db8::/32", "192.168.0.2"], "https://1.2.3.4:1234"),
|
||||
],
|
||||
)
|
||||
async def test_proxy_headers_multiple_proxies_with_ports(trusted_hosts: str | list[str], expected: str) -> None:
|
||||
async with make_httpx_client(trusted_hosts) as client:
|
||||
headers = {
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-For": "1.2.3.4, 10.0.2.1, 192.168.0.2",
|
||||
X_FORWARDED_FOR: "1.2.3.4:1234, [2001:db8::1]:8080, 192.168.0.2:9000",
|
||||
X_FORWARDED_PROTO: "https",
|
||||
}
|
||||
response = await client.get("/", headers=headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.text == response_text
|
||||
assert response.text == expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_proxy_headers_invalid_x_forwarded_for() -> None:
|
||||
app_with_middleware = ProxyHeadersMiddleware(app, trusted_hosts="*")
|
||||
async with httpx.AsyncClient(
|
||||
app=app_with_middleware, base_url="http://testserver"
|
||||
) as client:
|
||||
async with make_httpx_client("*") as client:
|
||||
headers = httpx.Headers(
|
||||
{
|
||||
"X-Forwarded-Proto": "https",
|
||||
"X-Forwarded-For": "1.2.3.4, \xf0\xfd\xfd\xfd",
|
||||
X_FORWARDED_FOR: "1.2.3.4, \xf0\xfd\xfd\xfd, unix:, ::1",
|
||||
X_FORWARDED_PROTO: "https",
|
||||
},
|
||||
encoding="latin-1",
|
||||
)
|
||||
response = await client.get("/", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.text == "Remote: https://1.2.3.4:0"
|
||||
assert response.text == "https://1.2.3.4:0"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize("http_protocol_cls", HTTP_PROTOCOLS)
|
||||
@pytest.mark.parametrize(
|
||||
("forwarded_for", "expected"),
|
||||
[
|
||||
# IPv4 without port
|
||||
("1.2.3.4", "https://1.2.3.4:0"),
|
||||
# IPv4 with port
|
||||
("1.2.3.4:1234", "https://1.2.3.4:1234"),
|
||||
# Bracketed IPv6 with port
|
||||
("[2001:db8::1]:443", "https://[2001:db8::1]:443"),
|
||||
# Bracketed IPv6 without port
|
||||
("[2001:db8::1]", "https://[2001:db8::1]:0"),
|
||||
# Bare IPv6 without port
|
||||
("2001:db8::1", "https://[2001:db8::1]:0"),
|
||||
# Invalid IPv4 port falls back to the original host value
|
||||
("1.2.3.4:notaport", "https://1.2.3.4:notaport:0"),
|
||||
# Invalid bracketed IPv6 port keeps the host and drops the port
|
||||
("[2001:db8::1]:notaport", "https://[2001:db8::1]:0"),
|
||||
# Trailing data after a bracketed IPv6 host is left untouched
|
||||
("[2001:db8::1]extra", "https://[2001:db8::1]extra:0"),
|
||||
# Malformed bracket is left untouched
|
||||
("[2001:db8::1", "https://[2001:db8::1:0"),
|
||||
],
|
||||
)
|
||||
async def test_proxy_headers_x_forwarded_for_port_shapes(forwarded_for: str, expected: str) -> None:
|
||||
async with make_httpx_client("*") as client:
|
||||
headers = {X_FORWARDED_FOR: forwarded_for, X_FORWARDED_PROTO: "https"}
|
||||
response = await client.get("/", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.text == expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"forwarded_proto,expected",
|
||||
[
|
||||
("http", "ws://1.2.3.4:0"),
|
||||
("https", "wss://1.2.3.4:0"),
|
||||
("ws", "ws://1.2.3.4:0"),
|
||||
("wss", "wss://1.2.3.4:0"),
|
||||
],
|
||||
)
|
||||
async def test_proxy_headers_websocket_x_forwarded_proto(
|
||||
ws_protocol_cls: "Type[WSProtocol | WebSocketProtocol]",
|
||||
http_protocol_cls,
|
||||
forwarded_proto: str,
|
||||
expected: str,
|
||||
ws_protocol_cls: type[WSProtocol | WebSocketProtocol],
|
||||
http_protocol_cls: type[H11Protocol | HttpToolsProtocol],
|
||||
unused_tcp_port: int,
|
||||
) -> None:
|
||||
async def websocket_app(scope, receive, send):
|
||||
async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
assert scope["type"] == "websocket"
|
||||
scheme = scope["scheme"]
|
||||
assert scope["client"] is not None
|
||||
host, port = scope["client"]
|
||||
addr = "%s://%s:%d" % (scheme, host, port)
|
||||
await send({"type": "websocket.accept"})
|
||||
await send({"type": "websocket.send", "text": addr})
|
||||
await send({"type": "websocket.send", "text": f"{scheme}://{host}:{port}"})
|
||||
await send({"type": "websocket.close"})
|
||||
|
||||
app_with_middleware = ProxyHeadersMiddleware(websocket_app, trusted_hosts="*")
|
||||
config = Config(
|
||||
@ -138,7 +539,18 @@ async def test_proxy_headers_websocket_x_forwarded_proto(
|
||||
|
||||
async with run_server(config):
|
||||
url = f"ws://127.0.0.1:{unused_tcp_port}"
|
||||
headers = {"X-Forwarded-Proto": "https", "X-Forwarded-For": "1.2.3.4"}
|
||||
headers = {X_FORWARDED_FOR: "1.2.3.4", X_FORWARDED_PROTO: forwarded_proto}
|
||||
async with websockets.client.connect(url, extra_headers=headers) as websocket:
|
||||
data = await websocket.recv()
|
||||
assert data == "wss://1.2.3.4:0"
|
||||
assert data == expected
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_proxy_headers_empty_x_forwarded_for() -> None:
|
||||
# fallback to the default behavior if x-forwarded-for is an empty list
|
||||
# https://github.com/Kludex/uvicorn/issues/1068#issuecomment-855371576
|
||||
async with make_httpx_client("*") as client:
|
||||
headers = {X_FORWARDED_FOR: "", X_FORWARDED_PROTO: "https"}
|
||||
response = await client.get("/", headers=headers)
|
||||
assert response.status_code == 200
|
||||
assert response.text == "https://127.0.0.1:123"
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
from typing import AsyncGenerator, Callable, List
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
|
||||
import a2wsgi
|
||||
import httpx
|
||||
@ -10,7 +12,7 @@ from uvicorn._types import Environ, HTTPRequestEvent, HTTPScope, StartResponse
|
||||
from uvicorn.middleware import wsgi
|
||||
|
||||
|
||||
def hello_world(environ: Environ, start_response: StartResponse) -> List[bytes]:
|
||||
def hello_world(environ: Environ, start_response: StartResponse) -> list[bytes]:
|
||||
status = "200 OK"
|
||||
output = b"Hello World!\n"
|
||||
headers = [
|
||||
@ -21,7 +23,7 @@ def hello_world(environ: Environ, start_response: StartResponse) -> List[bytes]:
|
||||
return [output]
|
||||
|
||||
|
||||
def echo_body(environ: Environ, start_response: StartResponse) -> List[bytes]:
|
||||
def echo_body(environ: Environ, start_response: StartResponse) -> list[bytes]:
|
||||
status = "200 OK"
|
||||
output = environ["wsgi.input"].read()
|
||||
headers = [
|
||||
@ -32,11 +34,11 @@ def echo_body(environ: Environ, start_response: StartResponse) -> List[bytes]:
|
||||
return [output]
|
||||
|
||||
|
||||
def raise_exception(environ: Environ, start_response: StartResponse) -> List[bytes]:
|
||||
def raise_exception(environ: Environ, start_response: StartResponse) -> list[bytes]:
|
||||
raise RuntimeError("Something went wrong")
|
||||
|
||||
|
||||
def return_exc_info(environ: Environ, start_response: StartResponse) -> List[bytes]:
|
||||
def return_exc_info(environ: Environ, start_response: StartResponse) -> list[bytes]:
|
||||
try:
|
||||
raise RuntimeError("Something went wrong")
|
||||
except RuntimeError:
|
||||
@ -57,8 +59,8 @@ def wsgi_middleware(request: pytest.FixtureRequest) -> Callable:
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_wsgi_get(wsgi_middleware: Callable) -> None:
|
||||
app = wsgi_middleware(hello_world)
|
||||
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
transport = httpx.ASGITransport(wsgi_middleware(hello_world))
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
response = await client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.text == "Hello World!\n"
|
||||
@ -66,11 +68,11 @@ async def test_wsgi_get(wsgi_middleware: Callable) -> None:
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_wsgi_post(wsgi_middleware: Callable) -> None:
|
||||
app = wsgi_middleware(echo_body)
|
||||
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
transport = httpx.ASGITransport(wsgi_middleware(echo_body))
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
response = await client.post("/", json={"example": 123})
|
||||
assert response.status_code == 200
|
||||
assert response.text == '{"example": 123}'
|
||||
assert response.text == '{"example":123}'
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@ -79,8 +81,8 @@ async def test_wsgi_put_more_body(wsgi_middleware: Callable) -> None:
|
||||
for _ in range(1024):
|
||||
yield b"123456789abcdef\n" * 64
|
||||
|
||||
app = wsgi_middleware(echo_body)
|
||||
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
transport = httpx.ASGITransport(wsgi_middleware(echo_body))
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
response = await client.put("/", content=generate_body())
|
||||
assert response.status_code == 200
|
||||
assert response.text == "123456789abcdef\n" * 64 * 1024
|
||||
@ -90,43 +92,34 @@ async def test_wsgi_put_more_body(wsgi_middleware: Callable) -> None:
|
||||
async def test_wsgi_exception(wsgi_middleware: Callable) -> None:
|
||||
# Note that we're testing the WSGI app directly here.
|
||||
# The HTTP protocol implementations would catch this error and return 500.
|
||||
app = wsgi_middleware(raise_exception)
|
||||
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
transport = httpx.ASGITransport(wsgi_middleware(raise_exception))
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
with pytest.raises(RuntimeError):
|
||||
await client.get("/")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_wsgi_exc_info(wsgi_middleware: Callable) -> None:
|
||||
# Note that we're testing the WSGI app directly here.
|
||||
# The HTTP protocol implementations would catch this error and return 500.
|
||||
app = wsgi_middleware(return_exc_info)
|
||||
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
|
||||
with pytest.raises(RuntimeError):
|
||||
response = await client.get("/")
|
||||
|
||||
app = wsgi_middleware(return_exc_info)
|
||||
transport = httpx.ASGITransport(
|
||||
app=app,
|
||||
raise_app_exceptions=False,
|
||||
)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://testserver"
|
||||
) as client:
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
|
||||
response = await client.get("/")
|
||||
assert response.status_code == 500
|
||||
assert response.text == "Internal Server Error"
|
||||
|
||||
|
||||
def test_build_environ_encoding() -> None:
|
||||
scope: "HTTPScope" = {
|
||||
scope: HTTPScope = {
|
||||
"asgi": {"version": "3.0", "spec_version": "2.0"},
|
||||
"scheme": "http",
|
||||
"raw_path": b"/\xe6\x96\x87",
|
||||
"raw_path": b"/\xe6\x96\x87%2Fall",
|
||||
"type": "http",
|
||||
"http_version": "1.1",
|
||||
"method": "GET",
|
||||
"path": "/文",
|
||||
"path": "/文/all",
|
||||
"root_path": "/文",
|
||||
"client": None,
|
||||
"server": None,
|
||||
@ -134,11 +127,12 @@ def test_build_environ_encoding() -> None:
|
||||
"headers": [(b"key", b"value1"), (b"key", b"value2")],
|
||||
"extensions": {},
|
||||
}
|
||||
message: "HTTPRequestEvent" = {
|
||||
message: HTTPRequestEvent = {
|
||||
"type": "http.request",
|
||||
"body": b"",
|
||||
"more_body": False,
|
||||
}
|
||||
environ = wsgi.build_environ(scope, message, io.BytesIO(b""))
|
||||
assert environ["PATH_INFO"] == "/文".encode("utf8").decode("latin-1")
|
||||
assert environ["SCRIPT_NAME"] == "/文".encode().decode("latin-1")
|
||||
assert environ["PATH_INFO"] == b"/all".decode("latin-1")
|
||||
assert environ["HTTP_KEY"] == "value1,value2"
|
||||
|
||||
0
tests/protocols/__init__.py
Normal file
0
tests/protocols/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from asyncio import Transport
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
@ -7,7 +10,12 @@ from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_remote_
|
||||
|
||||
|
||||
class MockSocket:
|
||||
def __init__(self, family, peername=None, sockname=None):
|
||||
def __init__(
|
||||
self,
|
||||
family: socket.AddressFamily,
|
||||
peername: tuple[str, int] | None = None,
|
||||
sockname: tuple[str, int] | str | None = None,
|
||||
):
|
||||
self.peername = peername
|
||||
self.sockname = sockname
|
||||
self.family = family
|
||||
@ -20,62 +28,52 @@ class MockSocket:
|
||||
|
||||
|
||||
class MockTransport(Transport):
|
||||
def __init__(self, info):
|
||||
def __init__(self, info: dict[str, Any]) -> None:
|
||||
self.info = info
|
||||
|
||||
def get_extra_info(self, info_type):
|
||||
return self.info.get(info_type)
|
||||
def get_extra_info(self, name: str, default: Any = None) -> Any:
|
||||
return self.info.get(name)
|
||||
|
||||
|
||||
def test_get_local_addr_with_socket():
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_IPX)})
|
||||
assert get_local_addr(transport) is None
|
||||
|
||||
transport = MockTransport(
|
||||
{"socket": MockSocket(family=socket.AF_INET6, sockname=("::1", 123))}
|
||||
)
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_INET6, sockname=("::1", 123))})
|
||||
assert get_local_addr(transport) == ("::1", 123)
|
||||
|
||||
transport = MockTransport(
|
||||
{"socket": MockSocket(family=socket.AF_INET, sockname=("123.45.6.7", 123))}
|
||||
)
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_INET, sockname=("123.45.6.7", 123))})
|
||||
assert get_local_addr(transport) == ("123.45.6.7", 123)
|
||||
|
||||
if hasattr(socket, "AF_UNIX"): # pragma: no cover
|
||||
transport = MockTransport(
|
||||
{"socket": MockSocket(family=socket.AF_UNIX, sockname=("127.0.0.1", 8000))}
|
||||
)
|
||||
assert get_local_addr(transport) == ("127.0.0.1", 8000)
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_INET, sockname="/tmp/test.sock")})
|
||||
assert get_local_addr(transport) == ("/tmp/test.sock", None)
|
||||
|
||||
|
||||
def test_get_remote_addr_with_socket():
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_IPX)})
|
||||
assert get_remote_addr(transport) is None
|
||||
|
||||
transport = MockTransport(
|
||||
{"socket": MockSocket(family=socket.AF_INET6, peername=("::1", 123))}
|
||||
)
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_INET6, peername=("::1", 123))})
|
||||
assert get_remote_addr(transport) == ("::1", 123)
|
||||
|
||||
transport = MockTransport(
|
||||
{"socket": MockSocket(family=socket.AF_INET, peername=("123.45.6.7", 123))}
|
||||
)
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_INET, peername=("123.45.6.7", 123))})
|
||||
assert get_remote_addr(transport) == ("123.45.6.7", 123)
|
||||
|
||||
if hasattr(socket, "AF_UNIX"): # pragma: no cover
|
||||
transport = MockTransport(
|
||||
{"socket": MockSocket(family=socket.AF_UNIX, peername=("127.0.0.1", 8000))}
|
||||
)
|
||||
transport = MockTransport({"socket": MockSocket(family=socket.AF_UNIX, peername=("127.0.0.1", 8000))})
|
||||
assert get_remote_addr(transport) == ("127.0.0.1", 8000)
|
||||
|
||||
|
||||
def test_get_local_addr():
|
||||
transport = MockTransport({"sockname": "path/to/unix-domain-socket"})
|
||||
assert get_local_addr(transport) is None
|
||||
assert get_local_addr(transport) == ("path/to/unix-domain-socket", None)
|
||||
|
||||
transport = MockTransport({"sockname": ("123.45.6.7", 123)})
|
||||
assert get_local_addr(transport) == ("123.45.6.7", 123)
|
||||
|
||||
transport = MockTransport({})
|
||||
assert get_local_addr(transport) is None
|
||||
|
||||
|
||||
def test_get_remote_addr():
|
||||
transport = MockTransport({"peername": None})
|
||||
@ -90,5 +88,5 @@ def test_get_remote_addr():
|
||||
[({"client": ("127.0.0.1", 36000)}, "127.0.0.1:36000"), ({"client": None}, "")],
|
||||
ids=["ip:port client", "None client"],
|
||||
)
|
||||
def test_get_client_addr(scope, expected_client):
|
||||
def test_get_client_addr(scope: Any, expected_client: str):
|
||||
assert get_client_addr(scope) == expected_client
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -10,17 +10,15 @@ class Response:
|
||||
self.set_content_length()
|
||||
|
||||
async def __call__(self, scope, receive, send) -> None:
|
||||
prefix = "websocket." if scope["type"] == "websocket" else ""
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"type": prefix + "http.response.start",
|
||||
"status": self.status_code,
|
||||
"headers": [
|
||||
[key.encode(), value.encode()]
|
||||
for key, value in self.headers.items()
|
||||
],
|
||||
"headers": [[key.encode(), value.encode()] for key, value in self.headers.items()],
|
||||
}
|
||||
)
|
||||
await send({"type": "http.response.body", "body": self.body})
|
||||
await send({"type": prefix + "http.response.body", "body": self.body})
|
||||
|
||||
def render(self, content) -> bytes:
|
||||
if isinstance(content, bytes):
|
||||
|
||||
0
tests/supervisors/__init__.py
Normal file
0
tests/supervisors/__init__.py
Normal file
@ -1,22 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import signal
|
||||
import socket
|
||||
from typing import List, Optional
|
||||
import threading
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from uvicorn import Config
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
from uvicorn.supervisors import Multiprocess
|
||||
from uvicorn.supervisors.multiprocess import Process
|
||||
|
||||
|
||||
async def app(
|
||||
scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None:
|
||||
def new_console_in_windows(test_function: Callable[[], Any]) -> Callable[[], Any]: # pragma: no cover
|
||||
if os.name != "nt":
|
||||
return test_function
|
||||
|
||||
@functools.wraps(test_function)
|
||||
def new_function():
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
module = test_function.__module__
|
||||
name = test_function.__name__
|
||||
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-c", f"from {module} import {name}; {name}.__wrapped__()"],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
|
||||
return new_function
|
||||
|
||||
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def run(sockets: Optional[List[socket.socket]]) -> None:
|
||||
pass # pragma: no cover
|
||||
def run(sockets: list[socket.socket] | None) -> None:
|
||||
while True: # pragma: no cover
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_process_ping_pong() -> None:
|
||||
process = Process(Config(app=app), target=lambda x: None, sockets=[])
|
||||
threading.Thread(target=process.always_pong, daemon=True).start()
|
||||
assert process.ping()
|
||||
|
||||
|
||||
def test_process_ping_pong_timeout() -> None:
|
||||
process = Process(Config(app=app), target=lambda x: None, sockets=[])
|
||||
assert not process.ping(0.1)
|
||||
|
||||
|
||||
@new_console_in_windows
|
||||
def test_multiprocess_run() -> None:
|
||||
"""
|
||||
A basic sanity check.
|
||||
@ -26,5 +67,109 @@ def test_multiprocess_run() -> None:
|
||||
"""
|
||||
config = Config(app=app, workers=2)
|
||||
supervisor = Multiprocess(config, target=run, sockets=[])
|
||||
supervisor.signal_handler(sig=signal.SIGINT, frame=None)
|
||||
supervisor.run()
|
||||
threading.Thread(target=supervisor.run, daemon=True).start()
|
||||
supervisor.signal_queue.append(signal.SIGINT)
|
||||
supervisor.join_all()
|
||||
|
||||
|
||||
@new_console_in_windows
|
||||
def test_multiprocess_health_check() -> None:
|
||||
"""
|
||||
Ensure that the health check works as expected.
|
||||
"""
|
||||
config = Config(app=app, workers=2)
|
||||
supervisor = Multiprocess(config, target=run, sockets=[])
|
||||
threading.Thread(target=supervisor.run, daemon=True).start()
|
||||
time.sleep(1)
|
||||
process = supervisor.processes[0]
|
||||
process.kill()
|
||||
assert not process.is_alive()
|
||||
deadline = time.monotonic() + 10
|
||||
while not all(p.is_alive() for p in supervisor.processes): # pragma: no cover
|
||||
assert time.monotonic() < deadline, "Timed out waiting for processes to be alive"
|
||||
time.sleep(0.1)
|
||||
supervisor.signal_queue.append(signal.SIGINT)
|
||||
supervisor.join_all()
|
||||
|
||||
|
||||
@new_console_in_windows
|
||||
def test_multiprocess_sigterm() -> None:
|
||||
"""
|
||||
Ensure that the SIGTERM signal is handled as expected.
|
||||
"""
|
||||
config = Config(app=app, workers=2)
|
||||
supervisor = Multiprocess(config, target=run, sockets=[])
|
||||
threading.Thread(target=supervisor.run, daemon=True).start()
|
||||
time.sleep(1)
|
||||
supervisor.signal_queue.append(signal.SIGTERM)
|
||||
supervisor.join_all()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(signal, "SIGBREAK"), reason="platform unsupports SIGBREAK")
|
||||
@new_console_in_windows
|
||||
def test_multiprocess_sigbreak() -> None: # pragma: py-not-win32
|
||||
"""
|
||||
Ensure that the SIGBREAK signal is handled as expected.
|
||||
"""
|
||||
config = Config(app=app, workers=2)
|
||||
supervisor = Multiprocess(config, target=run, sockets=[])
|
||||
threading.Thread(target=supervisor.run, daemon=True).start()
|
||||
time.sleep(1)
|
||||
supervisor.signal_queue.append(getattr(signal, "SIGBREAK"))
|
||||
supervisor.join_all()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(signal, "SIGHUP"), reason="platform unsupports SIGHUP")
|
||||
def test_multiprocess_sighup() -> None:
|
||||
"""
|
||||
Ensure that the SIGHUP signal is handled as expected.
|
||||
"""
|
||||
config = Config(app=app, workers=2)
|
||||
supervisor = Multiprocess(config, target=run, sockets=[])
|
||||
threading.Thread(target=supervisor.run, daemon=True).start()
|
||||
time.sleep(1)
|
||||
pids = [p.pid for p in supervisor.processes]
|
||||
supervisor.signal_queue.append(signal.SIGHUP)
|
||||
# Poll instead of a fixed sleep — the supervisor loop runs on a 0.5s interval and `restart_all()` terminates/joins
|
||||
# each worker sequentially, so the total time is non-deterministic.
|
||||
deadline = time.monotonic() + 10
|
||||
while time.monotonic() < deadline:
|
||||
if [p.pid for p in supervisor.processes] != pids:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
assert pids != [p.pid for p in supervisor.processes]
|
||||
supervisor.signal_queue.append(signal.SIGINT)
|
||||
supervisor.join_all()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(signal, "SIGTTIN"), reason="platform unsupports SIGTTIN")
|
||||
def test_multiprocess_sigttin() -> None:
|
||||
"""
|
||||
Ensure that the SIGTTIN signal is handled as expected.
|
||||
"""
|
||||
config = Config(app=app, workers=2)
|
||||
supervisor = Multiprocess(config, target=run, sockets=[])
|
||||
threading.Thread(target=supervisor.run, daemon=True).start()
|
||||
supervisor.signal_queue.append(signal.SIGTTIN)
|
||||
time.sleep(1)
|
||||
assert len(supervisor.processes) == 3
|
||||
supervisor.signal_queue.append(signal.SIGINT)
|
||||
supervisor.join_all()
|
||||
|
||||
|
||||
@pytest.mark.skipif(not hasattr(signal, "SIGTTOU"), reason="platform unsupports SIGTTOU")
|
||||
def test_multiprocess_sigttou() -> None:
|
||||
"""
|
||||
Ensure that the SIGTTOU signal is handled as expected.
|
||||
"""
|
||||
config = Config(app=app, workers=2)
|
||||
supervisor = Multiprocess(config, target=run, sockets=[])
|
||||
threading.Thread(target=supervisor.run, daemon=True).start()
|
||||
supervisor.signal_queue.append(signal.SIGTTOU)
|
||||
time.sleep(1)
|
||||
assert len(supervisor.processes) == 1
|
||||
supervisor.signal_queue.append(signal.SIGTTOU)
|
||||
time.sleep(1)
|
||||
assert len(supervisor.processes) == 1
|
||||
supervisor.signal_queue.append(signal.SIGINT)
|
||||
supervisor.join_all()
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import logging
|
||||
import platform
|
||||
from __future__ import annotations
|
||||
|
||||
import signal
|
||||
import socket
|
||||
import sys
|
||||
from collections.abc import Callable, Generator
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
from typing import List, Optional, Type
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from tests.utils import as_cwd
|
||||
from uvicorn.config import Config
|
||||
@ -19,30 +21,39 @@ try:
|
||||
except ImportError: # pragma: no cover
|
||||
WatchFilesReload = None # type: ignore[misc,assignment]
|
||||
|
||||
try:
|
||||
from uvicorn.supervisors.watchgodreload import WatchGodReload
|
||||
except ImportError: # pragma: no cover
|
||||
WatchGodReload = None # type: ignore[misc,assignment]
|
||||
|
||||
# TODO: Investigate why this is flaky on MacOS, and Windows.
|
||||
skip_non_linux = pytest.mark.skipif(sys.platform in ("darwin", "win32"), reason="Flaky on Windows and MacOS")
|
||||
|
||||
|
||||
# TODO: Investigate why this is flaky on MacOS M1.
|
||||
skip_if_m1 = pytest.mark.skipif(
|
||||
sys.platform == "darwin" and platform.processor() == "arm",
|
||||
reason="Flaky on MacOS M1",
|
||||
)
|
||||
|
||||
|
||||
def run(sockets):
|
||||
def run(sockets: list[socket.socket] | None) -> None:
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
def sleep_touch(*paths: Path):
|
||||
sleep(0.1)
|
||||
for p in paths:
|
||||
p.touch()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def touch_soon() -> Generator[Callable[[Path], None]]:
|
||||
threads: list[Thread] = []
|
||||
|
||||
def start(*paths: Path) -> None:
|
||||
thread = Thread(target=sleep_touch, args=paths)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
|
||||
yield start
|
||||
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
|
||||
class TestBaseReload:
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(
|
||||
self,
|
||||
reload_directory_structure: Path,
|
||||
reloader_class: Optional[Type[BaseReload]],
|
||||
):
|
||||
def setup(self, reload_directory_structure: Path, reloader_class: type[BaseReload] | None):
|
||||
if reloader_class is None: # pragma: no cover
|
||||
pytest.skip("Needed dependency not installed")
|
||||
self.reload_path = reload_directory_structure
|
||||
@ -51,19 +62,15 @@ class TestBaseReload:
|
||||
def _setup_reloader(self, config: Config) -> BaseReload:
|
||||
config.reload_delay = 0 # save time
|
||||
|
||||
if self.reloader_class is WatchGodReload:
|
||||
with pytest.deprecated_call():
|
||||
reloader = self.reloader_class(config, target=run, sockets=[])
|
||||
else:
|
||||
reloader = self.reloader_class(config, target=run, sockets=[])
|
||||
reloader = self.reloader_class(config, target=run, sockets=[])
|
||||
|
||||
assert config.should_reload
|
||||
reloader.startup()
|
||||
return reloader
|
||||
|
||||
def _reload_tester(
|
||||
self, touch_soon, reloader: BaseReload, *files: Path
|
||||
) -> Optional[List[Path]]:
|
||||
self, touch_soon: Callable[[Path], None], reloader: BaseReload, *files: Path
|
||||
) -> list[Path] | None:
|
||||
reloader.restart()
|
||||
if WatchFilesReload is not None and isinstance(reloader, WatchFilesReload):
|
||||
touch_soon(*files)
|
||||
@ -74,9 +81,7 @@ class TestBaseReload:
|
||||
file.touch()
|
||||
return next(reloader)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
|
||||
)
|
||||
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
|
||||
def test_reloader_should_initialize(self) -> None:
|
||||
"""
|
||||
A basic sanity check.
|
||||
@ -89,10 +94,8 @@ class TestBaseReload:
|
||||
reloader = self._setup_reloader(config)
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
|
||||
)
|
||||
def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
|
||||
@pytest.mark.parametrize("reloader_class", [StatReload, pytest.param(WatchFilesReload, marks=skip_non_linux)])
|
||||
def test_reload_when_python_file_is_changed(self, touch_soon: Callable[[Path], None]):
|
||||
file = self.reload_path / "main.py"
|
||||
|
||||
with as_cwd(self.reload_path):
|
||||
@ -104,12 +107,8 @@ class TestBaseReload:
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
|
||||
)
|
||||
def test_should_reload_when_python_file_in_subdir_is_changed(
|
||||
self, touch_soon
|
||||
) -> None:
|
||||
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
|
||||
def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
|
||||
file = self.reload_path / "app" / "sub" / "sub.py"
|
||||
|
||||
with as_cwd(self.reload_path):
|
||||
@ -120,10 +119,8 @@ class TestBaseReload:
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
|
||||
def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(
|
||||
self, touch_soon
|
||||
) -> None:
|
||||
@pytest.mark.parametrize("reloader_class", [WatchFilesReload])
|
||||
def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
|
||||
sub_dir = self.reload_path / "app" / "sub"
|
||||
sub_file = sub_dir / "sub.py"
|
||||
|
||||
@ -140,33 +137,25 @@ class TestBaseReload:
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class, result", [(StatReload, False), (WatchFilesReload, True)]
|
||||
"reloader_class, result", [(StatReload, False), pytest.param(WatchFilesReload, True, marks=skip_non_linux)]
|
||||
)
|
||||
def test_reload_when_pattern_matched_file_is_changed(
|
||||
self, result: bool, touch_soon
|
||||
) -> None:
|
||||
self, result: bool, touch_soon: Callable[[Path], None]
|
||||
): # pragma: py-not-linux
|
||||
file = self.reload_path / "app" / "js" / "main.js"
|
||||
|
||||
with as_cwd(self.reload_path):
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app", reload=True, reload_includes=["*.js"]
|
||||
)
|
||||
config = Config(app="tests.test_config:asgi_app", reload=True, reload_includes=["*.js"])
|
||||
reloader = self._setup_reloader(config)
|
||||
|
||||
assert bool(self._reload_tester(touch_soon, reloader, file)) == result
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class",
|
||||
[
|
||||
pytest.param(WatchFilesReload, marks=skip_if_m1),
|
||||
WatchGodReload,
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_non_linux)])
|
||||
def test_should_not_reload_when_exclude_pattern_match_file_is_changed(
|
||||
self, touch_soon
|
||||
) -> None:
|
||||
self, touch_soon: Callable[[Path], None]
|
||||
): # pragma: py-not-linux
|
||||
python_file = self.reload_path / "app" / "src" / "main.py"
|
||||
css_file = self.reload_path / "app" / "css" / "main.css"
|
||||
js_file = self.reload_path / "app" / "js" / "main.js"
|
||||
@ -186,10 +175,8 @@ class TestBaseReload:
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
|
||||
)
|
||||
def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
|
||||
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
|
||||
def test_should_not_reload_when_dot_file_is_changed(self, touch_soon: Callable[[Path], None]):
|
||||
file = self.reload_path / ".dotted"
|
||||
|
||||
with as_cwd(self.reload_path):
|
||||
@ -200,10 +187,10 @@ class TestBaseReload:
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class", [StatReload, WatchGodReload, WatchFilesReload]
|
||||
)
|
||||
def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> None:
|
||||
@pytest.mark.parametrize("reloader_class", [StatReload, pytest.param(WatchFilesReload, marks=skip_non_linux)])
|
||||
def test_should_reload_when_directories_have_same_prefix(
|
||||
self, touch_soon: Callable[[Path], None]
|
||||
): # pragma: py-not-linux
|
||||
app_dir = self.reload_path / "app"
|
||||
app_file = app_dir / "src" / "main.py"
|
||||
app_first_dir = self.reload_path / "app_first"
|
||||
@ -224,15 +211,11 @@ class TestBaseReload:
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class",
|
||||
[
|
||||
StatReload,
|
||||
WatchGodReload,
|
||||
pytest.param(WatchFilesReload, marks=skip_if_m1),
|
||||
],
|
||||
[StatReload, pytest.param(WatchFilesReload, marks=skip_non_linux)],
|
||||
)
|
||||
def test_should_not_reload_when_only_subdirectory_is_watched(
|
||||
self, touch_soon
|
||||
) -> None:
|
||||
self, touch_soon: Callable[[Path], None]
|
||||
): # pragma: py-not-linux
|
||||
app_dir = self.reload_path / "app"
|
||||
app_dir_file = self.reload_path / "app" / "src" / "main.py"
|
||||
root_file = self.reload_path / "main.py"
|
||||
@ -245,20 +228,12 @@ class TestBaseReload:
|
||||
reloader = self._setup_reloader(config)
|
||||
|
||||
assert self._reload_tester(touch_soon, reloader, app_dir_file)
|
||||
assert not self._reload_tester(
|
||||
touch_soon, reloader, root_file, app_dir / "~ignored"
|
||||
)
|
||||
assert not self._reload_tester(touch_soon, reloader, root_file, app_dir / "~ignored")
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reloader_class",
|
||||
[
|
||||
pytest.param(WatchFilesReload, marks=skip_if_m1),
|
||||
WatchGodReload,
|
||||
],
|
||||
)
|
||||
def test_override_defaults(self, touch_soon) -> None:
|
||||
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_non_linux)])
|
||||
def test_override_defaults(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-not-linux
|
||||
dotted_file = self.reload_path / ".dotted"
|
||||
dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
|
||||
python_file = self.reload_path / "main.py"
|
||||
@ -279,6 +254,26 @@ class TestBaseReload:
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize("reloader_class", [pytest.param(WatchFilesReload, marks=skip_non_linux)])
|
||||
def test_explicit_paths(self, touch_soon: Callable[[Path], None]) -> None: # pragma: py-not-linux
|
||||
dotted_file = self.reload_path / ".dotted"
|
||||
non_dotted_file = self.reload_path / "ext" / "ext.jpg"
|
||||
python_file = self.reload_path / "main.py"
|
||||
|
||||
with as_cwd(self.reload_path):
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app",
|
||||
reload=True,
|
||||
reload_includes=[".dotted", "ext/ext.jpg"],
|
||||
)
|
||||
reloader = self._setup_reloader(config)
|
||||
|
||||
assert self._reload_tester(touch_soon, reloader, dotted_file)
|
||||
assert self._reload_tester(touch_soon, reloader, non_dotted_file)
|
||||
assert self._reload_tester(touch_soon, reloader, python_file)
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
|
||||
@pytest.mark.parametrize("reloader_class", [WatchFilesReload])
|
||||
def test_watchfiles_no_changes(self) -> None:
|
||||
@ -308,54 +303,19 @@ class TestBaseReload:
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
@pytest.mark.parametrize("reloader_class", [WatchGodReload])
|
||||
def test_should_detect_new_reload_dirs(
|
||||
self, touch_soon, caplog: pytest.LogCaptureFixture, tmp_path: Path
|
||||
) -> None:
|
||||
app_dir = tmp_path / "app"
|
||||
app_file = app_dir / "file.py"
|
||||
app_dir.mkdir()
|
||||
app_file.touch()
|
||||
app_first_dir = tmp_path / "app_first"
|
||||
app_first_file = app_first_dir / "file.py"
|
||||
|
||||
with as_cwd(tmp_path):
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app", reload=True, reload_includes=["app*"]
|
||||
)
|
||||
reloader = self._setup_reloader(config)
|
||||
assert self._reload_tester(touch_soon, reloader, app_file)
|
||||
|
||||
app_first_dir.mkdir()
|
||||
assert self._reload_tester(touch_soon, reloader, app_first_file)
|
||||
assert caplog.records[-2].levelno == logging.INFO
|
||||
assert (
|
||||
caplog.records[-1].message == "WatchGodReload detected a new reload "
|
||||
f"dir '{app_first_dir.name}' in '{tmp_path}'; Adding to watch list."
|
||||
)
|
||||
|
||||
reloader.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
|
||||
def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
|
||||
def test_should_watch_cwd(mocker: MockerFixture, reload_directory_structure: Path):
|
||||
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
|
||||
app_dir = reload_directory_structure / "app"
|
||||
app_first_dir = reload_directory_structure / "app_first"
|
||||
|
||||
with as_cwd(reload_directory_structure):
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app",
|
||||
reload=True,
|
||||
reload_dirs=[str(app_dir), str(app_first_dir)],
|
||||
)
|
||||
WatchFilesReload(config, target=run, sockets=[])
|
||||
mock_watch.assert_called_once()
|
||||
assert mock_watch.call_args[0] == (Path.cwd(),)
|
||||
config = Config(app="tests.test_config:asgi_app", reload=True, reload_dirs=[])
|
||||
WatchFilesReload(config, target=run, sockets=[])
|
||||
mock_watch.assert_called_once()
|
||||
assert mock_watch.call_args[0] == (Path.cwd(),)
|
||||
|
||||
|
||||
@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
|
||||
def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structure):
|
||||
def test_should_watch_multiple_dirs(mocker: MockerFixture, reload_directory_structure: Path):
|
||||
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
|
||||
app_dir = reload_directory_structure / "app"
|
||||
app_first_dir = reload_directory_structure / "app_first"
|
||||
@ -369,11 +329,10 @@ def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structu
|
||||
assert set(mock_watch.call_args[0]) == {
|
||||
app_dir,
|
||||
app_first_dir,
|
||||
Path.cwd(),
|
||||
}
|
||||
|
||||
|
||||
def test_display_path_relative(tmp_path):
|
||||
def test_display_path_relative(tmp_path: Path):
|
||||
with as_cwd(tmp_path):
|
||||
p = tmp_path / "app" / "foobar.py"
|
||||
# accept windows paths as wells as posix
|
||||
@ -385,8 +344,8 @@ def test_display_path_non_relative():
|
||||
assert _display_path(p) in ("'/foo/bar.py'", "'\\foo\\bar.py'")
|
||||
|
||||
|
||||
def test_base_reloader_run(tmp_path):
|
||||
calls = []
|
||||
def test_base_reloader_run(tmp_path: Path):
|
||||
calls: list[str] = []
|
||||
step = 0
|
||||
|
||||
class CustomReload(BaseReload):
|
||||
@ -416,7 +375,7 @@ def test_base_reloader_run(tmp_path):
|
||||
assert calls == ["startup", "restart", "shutdown"]
|
||||
|
||||
|
||||
def test_base_reloader_should_exit(tmp_path):
|
||||
def test_base_reloader_should_exit(tmp_path: Path):
|
||||
config = Config(app="tests.test_config:asgi_app", reload=True)
|
||||
reloader = BaseReload(config, target=run, sockets=[])
|
||||
assert not reloader.should_exit.is_set()
|
||||
|
||||
@ -5,7 +5,7 @@ from asyncio import Event
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.utils import run_server
|
||||
from tests.utils import assert_signal, run_server
|
||||
from uvicorn import Server
|
||||
from uvicorn.config import Config
|
||||
|
||||
@ -29,12 +29,10 @@ async def test_sigint_finish_req(unused_tcp_port: int):
|
||||
await server_event.wait()
|
||||
await send({"type": "http.response.body", "body": b"end", "more_body": False})
|
||||
|
||||
config = Config(
|
||||
app=wait_app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1
|
||||
)
|
||||
config = Config(app=wait_app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1)
|
||||
server: Server
|
||||
async with run_server(config) as server:
|
||||
async with httpx.AsyncClient() as client:
|
||||
with assert_signal(signal.SIGINT):
|
||||
async with run_server(config) as server, httpx.AsyncClient() as client:
|
||||
req = asyncio.create_task(client.get(f"http://127.0.0.1:{unused_tcp_port}"))
|
||||
await asyncio.sleep(0.1) # ensure next tick
|
||||
server.handle_exit(sig=signal.SIGINT, frame=None) # exit
|
||||
@ -42,6 +40,7 @@ async def test_sigint_finish_req(unused_tcp_port: int):
|
||||
# ensure httpx has processed the response and result is complete
|
||||
await req
|
||||
assert req.result().status_code == 200
|
||||
await asyncio.sleep(0.1) # ensure shutdown is complete
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@ -62,14 +61,12 @@ async def test_sigint_abort_req(unused_tcp_port: int, caplog):
|
||||
await send({"type": "http.response.body", "body": b"start", "more_body": True})
|
||||
# we never continue this one, so this request will time out
|
||||
await server_event.wait()
|
||||
await send({"type": "http.response.body", "body": b"end", "more_body": False})
|
||||
await send({"type": "http.response.body", "body": b"end", "more_body": False}) # pragma: full coverage
|
||||
|
||||
config = Config(
|
||||
app=forever_app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1
|
||||
)
|
||||
config = Config(app=forever_app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1)
|
||||
server: Server
|
||||
async with run_server(config) as server:
|
||||
async with httpx.AsyncClient() as client:
|
||||
with assert_signal(signal.SIGINT):
|
||||
async with run_server(config) as server, httpx.AsyncClient() as client:
|
||||
req = asyncio.create_task(client.get(f"http://127.0.0.1:{unused_tcp_port}"))
|
||||
await asyncio.sleep(0.1) # next tick
|
||||
# trigger exit, this request should time out in ~1 sec
|
||||
@ -78,10 +75,7 @@ async def test_sigint_abort_req(unused_tcp_port: int, caplog):
|
||||
await req
|
||||
|
||||
# req.result()
|
||||
assert (
|
||||
"Cancel 1 running task(s), timeout graceful shutdown exceeded"
|
||||
in caplog.messages
|
||||
)
|
||||
assert "Cancel 1 running task(s), timeout graceful shutdown exceeded" in caplog.messages
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@ -97,16 +91,14 @@ async def test_sigint_deny_request_after_triggered(unused_tcp_port: int, caplog)
|
||||
|
||||
async def app(scope, receive, send):
|
||||
await send({"type": "http.response.start", "status": 200, "headers": []})
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(1) # pragma: full coverage
|
||||
|
||||
config = Config(
|
||||
app=app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1
|
||||
)
|
||||
config = Config(app=app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1)
|
||||
server: Server
|
||||
async with run_server(config) as server:
|
||||
# exit and ensure we do not accept more requests
|
||||
server.handle_exit(sig=signal.SIGINT, frame=None)
|
||||
await asyncio.sleep(0.1) # next tick
|
||||
async with httpx.AsyncClient() as client:
|
||||
with assert_signal(signal.SIGINT):
|
||||
async with run_server(config) as server, httpx.AsyncClient() as client:
|
||||
# exit and ensure we do not accept more requests
|
||||
server.handle_exit(sig=signal.SIGINT, frame=None)
|
||||
await asyncio.sleep(0.1) # next tick
|
||||
with pytest.raises(httpx.ConnectError):
|
||||
await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.loops.auto import auto_loop_setup
|
||||
from uvicorn.main import ServerState
|
||||
from uvicorn.loops.auto import auto_loop_factory
|
||||
from uvicorn.protocols.http.auto import AutoHTTPProtocol
|
||||
from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
try:
|
||||
importlib.import_module("uvloop")
|
||||
@ -32,24 +33,18 @@ async def app(scope, receive, send):
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
# TODO: Add pypy to our testing matrix, and assert we get the correct classes
|
||||
# dependent on the platform we're running the tests under.
|
||||
|
||||
|
||||
def test_loop_auto():
|
||||
auto_loop_setup()
|
||||
policy = asyncio.get_event_loop_policy()
|
||||
assert isinstance(policy, asyncio.events.BaseDefaultEventLoopPolicy)
|
||||
assert type(policy).__module__.startswith(expected_loop)
|
||||
loop_factory = auto_loop_factory(use_subprocess=True)
|
||||
with contextlib.closing(loop_factory()) as loop:
|
||||
assert isinstance(loop, asyncio.AbstractEventLoop)
|
||||
assert type(loop).__module__.startswith(expected_loop)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_http_auto():
|
||||
config = Config(app=app)
|
||||
server_state = ServerState()
|
||||
protocol = AutoHTTPProtocol( # type: ignore[call-arg]
|
||||
config=config, server_state=server_state, app_state={}
|
||||
)
|
||||
protocol = AutoHTTPProtocol(config=config, server_state=server_state, app_state={})
|
||||
assert type(protocol).__name__ == expected_http
|
||||
|
||||
|
||||
@ -59,7 +54,5 @@ async def test_websocket_auto():
|
||||
server_state = ServerState()
|
||||
|
||||
assert AutoWebSocketsProtocol is not None
|
||||
protocol = AutoWebSocketsProtocol(
|
||||
config=config, server_state=server_state, app_state={}
|
||||
)
|
||||
protocol = AutoWebSocketsProtocol(config=config, server_state=server_state, app_state={})
|
||||
assert type(protocol).__name__ == expected_websockets
|
||||
|
||||
@ -3,9 +3,9 @@ import importlib
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Iterator
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@ -41,12 +41,11 @@ def test_cli_print_version() -> None:
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert (
|
||||
"Running uvicorn %s with %s %s on %s"
|
||||
% (
|
||||
uvicorn.__version__,
|
||||
platform.python_implementation(),
|
||||
platform.python_version(),
|
||||
platform.system(),
|
||||
"Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032
|
||||
version=uvicorn.__version__,
|
||||
py_implementation=platform.python_implementation(),
|
||||
py_version=platform.python_version(),
|
||||
system=platform.system(),
|
||||
)
|
||||
) in result.output
|
||||
|
||||
@ -103,9 +102,7 @@ def test_cli_call_multiprocess_run() -> None:
|
||||
|
||||
|
||||
@pytest.fixture(params=(True, False))
|
||||
def uds_file(
|
||||
tmp_path: Path, request: pytest.FixtureRequest
|
||||
) -> Path: # pragma: py-win32
|
||||
def uds_file(tmp_path: Path, request: pytest.FixtureRequest) -> Path: # pragma: py-win32
|
||||
file = tmp_path / "uvicorn.sock"
|
||||
should_create_file = request.param
|
||||
if should_create_file:
|
||||
@ -119,9 +116,7 @@ def test_cli_uds(uds_file: Path) -> None: # pragma: py-win32
|
||||
|
||||
with mock.patch.object(Config, "bind_socket") as mock_bind_socket:
|
||||
with mock.patch.object(Multiprocess, "run") as mock_run:
|
||||
result = runner.invoke(
|
||||
cli, ["tests.test_cli:App", "--workers=2", "--uds", str(uds_file)]
|
||||
)
|
||||
result = runner.invoke(cli, ["tests.test_cli:App", "--workers=2", "--uds", str(uds_file)])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert result.output == ""
|
||||
@ -136,8 +131,7 @@ def test_cli_incomplete_app_parameter() -> None:
|
||||
result = runner.invoke(cli, ["tests.test_cli"])
|
||||
|
||||
assert (
|
||||
'Error loading ASGI app. Import string "tests.test_cli" '
|
||||
'must be in format "<module>:<attribute>".'
|
||||
'Error loading ASGI app. Import string "tests.test_cli" must be in format "<module>:<attribute>".'
|
||||
) in result.output
|
||||
assert result.exit_code == 1
|
||||
|
||||
|
||||
28
tests/test_compat.py
Normal file
28
tests/test_compat.py
Normal file
@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from asyncio import AbstractEventLoop
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.custom_loop_utils import CustomLoop
|
||||
from tests.utils import get_asyncio_default_loop_per_os
|
||||
from uvicorn._compat import asyncio_run
|
||||
|
||||
|
||||
async def assert_event_loop(expected_loop_class: type[AbstractEventLoop]):
|
||||
assert isinstance(asyncio.get_running_loop(), expected_loop_class)
|
||||
|
||||
|
||||
def test_asyncio_run__default_loop_factory() -> None:
|
||||
asyncio_run(assert_event_loop(get_asyncio_default_loop_per_os()), loop_factory=None)
|
||||
|
||||
|
||||
def test_asyncio_run__custom_loop_factory() -> None:
|
||||
asyncio_run(assert_event_loop(CustomLoop), loop_factory=CustomLoop)
|
||||
|
||||
|
||||
def test_asyncio_run__passing_a_non_awaitable_callback_should_throw_error() -> None:
|
||||
# TypeError on Python >= 3.14
|
||||
with pytest.raises((ValueError, TypeError)):
|
||||
asyncio_run(lambda: None, loop_factory=CustomLoop) # type: ignore
|
||||
@ -1,27 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import typing
|
||||
from collections.abc import Callable, Iterator
|
||||
from contextlib import closing
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
from typing import IO, Any, Literal
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from tests.utils import as_cwd
|
||||
from uvicorn._types import (
|
||||
ASGIApplication,
|
||||
ASGIReceiveCallable,
|
||||
ASGISendCallable,
|
||||
Environ,
|
||||
Scope,
|
||||
StartResponse,
|
||||
)
|
||||
from uvicorn.config import Config
|
||||
from tests.custom_loop_utils import CustomLoop
|
||||
from tests.utils import as_cwd, get_asyncio_default_loop_per_os
|
||||
from uvicorn._types import ASGIApplication, ASGIReceiveCallable, ASGISendCallable, Environ, Scope, StartResponse
|
||||
from uvicorn.config import Config, LoopFactoryType
|
||||
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||||
from uvicorn.middleware.wsgi import WSGIMiddleware
|
||||
from uvicorn.protocols.http.h11_impl import H11Protocol
|
||||
@ -42,9 +41,7 @@ def yaml_logging_config(logging_config: dict) -> str:
|
||||
return yaml.dump(logging_config)
|
||||
|
||||
|
||||
async def asgi_app(
|
||||
scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None:
|
||||
async def asgi_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
pass # pragma: nocover
|
||||
|
||||
|
||||
@ -56,64 +53,44 @@ def wsgi_app(environ: Environ, start_response: StartResponse) -> None:
|
||||
"app, expected_should_reload",
|
||||
[(asgi_app, False), ("tests.test_config:asgi_app", True)],
|
||||
)
|
||||
def test_config_should_reload_is_set(
|
||||
app: "ASGIApplication", expected_should_reload: bool
|
||||
) -> None:
|
||||
def test_config_should_reload_is_set(app: ASGIApplication, expected_should_reload: bool) -> None:
|
||||
config = Config(app=app, reload=True)
|
||||
assert config.reload is True
|
||||
assert config.should_reload is expected_should_reload
|
||||
|
||||
|
||||
def test_should_warn_on_invalid_reload_configuration(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
def test_should_warn_on_invalid_reload_configuration(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
||||
config_class = Config(app=asgi_app, reload_dirs=[str(tmp_path)])
|
||||
assert not config_class.should_reload
|
||||
assert len(caplog.records) == 1
|
||||
assert (
|
||||
caplog.records[-1].message
|
||||
== "Current configuration will not reload as not all conditions are met, "
|
||||
caplog.records[-1].message == "Current configuration will not reload as not all conditions are met, "
|
||||
"please refer to documentation."
|
||||
)
|
||||
|
||||
config_no_reload = Config(
|
||||
app="tests.test_config:asgi_app", reload_dirs=[str(tmp_path)]
|
||||
)
|
||||
config_no_reload = Config(app="tests.test_config:asgi_app", reload_dirs=[str(tmp_path)])
|
||||
assert not config_no_reload.should_reload
|
||||
assert len(caplog.records) == 2
|
||||
assert (
|
||||
caplog.records[-1].message
|
||||
== "Current configuration will not reload as not all conditions are met, "
|
||||
caplog.records[-1].message == "Current configuration will not reload as not all conditions are met, "
|
||||
"please refer to documentation."
|
||||
)
|
||||
|
||||
|
||||
def test_reload_dir_is_set(
|
||||
reload_directory_structure: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
def test_reload_dir_is_set(reload_directory_structure: Path, caplog: pytest.LogCaptureFixture) -> None:
|
||||
app_dir = reload_directory_structure / "app"
|
||||
with caplog.at_level(logging.INFO):
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app", reload=True, reload_dirs=[str(app_dir)]
|
||||
)
|
||||
config = Config(app="tests.test_config:asgi_app", reload=True, reload_dirs=[str(app_dir)])
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[
|
||||
-1
|
||||
].message == f"Will watch for changes in these directories: {[str(app_dir)]}"
|
||||
assert caplog.records[-1].message == f"Will watch for changes in these directories: {[str(app_dir)]}"
|
||||
assert config.reload_dirs == [app_dir]
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app", reload=True, reload_dirs=str(app_dir)
|
||||
)
|
||||
config = Config(app="tests.test_config:asgi_app", reload=True, reload_dirs=str(app_dir))
|
||||
assert config.reload_dirs == [app_dir]
|
||||
|
||||
|
||||
def test_non_existant_reload_dir_is_not_set(
|
||||
reload_directory_structure: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
def test_non_existant_reload_dir_is_not_set(reload_directory_structure: Path, caplog: pytest.LogCaptureFixture) -> None:
|
||||
with as_cwd(reload_directory_structure), caplog.at_level(logging.WARNING):
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app", reload=True, reload_dirs=["reload"]
|
||||
)
|
||||
config = Config(app="tests.test_config:asgi_app", reload=True, reload_dirs=["reload"])
|
||||
assert config.reload_dirs == [reload_directory_structure]
|
||||
assert (
|
||||
caplog.records[-1].message
|
||||
@ -128,9 +105,7 @@ def test_reload_subdir_removal(reload_directory_structure: Path) -> None:
|
||||
reload_dirs = [str(reload_directory_structure), "app", str(app_dir)]
|
||||
|
||||
with as_cwd(reload_directory_structure):
|
||||
config = Config(
|
||||
app="tests.test_config:asgi_app", reload=True, reload_dirs=reload_dirs
|
||||
)
|
||||
config = Config(app="tests.test_config:asgi_app", reload=True, reload_dirs=reload_dirs)
|
||||
assert config.reload_dirs == [reload_directory_structure]
|
||||
|
||||
|
||||
@ -187,9 +162,7 @@ def test_reload_excluded_subdirectories_are_removed(
|
||||
)
|
||||
assert frozenset(config.reload_dirs) == frozenset([reload_directory_structure])
|
||||
assert frozenset(config.reload_dirs_excludes) == frozenset([app_dir])
|
||||
assert frozenset(config.reload_excludes) == frozenset(
|
||||
[str(app_dir), str(app_sub_dir)]
|
||||
)
|
||||
assert frozenset(config.reload_excludes) == frozenset([str(app_dir), str(app_sub_dir)])
|
||||
|
||||
|
||||
def test_reload_includes_exclude_dir_patterns_are_matched(
|
||||
@ -208,13 +181,10 @@ def test_reload_includes_exclude_dir_patterns_are_matched(
|
||||
)
|
||||
assert len(caplog.records) == 1
|
||||
assert (
|
||||
caplog.records[-1].message
|
||||
== "Will watch for changes in these directories: "
|
||||
caplog.records[-1].message == "Will watch for changes in these directories: "
|
||||
f"{sorted([str(first_app_dir), str(second_app_dir)])}"
|
||||
)
|
||||
assert frozenset(config.reload_dirs) == frozenset(
|
||||
[first_app_dir, second_app_dir]
|
||||
)
|
||||
assert frozenset(config.reload_dirs) == frozenset([first_app_dir, second_app_dir])
|
||||
assert config.reload_includes == ["*/src"]
|
||||
|
||||
|
||||
@ -246,9 +216,7 @@ def test_app_unimportable_other(caplog: pytest.LogCaptureFixture) -> None:
|
||||
with pytest.raises(SystemExit):
|
||||
config.load()
|
||||
error_messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "uvicorn.error" and record.levelname == "ERROR"
|
||||
record.message for record in caplog.records if record.name == "uvicorn.error" and record.levelname == "ERROR"
|
||||
]
|
||||
assert (
|
||||
'Error loading ASGI app. Attribute "app" not found in module "tests.test_config".' # noqa: E501
|
||||
@ -257,7 +225,7 @@ def test_app_unimportable_other(caplog: pytest.LogCaptureFixture) -> None:
|
||||
|
||||
|
||||
def test_app_factory(caplog: pytest.LogCaptureFixture) -> None:
|
||||
def create_app() -> "ASGIApplication":
|
||||
def create_app() -> ASGIApplication:
|
||||
return asgi_app
|
||||
|
||||
config = Config(app=create_app, factory=True, proxy_headers=False)
|
||||
@ -318,21 +286,15 @@ def test_ssl_config_combined(tls_certificate_key_and_chain_path: str) -> None:
|
||||
assert config.is_ssl is True
|
||||
|
||||
|
||||
def asgi2_app(scope: "Scope") -> typing.Callable:
|
||||
async def asgi(
|
||||
receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None: # pragma: nocover
|
||||
def asgi2_app(scope: Scope) -> Callable:
|
||||
async def asgi(receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: # pragma: nocover
|
||||
pass
|
||||
|
||||
return asgi # pragma: nocover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"app, expected_interface", [(asgi_app, "3.0"), (asgi2_app, "2.0")]
|
||||
)
|
||||
def test_asgi_version(
|
||||
app: "ASGIApplication", expected_interface: Literal["2.0", "3.0"]
|
||||
) -> None:
|
||||
@pytest.mark.parametrize("app, expected_interface", [(asgi_app, "3.0"), (asgi2_app, "2.0")])
|
||||
def test_asgi_version(app: ASGIApplication, expected_interface: Literal["2.0", "3.0"]) -> None:
|
||||
config = Config(app=app)
|
||||
config.load()
|
||||
assert config.asgi_version == expected_interface
|
||||
@ -349,9 +311,9 @@ def test_asgi_version(
|
||||
)
|
||||
def test_log_config_default(
|
||||
mocked_logging_config_module: MagicMock,
|
||||
use_colors: typing.Optional[bool],
|
||||
expected: typing.Optional[bool],
|
||||
logging_config,
|
||||
use_colors: bool | None,
|
||||
expected: bool | None,
|
||||
logging_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""
|
||||
Test that one can specify the use_colors option when using the default logging
|
||||
@ -368,16 +330,14 @@ def test_log_config_default(
|
||||
|
||||
def test_log_config_json(
|
||||
mocked_logging_config_module: MagicMock,
|
||||
logging_config: dict,
|
||||
logging_config: dict[str, Any],
|
||||
json_logging_config: str,
|
||||
mocker: MockerFixture,
|
||||
) -> None:
|
||||
"""
|
||||
Test that one can load a json config from disk.
|
||||
"""
|
||||
mocked_open = mocker.patch(
|
||||
"uvicorn.config.open", mocker.mock_open(read_data=json_logging_config)
|
||||
)
|
||||
mocked_open = mocker.patch("uvicorn.config.open", mocker.mock_open(read_data=json_logging_config))
|
||||
|
||||
config = Config(app=asgi_app, log_config="log_config.json")
|
||||
config.load()
|
||||
@ -389,7 +349,7 @@ def test_log_config_json(
|
||||
@pytest.mark.parametrize("config_filename", ["log_config.yml", "log_config.yaml"])
|
||||
def test_log_config_yaml(
|
||||
mocked_logging_config_module: MagicMock,
|
||||
logging_config: dict,
|
||||
logging_config: dict[str, Any],
|
||||
yaml_logging_config: str,
|
||||
mocker: MockerFixture,
|
||||
config_filename: str,
|
||||
@ -397,9 +357,7 @@ def test_log_config_yaml(
|
||||
"""
|
||||
Test that one can load a yaml config from disk.
|
||||
"""
|
||||
mocked_open = mocker.patch(
|
||||
"uvicorn.config.open", mocker.mock_open(read_data=yaml_logging_config)
|
||||
)
|
||||
mocked_open = mocker.patch("uvicorn.config.open", mocker.mock_open(read_data=yaml_logging_config))
|
||||
|
||||
config = Config(app=asgi_app, log_config=config_filename)
|
||||
config.load()
|
||||
@ -408,27 +366,58 @@ def test_log_config_yaml(
|
||||
mocked_logging_config_module.dictConfig.assert_called_once_with(logging_config)
|
||||
|
||||
|
||||
def test_log_config_file(mocked_logging_config_module: MagicMock) -> None:
|
||||
def test_log_config_yaml_missing_pyyaml(mocked_logging_config_module: MagicMock, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
Test that a helpful error is raised when PyYAML is not installed.
|
||||
"""
|
||||
mocker.patch.dict(sys.modules, {"yaml": None})
|
||||
with pytest.raises(ImportError, match=r"Install the PyYAML package or uvicorn\[standard\]"):
|
||||
Config(app=asgi_app, log_config="log_config.yaml")
|
||||
|
||||
|
||||
def test_log_config_pathlike(
|
||||
mocked_logging_config_module: MagicMock,
|
||||
logging_config: dict[str, Any],
|
||||
json_logging_config: str,
|
||||
mocker: MockerFixture,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""
|
||||
Test that one can pass a `os.PathLike` (e.g. `pathlib.Path`) as the log config path.
|
||||
"""
|
||||
path = tmp_path / "log_config.json"
|
||||
mocked_open = mocker.patch("uvicorn.config.open", mocker.mock_open(read_data=json_logging_config))
|
||||
|
||||
config = Config(app=asgi_app, log_config=path)
|
||||
config.load()
|
||||
|
||||
mocked_open.assert_called_once_with(os.fspath(path))
|
||||
mocked_logging_config_module.dictConfig.assert_called_once_with(logging_config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_file", ["log_config.ini", configparser.ConfigParser(), io.StringIO()])
|
||||
def test_log_config_file(
|
||||
mocked_logging_config_module: MagicMock,
|
||||
config_file: str | configparser.RawConfigParser | IO[Any],
|
||||
) -> None:
|
||||
"""
|
||||
Test that one can load a configparser config from disk.
|
||||
"""
|
||||
config = Config(app=asgi_app, log_config="log_config")
|
||||
config = Config(app=asgi_app, log_config=config_file)
|
||||
config.load()
|
||||
|
||||
mocked_logging_config_module.fileConfig.assert_called_once_with(
|
||||
"log_config", disable_existing_loggers=False
|
||||
)
|
||||
mocked_logging_config_module.fileConfig.assert_called_once_with(config_file, disable_existing_loggers=False)
|
||||
|
||||
|
||||
@pytest.fixture(params=[0, 1])
|
||||
def web_concurrency(request: pytest.FixtureRequest) -> typing.Iterator[int]:
|
||||
def web_concurrency(request: pytest.FixtureRequest) -> Iterator[int]:
|
||||
yield request.param
|
||||
if os.getenv("WEB_CONCURRENCY"):
|
||||
del os.environ["WEB_CONCURRENCY"]
|
||||
|
||||
|
||||
@pytest.fixture(params=["127.0.0.1", "127.0.0.2"])
|
||||
def forwarded_allow_ips(request: pytest.FixtureRequest) -> typing.Iterator[str]:
|
||||
def forwarded_allow_ips(request: pytest.FixtureRequest) -> Iterator[str]:
|
||||
yield request.param
|
||||
if os.getenv("FORWARDED_ALLOW_IPS"):
|
||||
del os.environ["FORWARDED_ALLOW_IPS"]
|
||||
@ -444,10 +433,7 @@ def test_env_file(
|
||||
Test that one can load environment variables using an env file.
|
||||
"""
|
||||
fp = tmp_path / ".env"
|
||||
content = (
|
||||
f"WEB_CONCURRENCY={web_concurrency}\n"
|
||||
f"FORWARDED_ALLOW_IPS={forwarded_allow_ips}\n"
|
||||
)
|
||||
content = f"WEB_CONCURRENCY={web_concurrency}\nFORWARDED_ALLOW_IPS={forwarded_allow_ips}\n"
|
||||
fp.write_text(content)
|
||||
with caplog.at_level(logging.INFO):
|
||||
config = Config(app=asgi_app, env_file=fp)
|
||||
@ -487,9 +473,7 @@ def test_config_log_level(log_level: int) -> None:
|
||||
|
||||
@pytest.mark.parametrize("log_level", [None, 0, 5, 10, 20, 30, 40, 50])
|
||||
@pytest.mark.parametrize("uvicorn_logger_level", [0, 5, 10, 20, 30, 40, 50])
|
||||
def test_config_log_effective_level(
|
||||
log_level: Optional[int], uvicorn_logger_level: Optional[int]
|
||||
) -> None:
|
||||
def test_config_log_effective_level(log_level: int, uvicorn_logger_level: int) -> None:
|
||||
default_level = 30
|
||||
log_config = {
|
||||
"version": 1,
|
||||
@ -507,6 +491,13 @@ def test_config_log_effective_level(
|
||||
assert logging.getLogger("uvicorn.asgi").getEffectiveLevel() == effective_level
|
||||
|
||||
|
||||
@pytest.mark.parametrize("log_level", ["INFO", "Info", "info"])
|
||||
def test_config_log_level_case_insensitive(log_level: str) -> None:
|
||||
config = Config(app=asgi_app, log_level=log_level)
|
||||
config.load()
|
||||
assert logging.getLogger("uvicorn.error").level == logging.INFO
|
||||
|
||||
|
||||
def test_ws_max_size() -> None:
|
||||
config = Config(app=asgi_app, ws_max_size=1000)
|
||||
config.load()
|
||||
@ -529,7 +520,7 @@ def test_ws_max_queue() -> None:
|
||||
)
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system")
|
||||
def test_bind_unix_socket_works_with_reload_or_workers(
|
||||
tmp_path, reload, workers, short_socket_name
|
||||
tmp_path: Path, reload: bool, workers: int, short_socket_name: str
|
||||
): # pragma: py-win32
|
||||
config = Config(app=asgi_app, uds=short_socket_name, reload=reload, workers=workers)
|
||||
config.load()
|
||||
@ -549,7 +540,7 @@ def test_bind_unix_socket_works_with_reload_or_workers(
|
||||
ids=["--reload=True --workers=1", "--reload=False --workers=2"],
|
||||
)
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system")
|
||||
def test_bind_fd_works_with_reload_or_workers(reload, workers): # pragma: py-win32
|
||||
def test_bind_fd_works_with_reload_or_workers(reload: bool, workers: int): # pragma: py-win32
|
||||
fdsock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
fd = fdsock.fileno()
|
||||
config = Config(app=asgi_app, fd=fd, reload=reload, workers=workers)
|
||||
@ -562,6 +553,37 @@ def test_bind_fd_works_with_reload_or_workers(reload, workers): # pragma: py-wi
|
||||
fdsock.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stdin_socket() -> Iterator[socket.socket]: # pragma: py-win32
|
||||
with closing(socket.socket(socket.AF_INET)) as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
saved_stdin = os.dup(0)
|
||||
os.dup2(sock.fileno(), 0)
|
||||
try:
|
||||
yield sock
|
||||
finally:
|
||||
os.dup2(saved_stdin, 0)
|
||||
os.close(saved_stdin)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reload, workers",
|
||||
[
|
||||
(True, 1),
|
||||
(False, 2),
|
||||
],
|
||||
ids=["--reload=True --workers=1", "--reload=False --workers=2"],
|
||||
)
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system")
|
||||
def test_bind_stdin_works_with_reload_or_workers(
|
||||
reload: bool, workers: int, stdin_socket: socket.socket
|
||||
): # pragma: py-win32
|
||||
config = Config(app=asgi_app, fd=0, reload=reload, workers=workers)
|
||||
config.load()
|
||||
with closing(config.bind_socket()) as sock:
|
||||
assert sock.getsockname() == stdin_socket.getsockname()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"reload, workers, expected",
|
||||
[
|
||||
@ -575,7 +597,7 @@ def test_bind_fd_works_with_reload_or_workers(reload, workers): # pragma: py-wi
|
||||
"--reload=False --workers=1",
|
||||
],
|
||||
)
|
||||
def test_config_use_subprocess(reload, workers, expected):
|
||||
def test_config_use_subprocess(reload: bool, workers: int, expected: bool):
|
||||
config = Config(app=asgi_app, reload=reload, workers=workers)
|
||||
config.load()
|
||||
assert config.use_subprocess == expected
|
||||
@ -584,7 +606,57 @@ def test_config_use_subprocess(reload, workers, expected):
|
||||
def test_warn_when_using_reload_and_workers(caplog: pytest.LogCaptureFixture) -> None:
|
||||
Config(app=asgi_app, reload=True, workers=2)
|
||||
assert len(caplog.records) == 1
|
||||
assert '"workers" flag is ignored when reloading is enabled.' in caplog.records[0].message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("loop_type", "expected_loop_factory"),
|
||||
[
|
||||
("none", None),
|
||||
("asyncio", get_asyncio_default_loop_per_os()),
|
||||
],
|
||||
)
|
||||
def test_get_loop_factory(loop_type: LoopFactoryType, expected_loop_factory: Any):
|
||||
config = Config(app=asgi_app, loop=loop_type)
|
||||
loop_factory = config.get_loop_factory()
|
||||
if loop_factory is None:
|
||||
assert expected_loop_factory is loop_factory
|
||||
else:
|
||||
loop = loop_factory()
|
||||
with closing(loop):
|
||||
assert loop is not None
|
||||
assert isinstance(loop, expected_loop_factory)
|
||||
|
||||
|
||||
def test_custom_loop__importable_custom_loop_setup_function() -> None:
|
||||
config = Config(app=asgi_app, loop="tests.custom_loop_utils:CustomLoop")
|
||||
config.load()
|
||||
loop_factory = config.get_loop_factory()
|
||||
assert loop_factory, "Loop factory should be set"
|
||||
event_loop = loop_factory()
|
||||
with closing(event_loop):
|
||||
assert event_loop is not None
|
||||
assert isinstance(event_loop, CustomLoop)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
|
||||
def test_custom_loop__not_importable_custom_loop_setup_function(caplog: pytest.LogCaptureFixture) -> None:
|
||||
config = Config(app=asgi_app, loop="tests.test_config:non_existing_setup_function")
|
||||
config.load()
|
||||
with pytest.raises(SystemExit):
|
||||
config.get_loop_factory()
|
||||
error_messages = [
|
||||
record.message for record in caplog.records if record.name == "uvicorn.error" and record.levelname == "ERROR"
|
||||
]
|
||||
assert (
|
||||
'"workers" flag is ignored when reloading is enabled.'
|
||||
in caplog.records[0].message
|
||||
'Error loading custom loop setup function. Attribute "non_existing_setup_function" not found in module "tests.test_config".' # noqa: E501
|
||||
== error_messages.pop(0)
|
||||
)
|
||||
|
||||
|
||||
def test_setup_event_loop_is_removed(caplog: pytest.LogCaptureFixture) -> None:
|
||||
config = Config(app=asgi_app)
|
||||
with pytest.raises(
|
||||
AttributeError, match="The `setup_event_loop` method was replaced by `get_loop_factory` in uvicorn 0.36.0."
|
||||
):
|
||||
config.setup_event_loop()
|
||||
|
||||
@ -1,17 +1,21 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.utils import run_server
|
||||
from uvicorn import Config
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
assert scope["type"] == "http"
|
||||
await send({"type": "http.response.start", "status": 200, "headers": []})
|
||||
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_default_default_headers(unused_tcp_port: int):
|
||||
config = Config(app=app, loop="asyncio", limit_max_requests=1, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
@ -20,84 +24,45 @@ async def test_default_default_headers(unused_tcp_port: int):
|
||||
assert response.headers["server"] == "uvicorn" and response.headers["date"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_override_server_header(unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
limit_max_requests=1,
|
||||
headers=[("Server", "over-ridden")],
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
headers: list[tuple[str, str]] = [("Server", "over-ridden")]
|
||||
config = Config(app=app, loop="asyncio", limit_max_requests=1, headers=headers, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert (
|
||||
response.headers["server"] == "over-ridden" and response.headers["date"]
|
||||
)
|
||||
assert response.headers["server"] == "over-ridden" and response.headers["date"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_disable_default_server_header(unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
limit_max_requests=1,
|
||||
server_header=False,
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
config = Config(app=app, loop="asyncio", limit_max_requests=1, server_header=False, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert "server" not in response.headers
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_override_server_header_multiple_times(unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
limit_max_requests=1,
|
||||
headers=[("Server", "over-ridden"), ("Server", "another-value")],
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
headers: list[tuple[str, str]] = [("Server", "over-ridden"), ("Server", "another-value")]
|
||||
config = Config(app=app, loop="asyncio", limit_max_requests=1, headers=headers, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert (
|
||||
response.headers["server"] == "over-ridden, another-value"
|
||||
and response.headers["date"]
|
||||
)
|
||||
assert response.headers["server"] == "over-ridden, another-value" and response.headers["date"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_add_additional_header(unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
limit_max_requests=1,
|
||||
headers=[("X-Additional", "new-value")],
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
headers: list[tuple[str, str]] = [("X-Additional", "new-value")]
|
||||
config = Config(app=app, loop="asyncio", limit_max_requests=1, headers=headers, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert (
|
||||
response.headers["x-additional"] == "new-value"
|
||||
and response.headers["server"] == "uvicorn"
|
||||
and response.headers["date"]
|
||||
)
|
||||
assert response.headers["x-additional"] == "new-value"
|
||||
assert response.headers["server"] == "uvicorn"
|
||||
assert response.headers["date"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_disable_default_date_header(unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
limit_max_requests=1,
|
||||
date_header=False,
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
config = Config(app=app, loop="asyncio", limit_max_requests=1, date_header=False, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
|
||||
@ -98,7 +98,7 @@ def test_lifespan_auto_with_error():
|
||||
lifespan = LifespanOn(config)
|
||||
|
||||
await lifespan.startup()
|
||||
assert lifespan.error_occured
|
||||
assert lifespan.error_occurred
|
||||
assert not lifespan.should_exit
|
||||
await lifespan.shutdown()
|
||||
|
||||
@ -117,7 +117,7 @@ def test_lifespan_on_with_error():
|
||||
lifespan = LifespanOn(config)
|
||||
|
||||
await lifespan.startup()
|
||||
assert lifespan.error_occured
|
||||
assert lifespan.error_occurred
|
||||
assert lifespan.should_exit
|
||||
await lifespan.shutdown()
|
||||
|
||||
@ -132,9 +132,7 @@ def test_lifespan_with_failed_startup(mode, raise_exception, caplog):
|
||||
async def app(scope, receive, send):
|
||||
message = await receive()
|
||||
assert message["type"] == "lifespan.startup"
|
||||
await send(
|
||||
{"type": "lifespan.startup.failed", "message": "the lifespan event failed"}
|
||||
)
|
||||
await send({"type": "lifespan.startup.failed", "message": "the lifespan event failed"})
|
||||
if raise_exception:
|
||||
# App should be able to re-raise an exception if startup failed.
|
||||
raise RuntimeError()
|
||||
@ -145,7 +143,7 @@ def test_lifespan_with_failed_startup(mode, raise_exception, caplog):
|
||||
|
||||
await lifespan.startup()
|
||||
assert lifespan.startup_failed
|
||||
assert lifespan.error_occured is raise_exception
|
||||
assert lifespan.error_occurred is raise_exception
|
||||
assert lifespan.should_exit
|
||||
await lifespan.shutdown()
|
||||
|
||||
@ -153,9 +151,7 @@ def test_lifespan_with_failed_startup(mode, raise_exception, caplog):
|
||||
loop.run_until_complete(test())
|
||||
loop.close()
|
||||
error_messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "uvicorn.error" and record.levelname == "ERROR"
|
||||
record.message for record in caplog.records if record.name == "uvicorn.error" and record.levelname == "ERROR"
|
||||
]
|
||||
assert "the lifespan event failed" in error_messages.pop(0)
|
||||
assert "Application startup failed. Exiting." in error_messages.pop(0)
|
||||
@ -175,7 +171,7 @@ def test_lifespan_scope_asgi3app():
|
||||
|
||||
await lifespan.startup()
|
||||
assert not lifespan.startup_failed
|
||||
assert not lifespan.error_occured
|
||||
assert not lifespan.error_occurred
|
||||
assert not lifespan.should_exit
|
||||
await lifespan.shutdown()
|
||||
|
||||
@ -218,9 +214,7 @@ def test_lifespan_with_failed_shutdown(mode, raise_exception, caplog):
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
message = await receive()
|
||||
assert message["type"] == "lifespan.shutdown"
|
||||
await send(
|
||||
{"type": "lifespan.shutdown.failed", "message": "the lifespan event failed"}
|
||||
)
|
||||
await send({"type": "lifespan.shutdown.failed", "message": "the lifespan event failed"})
|
||||
|
||||
if raise_exception:
|
||||
# App should be able to re-raise an exception if startup failed.
|
||||
@ -234,15 +228,13 @@ def test_lifespan_with_failed_shutdown(mode, raise_exception, caplog):
|
||||
assert not lifespan.startup_failed
|
||||
await lifespan.shutdown()
|
||||
assert lifespan.shutdown_failed
|
||||
assert lifespan.error_occured is raise_exception
|
||||
assert lifespan.error_occurred is raise_exception
|
||||
assert lifespan.should_exit
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(test())
|
||||
error_messages = [
|
||||
record.message
|
||||
for record in caplog.records
|
||||
if record.name == "uvicorn.error" and record.levelname == "ERROR"
|
||||
record.message for record in caplog.records if record.name == "uvicorn.error" and record.levelname == "ERROR"
|
||||
]
|
||||
assert "the lifespan event failed" in error_messages.pop(0)
|
||||
assert "Application shutdown failed. Exiting." in error_messages.pop(0)
|
||||
|
||||
@ -1,23 +1,31 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import socket
|
||||
import sys
|
||||
from logging import WARNING
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import uvicorn.server
|
||||
from tests.utils import run_server
|
||||
from uvicorn import Server
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.main import run
|
||||
from uvicorn.supervisors import Multiprocess
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
assert scope["type"] == "http"
|
||||
await send({"type": "http.response.start", "status": 204, "headers": []})
|
||||
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
||||
|
||||
|
||||
def _has_ipv6(host):
|
||||
def _has_ipv6(host: str):
|
||||
sock = None
|
||||
has_ipv6 = False
|
||||
if socket.has_ipv6:
|
||||
@ -32,7 +40,6 @@ def _has_ipv6(host):
|
||||
return has_ipv6
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@pytest.mark.parametrize(
|
||||
"host, url",
|
||||
[
|
||||
@ -47,31 +54,23 @@ def _has_ipv6(host):
|
||||
],
|
||||
)
|
||||
async def test_run(host, url: str, unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app, host=host, loop="asyncio", limit_max_requests=1, port=unused_tcp_port
|
||||
)
|
||||
config = Config(app=app, host=host, loop="asyncio", limit_max_requests=1, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{url}:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_multiprocess(unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app, loop="asyncio", workers=2, limit_max_requests=1, port=unused_tcp_port
|
||||
)
|
||||
config = Config(app=app, loop="asyncio", workers=2, limit_max_requests=1, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_reload(unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app, loop="asyncio", reload=True, limit_max_requests=1, port=unused_tcp_port
|
||||
)
|
||||
config = Config(app=app, loop="asyncio", reload=True, limit_max_requests=1, port=unused_tcp_port)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"http://127.0.0.1:{unused_tcp_port}")
|
||||
@ -85,11 +84,65 @@ def test_run_invalid_app_config_combination(caplog: pytest.LogCaptureFixture) ->
|
||||
assert caplog.records[-1].name == "uvicorn.error"
|
||||
assert caplog.records[-1].levelno == WARNING
|
||||
assert caplog.records[-1].message == (
|
||||
"You must pass the application as an import string to enable "
|
||||
"'reload' or 'workers'."
|
||||
"You must pass the application as an import string to enable 'reload' or 'workers'."
|
||||
)
|
||||
|
||||
|
||||
def test_run_fails_fast_in_parent_on_bad_app_path(
|
||||
caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Bad app path with `--workers > 1` exits in the parent.
|
||||
|
||||
Regression for https://github.com/encode/uvicorn/discussions/2440: without
|
||||
parent-side validation the supervisor restarts dying workers forever.
|
||||
"""
|
||||
|
||||
def fail(*args: object, **kwargs: object) -> None: # pragma: no cover
|
||||
pytest.fail("parent reached supervisor; should have exited on bad app path")
|
||||
|
||||
monkeypatch.setattr(Config, "bind_socket", fail)
|
||||
monkeypatch.setattr(Multiprocess, "run", fail)
|
||||
|
||||
with pytest.raises(SystemExit) as exit_exception:
|
||||
run("tests.test_main:nonexistent_attr", workers=2)
|
||||
assert exit_exception.value.code == 1
|
||||
assert any("Error loading ASGI app" in record.message for record in caplog.records)
|
||||
|
||||
|
||||
def test_run_imports_app_before_starting_event_loop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""`uvicorn.run()` imports the app before `Server.run` opens the event loop.
|
||||
|
||||
Regression for https://github.com/encode/uvicorn/issues/941: an app whose
|
||||
module body calls `asyncio.run(...)` crashes with "loop already running"
|
||||
if Uvicorn imports it inside the server's event loop. The parent must
|
||||
import the app synchronously, before `Server.run` enters `asyncio.run`.
|
||||
"""
|
||||
module = tmp_path / "eager_async_app.py"
|
||||
module.write_text(
|
||||
"import asyncio\n"
|
||||
"async def _build():\n"
|
||||
" async def app(scope, receive, send):\n"
|
||||
" pass\n"
|
||||
" return app\n"
|
||||
"app = asyncio.run(_build())\n"
|
||||
)
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
imported_before_server_run: list[bool] = []
|
||||
|
||||
def tracking_run(self: Server, sockets: object = None) -> None:
|
||||
imported_before_server_run.append("eager_async_app" in sys.modules)
|
||||
self.started = True
|
||||
|
||||
monkeypatch.setattr(Server, "run", tracking_run)
|
||||
|
||||
# The import side effect (`eager_async_app` lands in `sys.modules`) must
|
||||
# happen before `Server.run`, which is where the event loop opens.
|
||||
run("eager_async_app:app")
|
||||
|
||||
assert imported_before_server_run == [True]
|
||||
|
||||
|
||||
def test_run_startup_failure(caplog: pytest.LogCaptureFixture) -> None:
|
||||
async def app(scope, receive, send):
|
||||
assert scope["type"] == "lifespan"
|
||||
@ -109,17 +162,23 @@ def test_run_match_config_params() -> None:
|
||||
if key not in ("self", "timeout_notify", "callback_notify")
|
||||
}
|
||||
run_params = {
|
||||
key: repr(value)
|
||||
for key, value in inspect.signature(run).parameters.items()
|
||||
if key not in ("app_dir",)
|
||||
key: repr(value) for key, value in inspect.signature(run).parameters.items() if key not in ("app_dir",)
|
||||
}
|
||||
assert config_params == run_params
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_exit_on_create_server_with_invalid_host() -> None:
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
config = Config(app=app, host="illegal_host")
|
||||
server = Server(config=config)
|
||||
await server.serve()
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
|
||||
def test_deprecated_server_state_from_main() -> None:
|
||||
with pytest.deprecated_call(
|
||||
match="uvicorn.main.ServerState is deprecated, use uvicorn.server.ServerState instead."
|
||||
):
|
||||
main = importlib.import_module("uvicorn.main")
|
||||
server_state_cls = getattr(main, "ServerState")
|
||||
assert server_state_cls is uvicorn.server.ServerState
|
||||
|
||||
263
tests/test_server.py
Normal file
263
tests/test_server.py
Normal file
@ -0,0 +1,263 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import contextvars
|
||||
import json
|
||||
import logging
|
||||
import signal
|
||||
import sys
|
||||
from collections.abc import Callable, Generator
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.protocols.test_http import SIMPLE_GET_REQUEST
|
||||
from tests.utils import run_server
|
||||
from uvicorn._types import ASGIApplication, ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.protocols.http.flow_control import HIGH_WATER_LIMIT
|
||||
from uvicorn.protocols.http.h11_impl import H11Protocol
|
||||
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
||||
from uvicorn.server import Server
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
# asyncio does NOT allow raising in signal handlers, so to detect
|
||||
# raised signals raised a mutable `witness` receives the signal
|
||||
@contextlib.contextmanager
|
||||
def capture_signal_sync(sig: signal.Signals) -> Generator[list[int], None, None]:
|
||||
"""Replace `sig` handling with a normal exception via `signal"""
|
||||
witness: list[int] = []
|
||||
original_handler = signal.signal(sig, lambda signum, frame: witness.append(signum))
|
||||
yield witness
|
||||
signal.signal(sig, original_handler)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def capture_signal_async(sig: signal.Signals) -> Generator[list[int], None, None]: # pragma: py-win32
|
||||
"""Replace `sig` handling with a normal exception via `asyncio"""
|
||||
witness: list[int] = []
|
||||
original_handler = signal.getsignal(sig)
|
||||
asyncio.get_running_loop().add_signal_handler(sig, witness.append, sig)
|
||||
yield witness
|
||||
signal.signal(sig, original_handler)
|
||||
|
||||
|
||||
async def dummy_app(scope, receive, send): # pragma: py-win32
|
||||
pass
|
||||
|
||||
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
assert scope["type"] == "http"
|
||||
await send({"type": "http.response.start", "status": 200, "headers": []})
|
||||
await send({"type": "http.response.body", "body": b"", "more_body": False})
|
||||
|
||||
|
||||
if sys.platform == "win32": # pragma: py-not-win32
|
||||
signals = [signal.SIGBREAK]
|
||||
signal_captures = [capture_signal_sync]
|
||||
else: # pragma: py-win32
|
||||
signals = [signal.SIGTERM, signal.SIGINT]
|
||||
signal_captures = [capture_signal_sync, capture_signal_async]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exception_signal", signals)
|
||||
@pytest.mark.parametrize("capture_signal", signal_captures)
|
||||
async def test_server_interrupt(
|
||||
exception_signal: signal.Signals,
|
||||
capture_signal: Callable[[signal.Signals], AbstractContextManager[None]],
|
||||
unused_tcp_port: int,
|
||||
): # pragma: py-win32
|
||||
"""Test interrupting a Server that is run explicitly inside asyncio"""
|
||||
|
||||
async def interrupt_running(srv: Server):
|
||||
while not srv.started:
|
||||
await asyncio.sleep(0.01)
|
||||
signal.raise_signal(exception_signal)
|
||||
|
||||
server = Server(Config(app=dummy_app, loop="asyncio", port=unused_tcp_port))
|
||||
asyncio.create_task(interrupt_running(server))
|
||||
with capture_signal(exception_signal) as witness:
|
||||
await server.serve()
|
||||
assert witness
|
||||
# set by the server's graceful exit handler
|
||||
assert server.should_exit
|
||||
|
||||
|
||||
async def test_shutdown_on_early_exit_during_startup(unused_tcp_port: int):
|
||||
"""Test that lifespan.shutdown is called even when should_exit is set during startup."""
|
||||
startup_complete = False
|
||||
shutdown_complete = False
|
||||
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
nonlocal startup_complete, shutdown_complete
|
||||
if scope["type"] == "lifespan":
|
||||
while True:
|
||||
message = await receive()
|
||||
if message["type"] == "lifespan.startup":
|
||||
await asyncio.sleep(0.5)
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
startup_complete = True
|
||||
elif message["type"] == "lifespan.shutdown":
|
||||
await send({"type": "lifespan.shutdown.complete"})
|
||||
shutdown_complete = True
|
||||
return
|
||||
|
||||
config = Config(app=app, lifespan="on", port=unused_tcp_port)
|
||||
server = Server(config=config)
|
||||
|
||||
# Simulate a reload signal arriving during startup:
|
||||
# set should_exit before the 0.5s startup sleep finishes.
|
||||
async def set_exit():
|
||||
await asyncio.sleep(0.2)
|
||||
server.should_exit = True
|
||||
|
||||
asyncio.create_task(set_exit())
|
||||
await server.serve()
|
||||
|
||||
assert startup_complete
|
||||
assert shutdown_complete, "lifespan.shutdown was not called despite startup completing"
|
||||
|
||||
|
||||
async def test_request_than_limit_max_requests_warn_log(
|
||||
unused_tcp_port: int, http_protocol_cls: type[H11Protocol | HttpToolsProtocol], caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
caplog.set_level(logging.INFO, logger="uvicorn.error")
|
||||
config = Config(app=app, limit_max_requests=1, port=unused_tcp_port, http=http_protocol_cls)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient() as client:
|
||||
tasks = [client.get(f"http://127.0.0.1:{unused_tcp_port}") for _ in range(2)]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
assert len(responses) == 2
|
||||
assert "Maximum request limit of 1 exceeded. Terminating process." in caplog.text
|
||||
|
||||
|
||||
async def test_limit_max_requests_jitter(
|
||||
unused_tcp_port: int, http_protocol_cls: type[H11Protocol | HttpToolsProtocol], caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
caplog.set_level(logging.INFO, logger="uvicorn.error")
|
||||
config = Config(
|
||||
app=app, limit_max_requests=1, limit_max_requests_jitter=2, port=unused_tcp_port, http=http_protocol_cls
|
||||
)
|
||||
async with run_server(config) as server:
|
||||
limit = server.limit_max_requests
|
||||
assert limit is not None
|
||||
assert 1 <= limit <= 3
|
||||
async with httpx.AsyncClient() as client:
|
||||
tasks = [client.get(f"http://127.0.0.1:{unused_tcp_port}") for _ in range(limit + 1)]
|
||||
await asyncio.gather(*tasks)
|
||||
assert f"Maximum request limit of {limit} exceeded. Terminating process." in caplog.text
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def _raw_server(
|
||||
*,
|
||||
app: ASGIApplication,
|
||||
port: int,
|
||||
http_protocol_cls: type[H11Protocol | HttpToolsProtocol],
|
||||
reset_contextvars: bool = False,
|
||||
):
|
||||
config = Config(app=app, port=port, loop="asyncio", http=http_protocol_cls, reset_contextvars=reset_contextvars)
|
||||
server = Server(config=config)
|
||||
task = asyncio.create_task(server.serve())
|
||||
|
||||
while not server.started:
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
||||
|
||||
async def extract_json_body(request: bytes):
|
||||
writer.write(request)
|
||||
await writer.drain()
|
||||
|
||||
status, *headers = (await reader.readuntil(b"\r\n\r\n")).split(b"\r\n")[:-2]
|
||||
assert status == b"HTTP/1.1 200 OK"
|
||||
|
||||
content_length = next(int(h.split(b":", 1)[1]) for h in headers if h.lower().startswith(b"content-length:"))
|
||||
return json.loads(await reader.readexactly(content_length))
|
||||
|
||||
try:
|
||||
yield extract_json_body
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
server.should_exit = True
|
||||
await task
|
||||
|
||||
|
||||
async def test_contextvars_preserved_by_default(
|
||||
http_protocol_cls: type[H11Protocol | HttpToolsProtocol], unused_tcp_port: int
|
||||
):
|
||||
"""By default, context set outside the ASGI task is visible inside it."""
|
||||
ctx: contextvars.ContextVar[str] = contextvars.ContextVar("ctx")
|
||||
ctx.set("outer-value")
|
||||
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
||||
assert scope["type"] == "http"
|
||||
while True:
|
||||
message = await receive()
|
||||
assert message["type"] == "http.request"
|
||||
if not message["more_body"]:
|
||||
break
|
||||
body = json.dumps({"ctx": ctx.get("MISSING")}).encode("utf-8")
|
||||
headers = [(b"content-type", b"application/json"), (b"content-length", str(len(body)).encode("utf-8"))]
|
||||
await send({"type": "http.response.start", "status": 200, "headers": headers})
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
|
||||
async with _raw_server(app=app, http_protocol_cls=http_protocol_cls, port=unused_tcp_port) as extract_json_body:
|
||||
assert await extract_json_body(SIMPLE_GET_REQUEST) == {"ctx": "outer-value"}
|
||||
|
||||
|
||||
async def test_reset_contextvars_asyncio(
|
||||
http_protocol_cls: type[H11Protocol | HttpToolsProtocol], unused_tcp_port: int
|
||||
):
|
||||
"""With reset_contextvars=True, each ASGI run starts with a fresh context.
|
||||
|
||||
Non-regression test for https://github.com/encode/uvicorn/issues/2167.
|
||||
"""
|
||||
default_contextvars = {c.name for c in contextvars.copy_context().keys()}
|
||||
ctx: contextvars.ContextVar[str] = contextvars.ContextVar("ctx")
|
||||
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
|
||||
assert scope["type"] == "http"
|
||||
|
||||
# initial context should be empty
|
||||
initial_context = {
|
||||
n: v for c, v in contextvars.copy_context().items() if (n := c.name) not in default_contextvars
|
||||
}
|
||||
# set any contextvar before the body is read
|
||||
ctx.set(scope["path"])
|
||||
|
||||
while True:
|
||||
message = await receive()
|
||||
assert message["type"] == "http.request"
|
||||
if not message["more_body"]:
|
||||
break
|
||||
|
||||
body = json.dumps(initial_context).encode("utf-8")
|
||||
headers = [(b"content-type", b"application/json"), (b"content-length", str(len(body)).encode("utf-8"))]
|
||||
await send({"type": "http.response.start", "status": 200, "headers": headers})
|
||||
await send({"type": "http.response.body", "body": body})
|
||||
|
||||
# body larger than HIGH_WATER_LIMIT forces a reading pause on the main thread
|
||||
# and a resumption inside the ASGI task, which is where the original pollution showed up.
|
||||
large_body = b"a" * (HIGH_WATER_LIMIT + 1)
|
||||
large_request = b"\r\n".join(
|
||||
[
|
||||
b"POST /large-body HTTP/1.1",
|
||||
b"Host: example.org",
|
||||
b"Content-Type: application/octet-stream",
|
||||
f"Content-Length: {len(large_body)}".encode(),
|
||||
b"",
|
||||
large_body,
|
||||
]
|
||||
)
|
||||
|
||||
async with _raw_server(
|
||||
app=app, http_protocol_cls=http_protocol_cls, port=unused_tcp_port, reset_contextvars=True
|
||||
) as extract_json_body:
|
||||
assert await extract_json_body(large_request) == {}
|
||||
assert await extract_json_body(SIMPLE_GET_REQUEST) == {}
|
||||
@ -1,9 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
from collections.abc import Callable
|
||||
from typing import TypeAlias
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.utils import run_server
|
||||
from uvicorn.config import Config
|
||||
|
||||
DefaultFactory: TypeAlias = Callable[[], ssl.SSLContext]
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
assert scope["type"] == "http"
|
||||
@ -56,9 +64,7 @@ async def test_run_chain(
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_chain_only(
|
||||
tls_ca_ssl_context, tls_certificate_key_and_chain_path, unused_tcp_port: int
|
||||
):
|
||||
async def test_run_chain_only(tls_ca_ssl_context, tls_certificate_key_and_chain_path, unused_tcp_port: int):
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
@ -94,3 +100,108 @@ async def test_run_password(
|
||||
async with httpx.AsyncClient(verify=tls_ca_ssl_context) as client:
|
||||
response = await client.get(f"https://127.0.0.1:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_ssl_context_factory_default(
|
||||
tls_ca_ssl_context: ssl.SSLContext,
|
||||
tls_certificate_server_cert_path: str,
|
||||
tls_certificate_private_key_path: str,
|
||||
unused_tcp_port: int,
|
||||
) -> None:
|
||||
"""A factory that just delegates to the default factory should produce a working server."""
|
||||
|
||||
def ssl_context_factory(config: Config, default_ssl_context_factory: DefaultFactory) -> ssl.SSLContext:
|
||||
return default_ssl_context_factory()
|
||||
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
limit_max_requests=1,
|
||||
ssl_keyfile=tls_certificate_private_key_path,
|
||||
ssl_certfile=tls_certificate_server_cert_path,
|
||||
ssl_context_factory=ssl_context_factory,
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient(verify=tls_ca_ssl_context) as client:
|
||||
response = await client.get(f"https://127.0.0.1:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_ssl_context_factory_custom(
|
||||
tls_ca_ssl_context: ssl.SSLContext,
|
||||
tls_certificate_server_cert_path: str,
|
||||
tls_certificate_private_key_path: str,
|
||||
unused_tcp_port: int,
|
||||
) -> None:
|
||||
"""A factory that builds its own SSLContext from scratch should work without ssl_keyfile/ssl_certfile."""
|
||||
|
||||
def ssl_context_factory(config: Config, default_ssl_context_factory: DefaultFactory) -> ssl.SSLContext:
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ctx.load_cert_chain(tls_certificate_server_cert_path, tls_certificate_private_key_path)
|
||||
return ctx
|
||||
|
||||
config = Config(
|
||||
app=app,
|
||||
loop="asyncio",
|
||||
limit_max_requests=1,
|
||||
ssl_context_factory=ssl_context_factory,
|
||||
port=unused_tcp_port,
|
||||
)
|
||||
async with run_server(config):
|
||||
async with httpx.AsyncClient(verify=tls_ca_ssl_context) as client:
|
||||
response = await client.get(f"https://127.0.0.1:{unused_tcp_port}")
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_ssl_context_factory_mutates_default(
|
||||
tls_certificate_server_cert_path: str,
|
||||
tls_certificate_private_key_path: str,
|
||||
) -> None:
|
||||
"""The factory can call the default and mutate the result (e.g., bump TLS minimum version)."""
|
||||
|
||||
def ssl_context_factory(config: Config, default_ssl_context_factory: DefaultFactory) -> ssl.SSLContext:
|
||||
ctx = default_ssl_context_factory()
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
return ctx
|
||||
|
||||
config = Config(
|
||||
app=app,
|
||||
ssl_keyfile=tls_certificate_private_key_path,
|
||||
ssl_certfile=tls_certificate_server_cert_path,
|
||||
ssl_context_factory=ssl_context_factory,
|
||||
)
|
||||
config.load()
|
||||
assert config.is_ssl
|
||||
assert isinstance(config.ssl, ssl.SSLContext)
|
||||
assert config.ssl.minimum_version == ssl.TLSVersion.TLSv1_3
|
||||
|
||||
|
||||
def test_default_ssl_context_factory_requires_ssl_certfile() -> None:
|
||||
"""Calling `default_ssl_context_factory()` without `ssl_certfile` raises a clear error."""
|
||||
|
||||
def ssl_context_factory(config: Config, default_ssl_context_factory: DefaultFactory) -> ssl.SSLContext:
|
||||
return default_ssl_context_factory()
|
||||
|
||||
config = Config(app=app, ssl_context_factory=ssl_context_factory)
|
||||
with pytest.raises(RuntimeError, match="requires `ssl_certfile`"):
|
||||
config.load()
|
||||
|
||||
|
||||
def test_ssl_context_factory_must_return_ssl_context() -> None:
|
||||
def bad_factory(config: Config, default_ssl_context_factory: DefaultFactory) -> object:
|
||||
return "not an SSLContext"
|
||||
|
||||
config = Config(app=app, ssl_context_factory=bad_factory) # type: ignore[arg-type]
|
||||
with pytest.raises(TypeError, match="must return an `ssl.SSLContext`"):
|
||||
config.load()
|
||||
|
||||
|
||||
def test_is_ssl_true_when_only_factory_set() -> None:
|
||||
def ssl_context_factory(config: Config, default_ssl_context_factory: DefaultFactory) -> ssl.SSLContext:
|
||||
return ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) # pragma: no cover
|
||||
|
||||
config = Config(app=app, ssl_context_factory=ssl_context_factory)
|
||||
assert config.is_ssl is True
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from typing import List
|
||||
from unittest.mock import patch
|
||||
|
||||
from uvicorn._subprocess import SpawnProcess, get_subprocess, subprocess_started
|
||||
@ -7,13 +8,11 @@ from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
from uvicorn.config import Config
|
||||
|
||||
|
||||
def server_run(sockets: List[socket.socket]): # pragma: no cover
|
||||
def server_run(sockets: list[socket.socket]): # pragma: no cover
|
||||
...
|
||||
|
||||
|
||||
async def app(
|
||||
scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None: # pragma: no cover
|
||||
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: # pragma: no cover
|
||||
...
|
||||
|
||||
|
||||
|
||||
@ -1,16 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
|
||||
from uvicorn import Config, Server
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def run_server(config: Config, sockets=None):
|
||||
async def run_server(config: Config, sockets: list[socket] | None = None) -> AsyncIterator[Server]:
|
||||
server = Server(config=config)
|
||||
task = asyncio.create_task(server.serve(sockets=sockets))
|
||||
await asyncio.sleep(0.1)
|
||||
while not server.started:
|
||||
await asyncio.sleep(0.05)
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
@ -18,6 +25,18 @@ async def run_server(config: Config, sockets=None):
|
||||
task.cancel()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def assert_signal(sig: signal.Signals):
|
||||
"""Check that a signal was received and handled in a block"""
|
||||
seen: set[int] = set()
|
||||
prev_handler = signal.signal(sig, lambda num, frame: seen.add(num))
|
||||
try:
|
||||
yield
|
||||
assert sig in seen, f"process signal {signal.Signals(sig)!r} was not received or handled"
|
||||
finally:
|
||||
signal.signal(sig, prev_handler)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def as_cwd(path: Path):
|
||||
"""Changes working directory and returns to previous on exit."""
|
||||
@ -27,3 +46,11 @@ def as_cwd(path: Path):
|
||||
yield
|
||||
finally:
|
||||
os.chdir(prev_cwd)
|
||||
|
||||
|
||||
def get_asyncio_default_loop_per_os() -> type[asyncio.AbstractEventLoop]:
|
||||
"""Get the default asyncio loop per OS."""
|
||||
if sys.platform == "win32":
|
||||
return asyncio.ProactorEventLoop # type: ignore # pragma: nocover
|
||||
else:
|
||||
return asyncio.SelectorEventLoop # pragma: nocover
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
"""
|
||||
Look for a marker comment in docs pages, and place the output of
|
||||
`$ uvicorn --help` there. Pass `--check` to ensure the content is in sync.
|
||||
"""
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _get_usage_lines() -> typing.List[str]:
|
||||
res = subprocess.run(["uvicorn", "--help"], stdout=subprocess.PIPE)
|
||||
help_text = res.stdout.decode("utf-8")
|
||||
return ["```", "$ uvicorn --help", *help_text.splitlines(), "```"]
|
||||
|
||||
|
||||
def _find_next_codefence_lineno(lines: typing.List[str], after: int) -> int:
|
||||
return next(
|
||||
lineno for lineno, line in enumerate(lines[after:], after) if line == "```"
|
||||
)
|
||||
|
||||
|
||||
def _get_insert_location(lines: typing.List[str]) -> typing.Tuple[int, int]:
|
||||
marker = lines.index("<!-- :cli_usage: -->")
|
||||
start = marker + 1
|
||||
|
||||
if lines[start] == "```":
|
||||
# Already generated.
|
||||
# <!-- :cli_usage: -->
|
||||
# ``` <- start
|
||||
# [...]
|
||||
# ``` <- end
|
||||
next_codefence = _find_next_codefence_lineno(lines, after=start + 1)
|
||||
end = next_codefence + 1
|
||||
else:
|
||||
# Not generated yet.
|
||||
end = start
|
||||
|
||||
return start, end
|
||||
|
||||
|
||||
def _generate_cli_usage(path: Path, check: bool = False) -> int:
|
||||
content = path.read_text()
|
||||
|
||||
lines = content.splitlines()
|
||||
usage_lines = _get_usage_lines()
|
||||
start, end = _get_insert_location(lines)
|
||||
lines = lines[:start] + usage_lines + lines[end:]
|
||||
output = "\n".join(lines) + "\n"
|
||||
|
||||
if check:
|
||||
if content == output:
|
||||
return 0
|
||||
print(f"ERROR: CLI usage in {path} is out of sync. Run scripts/lint to fix.")
|
||||
return 1
|
||||
|
||||
path.write_text(output)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
paths = [Path("docs", "index.md"), Path("docs", "deployment.md")]
|
||||
rv = 0
|
||||
for path in paths:
|
||||
rv |= _generate_cli_usage(path, check=args.check)
|
||||
sys.exit(rv)
|
||||
@ -1,5 +1,5 @@
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.main import Server, main, run
|
||||
|
||||
__version__ = "0.24.0.post1"
|
||||
__version__ = "0.47.0"
|
||||
__all__ = ["main", "run", "Config", "Server"]
|
||||
|
||||
91
uvicorn/_compat.py
Normal file
91
uvicorn/_compat.py
Normal file
@ -0,0 +1,91 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, TypeVar
|
||||
|
||||
__all__ = ["asyncio_run", "iscoroutinefunction"]
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
from inspect import iscoroutinefunction
|
||||
else:
|
||||
from asyncio import iscoroutinefunction
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
asyncio_run = asyncio.run
|
||||
elif sys.version_info >= (3, 11):
|
||||
|
||||
def asyncio_run(
|
||||
main: Coroutine[Any, Any, _T],
|
||||
*,
|
||||
debug: bool = False,
|
||||
loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None,
|
||||
) -> _T:
|
||||
# asyncio.run from Python 3.12
|
||||
# https://docs.python.org/3/license.html#psf-license
|
||||
with asyncio.Runner(debug=debug, loop_factory=loop_factory) as runner:
|
||||
return runner.run(main)
|
||||
|
||||
else:
|
||||
# modified version of asyncio.run from Python 3.10 to add loop_factory kwarg
|
||||
# https://docs.python.org/3/license.html#psf-license
|
||||
def asyncio_run(
|
||||
main: Coroutine[Any, Any, _T],
|
||||
*,
|
||||
debug: bool = False,
|
||||
loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None,
|
||||
) -> _T:
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
else:
|
||||
raise RuntimeError("asyncio.run() cannot be called from a running event loop")
|
||||
|
||||
if not asyncio.iscoroutine(main):
|
||||
raise ValueError(f"a coroutine was expected, got {main!r}")
|
||||
|
||||
if loop_factory is None:
|
||||
loop = asyncio.new_event_loop()
|
||||
else:
|
||||
loop = loop_factory()
|
||||
try:
|
||||
if loop_factory is None:
|
||||
asyncio.set_event_loop(loop)
|
||||
if debug is not None:
|
||||
loop.set_debug(debug)
|
||||
return loop.run_until_complete(main)
|
||||
finally:
|
||||
try:
|
||||
_cancel_all_tasks(loop)
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.run_until_complete(loop.shutdown_default_executor())
|
||||
finally:
|
||||
if loop_factory is None:
|
||||
asyncio.set_event_loop(None)
|
||||
loop.close()
|
||||
|
||||
def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None:
|
||||
to_cancel = asyncio.all_tasks(loop)
|
||||
if not to_cancel:
|
||||
return
|
||||
|
||||
for task in to_cancel:
|
||||
task.cancel()
|
||||
|
||||
loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
|
||||
|
||||
for task in to_cancel:
|
||||
if task.cancelled():
|
||||
continue
|
||||
if task.exception() is not None:
|
||||
loop.call_exception_handler(
|
||||
{
|
||||
"message": "unhandled exception during asyncio.run() shutdown",
|
||||
"exception": task.exception(),
|
||||
"task": task,
|
||||
}
|
||||
)
|
||||
@ -2,12 +2,15 @@
|
||||
Some light wrappers around Python's multiprocessing, to deal with cleanly
|
||||
starting child processes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from multiprocessing.context import SpawnProcess
|
||||
from socket import socket
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from uvicorn.config import Config
|
||||
|
||||
@ -18,7 +21,7 @@ spawn = multiprocessing.get_context("spawn")
|
||||
def get_subprocess(
|
||||
config: Config,
|
||||
target: Callable[..., None],
|
||||
sockets: List[socket],
|
||||
sockets: list[socket],
|
||||
) -> SpawnProcess:
|
||||
"""
|
||||
Called in the parent process, to instantiate a new child process instance.
|
||||
@ -32,10 +35,10 @@ def get_subprocess(
|
||||
"""
|
||||
# We pass across the stdin fileno, and reopen it in the child process.
|
||||
# This is required for some debugging environments.
|
||||
stdin_fileno: Optional[int]
|
||||
try:
|
||||
stdin_fileno = sys.stdin.fileno()
|
||||
except OSError:
|
||||
# The `sys.stdin` can be `None`, see https://docs.python.org/3/library/sys.html#sys.__stdin__.
|
||||
except (AttributeError, OSError):
|
||||
stdin_fileno = None
|
||||
|
||||
kwargs = {
|
||||
@ -51,8 +54,8 @@ def get_subprocess(
|
||||
def subprocess_started(
|
||||
config: Config,
|
||||
target: Callable[..., None],
|
||||
sockets: List[socket],
|
||||
stdin_fileno: Optional[int],
|
||||
sockets: list[socket],
|
||||
stdin_fileno: int | None,
|
||||
) -> None:
|
||||
"""
|
||||
Called when the child process starts.
|
||||
@ -67,10 +70,15 @@ def subprocess_started(
|
||||
"""
|
||||
# Re-open stdin.
|
||||
if stdin_fileno is not None:
|
||||
sys.stdin = os.fdopen(stdin_fileno)
|
||||
sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage
|
||||
|
||||
# Logging needs to be setup again for each child.
|
||||
config.configure_logging()
|
||||
|
||||
# Now we can call into `Server.run(sockets=sockets)`
|
||||
target(sockets=sockets)
|
||||
try:
|
||||
# Now we can call into `Server.run(sockets=sockets)`
|
||||
target(sockets=sockets)
|
||||
except KeyboardInterrupt: # pragma: no cover
|
||||
# suppress the exception to avoid a traceback from subprocess.Popen
|
||||
# the parent already expects us to end, so no vital information is lost
|
||||
pass
|
||||
|
||||
@ -28,25 +28,12 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
MutableMapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
if sys.version_info >= (3, 8): # pragma: py-lt-38
|
||||
from typing import Literal, Protocol, TypedDict
|
||||
else: # pragma: py-gte-38
|
||||
from typing_extensions import Literal, Protocol, TypedDict
|
||||
from collections.abc import Awaitable, Callable, Iterable, MutableMapping
|
||||
from typing import Any, Literal, Protocol, TypedDict
|
||||
|
||||
if sys.version_info >= (3, 11): # pragma: py-lt-311
|
||||
from typing import NotRequired
|
||||
@ -55,15 +42,15 @@ else: # pragma: py-gte-311
|
||||
|
||||
# WSGI
|
||||
Environ = MutableMapping[str, Any]
|
||||
ExcInfo = Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]]
|
||||
StartResponse = Callable[[str, Iterable[Tuple[str, str]], Optional[ExcInfo]], None]
|
||||
WSGIApp = Callable[[Environ, StartResponse], Union[Iterable[bytes], BaseException]]
|
||||
ExcInfo = tuple[type[BaseException], BaseException, types.TracebackType | None]
|
||||
StartResponse = Callable[[str, Iterable[tuple[str, str]], ExcInfo | None], None]
|
||||
WSGIApp = Callable[[Environ, StartResponse], Iterable[bytes] | BaseException]
|
||||
|
||||
|
||||
# ASGI
|
||||
class ASGIVersions(TypedDict):
|
||||
spec_version: str
|
||||
version: Union[Literal["2.0"], Literal["3.0"]]
|
||||
version: Literal["2.0"] | Literal["3.0"]
|
||||
|
||||
|
||||
class HTTPScope(TypedDict):
|
||||
@ -76,11 +63,11 @@ class HTTPScope(TypedDict):
|
||||
raw_path: bytes
|
||||
query_string: bytes
|
||||
root_path: str
|
||||
headers: Iterable[Tuple[bytes, bytes]]
|
||||
client: Optional[Tuple[str, int]]
|
||||
server: Optional[Tuple[str, Optional[int]]]
|
||||
state: NotRequired[Dict[str, Any]]
|
||||
extensions: NotRequired[Dict[str, Dict[object, object]]]
|
||||
headers: Iterable[tuple[bytes, bytes]]
|
||||
client: tuple[str, int] | None
|
||||
server: tuple[str, int | None] | None
|
||||
state: NotRequired[dict[str, Any]]
|
||||
extensions: NotRequired[dict[str, dict[object, object]]]
|
||||
|
||||
|
||||
class WebSocketScope(TypedDict):
|
||||
@ -92,22 +79,22 @@ class WebSocketScope(TypedDict):
|
||||
raw_path: bytes
|
||||
query_string: bytes
|
||||
root_path: str
|
||||
headers: Iterable[Tuple[bytes, bytes]]
|
||||
client: Optional[Tuple[str, int]]
|
||||
server: Optional[Tuple[str, Optional[int]]]
|
||||
headers: Iterable[tuple[bytes, bytes]]
|
||||
client: tuple[str, int] | None
|
||||
server: tuple[str, int | None] | None
|
||||
subprotocols: Iterable[str]
|
||||
state: NotRequired[Dict[str, Any]]
|
||||
extensions: NotRequired[Dict[str, Dict[object, object]]]
|
||||
state: NotRequired[dict[str, Any]]
|
||||
extensions: NotRequired[dict[str, dict[object, object]]]
|
||||
|
||||
|
||||
class LifespanScope(TypedDict):
|
||||
type: Literal["lifespan"]
|
||||
asgi: ASGIVersions
|
||||
state: NotRequired[Dict[str, Any]]
|
||||
state: NotRequired[dict[str, Any]]
|
||||
|
||||
|
||||
WWWScope = Union[HTTPScope, WebSocketScope]
|
||||
Scope = Union[HTTPScope, WebSocketScope, LifespanScope]
|
||||
WWWScope = HTTPScope | WebSocketScope
|
||||
Scope = HTTPScope | WebSocketScope | LifespanScope
|
||||
|
||||
|
||||
class HTTPRequestEvent(TypedDict):
|
||||
@ -118,32 +105,32 @@ class HTTPRequestEvent(TypedDict):
|
||||
|
||||
class HTTPResponseDebugEvent(TypedDict):
|
||||
type: Literal["http.response.debug"]
|
||||
info: Dict[str, object]
|
||||
info: dict[str, object]
|
||||
|
||||
|
||||
class HTTPResponseStartEvent(TypedDict):
|
||||
type: Literal["http.response.start"]
|
||||
status: int
|
||||
headers: Iterable[Tuple[bytes, bytes]]
|
||||
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
||||
trailers: NotRequired[bool]
|
||||
|
||||
|
||||
class HTTPResponseBodyEvent(TypedDict):
|
||||
type: Literal["http.response.body"]
|
||||
body: bytes
|
||||
more_body: bool
|
||||
more_body: NotRequired[bool]
|
||||
|
||||
|
||||
class HTTPResponseTrailersEvent(TypedDict):
|
||||
type: Literal["http.response.trailers"]
|
||||
headers: Iterable[Tuple[bytes, bytes]]
|
||||
headers: Iterable[tuple[bytes, bytes]]
|
||||
more_trailers: bool
|
||||
|
||||
|
||||
class HTTPServerPushEvent(TypedDict):
|
||||
type: Literal["http.response.push"]
|
||||
path: str
|
||||
headers: Iterable[Tuple[bytes, bytes]]
|
||||
headers: Iterable[tuple[bytes, bytes]]
|
||||
|
||||
|
||||
class HTTPDisconnectEvent(TypedDict):
|
||||
@ -156,43 +143,62 @@ class WebSocketConnectEvent(TypedDict):
|
||||
|
||||
class WebSocketAcceptEvent(TypedDict):
|
||||
type: Literal["websocket.accept"]
|
||||
subprotocol: Optional[str]
|
||||
headers: Iterable[Tuple[bytes, bytes]]
|
||||
subprotocol: NotRequired[str | None]
|
||||
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
||||
|
||||
|
||||
class WebSocketReceiveEvent(TypedDict):
|
||||
class _WebSocketReceiveEventBytes(TypedDict):
|
||||
type: Literal["websocket.receive"]
|
||||
bytes: Optional[bytes]
|
||||
text: Optional[str]
|
||||
bytes: bytes
|
||||
text: NotRequired[None]
|
||||
|
||||
|
||||
class WebSocketSendEvent(TypedDict):
|
||||
class _WebSocketReceiveEventText(TypedDict):
|
||||
type: Literal["websocket.receive"]
|
||||
bytes: NotRequired[None]
|
||||
text: str
|
||||
|
||||
|
||||
WebSocketReceiveEvent = _WebSocketReceiveEventBytes | _WebSocketReceiveEventText
|
||||
|
||||
|
||||
class _WebSocketSendEventBytes(TypedDict):
|
||||
type: Literal["websocket.send"]
|
||||
bytes: Optional[bytes]
|
||||
text: Optional[str]
|
||||
bytes: bytes
|
||||
text: NotRequired[None]
|
||||
|
||||
|
||||
class _WebSocketSendEventText(TypedDict):
|
||||
type: Literal["websocket.send"]
|
||||
bytes: NotRequired[None]
|
||||
text: str
|
||||
|
||||
|
||||
WebSocketSendEvent = _WebSocketSendEventBytes | _WebSocketSendEventText
|
||||
|
||||
|
||||
class WebSocketResponseStartEvent(TypedDict):
|
||||
type: Literal["websocket.http.response.start"]
|
||||
status: int
|
||||
headers: Iterable[Tuple[bytes, bytes]]
|
||||
headers: Iterable[tuple[bytes, bytes]]
|
||||
|
||||
|
||||
class WebSocketResponseBodyEvent(TypedDict):
|
||||
type: Literal["websocket.http.response.body"]
|
||||
body: bytes
|
||||
more_body: bool
|
||||
more_body: NotRequired[bool]
|
||||
|
||||
|
||||
class WebSocketDisconnectEvent(TypedDict):
|
||||
type: Literal["websocket.disconnect"]
|
||||
code: int
|
||||
reason: NotRequired[str | None]
|
||||
|
||||
|
||||
class WebSocketCloseEvent(TypedDict):
|
||||
type: Literal["websocket.close"]
|
||||
code: int
|
||||
reason: Optional[str]
|
||||
code: NotRequired[int]
|
||||
reason: NotRequired[str | None]
|
||||
|
||||
|
||||
class LifespanStartupEvent(TypedDict):
|
||||
@ -221,38 +227,36 @@ class LifespanShutdownFailedEvent(TypedDict):
|
||||
message: str
|
||||
|
||||
|
||||
WebSocketEvent = Union[
|
||||
WebSocketReceiveEvent, WebSocketDisconnectEvent, WebSocketConnectEvent
|
||||
]
|
||||
WebSocketEvent = WebSocketReceiveEvent | WebSocketDisconnectEvent | WebSocketConnectEvent
|
||||
|
||||
|
||||
ASGIReceiveEvent = Union[
|
||||
HTTPRequestEvent,
|
||||
HTTPDisconnectEvent,
|
||||
WebSocketConnectEvent,
|
||||
WebSocketReceiveEvent,
|
||||
WebSocketDisconnectEvent,
|
||||
LifespanStartupEvent,
|
||||
LifespanShutdownEvent,
|
||||
]
|
||||
ASGIReceiveEvent = (
|
||||
HTTPRequestEvent
|
||||
| HTTPDisconnectEvent
|
||||
| WebSocketConnectEvent
|
||||
| WebSocketReceiveEvent
|
||||
| WebSocketDisconnectEvent
|
||||
| LifespanStartupEvent
|
||||
| LifespanShutdownEvent
|
||||
)
|
||||
|
||||
|
||||
ASGISendEvent = Union[
|
||||
HTTPResponseStartEvent,
|
||||
HTTPResponseBodyEvent,
|
||||
HTTPResponseTrailersEvent,
|
||||
HTTPServerPushEvent,
|
||||
HTTPDisconnectEvent,
|
||||
WebSocketAcceptEvent,
|
||||
WebSocketSendEvent,
|
||||
WebSocketResponseStartEvent,
|
||||
WebSocketResponseBodyEvent,
|
||||
WebSocketCloseEvent,
|
||||
LifespanStartupCompleteEvent,
|
||||
LifespanStartupFailedEvent,
|
||||
LifespanShutdownCompleteEvent,
|
||||
LifespanShutdownFailedEvent,
|
||||
]
|
||||
ASGISendEvent = (
|
||||
HTTPResponseStartEvent
|
||||
| HTTPResponseBodyEvent
|
||||
| HTTPResponseTrailersEvent
|
||||
| HTTPServerPushEvent
|
||||
| HTTPDisconnectEvent
|
||||
| WebSocketAcceptEvent
|
||||
| WebSocketSendEvent
|
||||
| WebSocketResponseStartEvent
|
||||
| WebSocketResponseBodyEvent
|
||||
| WebSocketCloseEvent
|
||||
| LifespanStartupCompleteEvent
|
||||
| LifespanStartupFailedEvent
|
||||
| LifespanShutdownCompleteEvent
|
||||
| LifespanShutdownFailedEvent
|
||||
)
|
||||
|
||||
|
||||
ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]]
|
||||
@ -260,22 +264,11 @@ ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]]
|
||||
|
||||
|
||||
class ASGI2Protocol(Protocol):
|
||||
def __init__(self, scope: Scope) -> None:
|
||||
... # pragma: no cover
|
||||
def __init__(self, scope: Scope) -> None: ... # pragma: no cover
|
||||
|
||||
async def __call__(
|
||||
self, receive: ASGIReceiveCallable, send: ASGISendCallable
|
||||
) -> None:
|
||||
... # pragma: no cover
|
||||
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover
|
||||
|
||||
|
||||
ASGI2Application = Type[ASGI2Protocol]
|
||||
ASGI3Application = Callable[
|
||||
[
|
||||
Scope,
|
||||
ASGIReceiveCallable,
|
||||
ASGISendCallable,
|
||||
],
|
||||
Awaitable[None],
|
||||
]
|
||||
ASGIApplication = Union[ASGI2Application, ASGI3Application]
|
||||
ASGI2Application = type[ASGI2Protocol]
|
||||
ASGI3Application = Callable[[Scope, ASGIReceiveCallable, ASGISendCallable], Awaitable[None]]
|
||||
ASGIApplication = ASGI2Application | ASGI3Application
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
@ -7,22 +9,14 @@ import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable
|
||||
from configparser import RawConfigParser
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from typing import IO, Any, Literal
|
||||
|
||||
import click
|
||||
|
||||
from uvicorn._compat import iscoroutinefunction
|
||||
from uvicorn._types import ASGIApplication
|
||||
from uvicorn.importer import ImportFromStringError, import_from_string
|
||||
from uvicorn.logging import TRACE_LOG_LEVEL
|
||||
@ -32,12 +26,12 @@ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
||||
from uvicorn.middleware.wsgi import WSGIMiddleware
|
||||
|
||||
HTTPProtocolType = Literal["auto", "h11", "httptools"]
|
||||
WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
|
||||
WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"]
|
||||
LifespanType = Literal["auto", "on", "off"]
|
||||
LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
|
||||
LoopFactoryType = Literal["none", "auto", "asyncio", "uvloop"]
|
||||
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
|
||||
|
||||
LOG_LEVELS: Dict[str, int] = {
|
||||
LOG_LEVELS: dict[str, int] = {
|
||||
"critical": logging.CRITICAL,
|
||||
"error": logging.ERROR,
|
||||
"warning": logging.WARNING,
|
||||
@ -45,33 +39,34 @@ LOG_LEVELS: Dict[str, int] = {
|
||||
"debug": logging.DEBUG,
|
||||
"trace": TRACE_LOG_LEVEL,
|
||||
}
|
||||
HTTP_PROTOCOLS: Dict[HTTPProtocolType, str] = {
|
||||
HTTP_PROTOCOLS: dict[str, str] = {
|
||||
"auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol",
|
||||
"h11": "uvicorn.protocols.http.h11_impl:H11Protocol",
|
||||
"httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol",
|
||||
}
|
||||
WS_PROTOCOLS: Dict[WSProtocolType, Optional[str]] = {
|
||||
WS_PROTOCOLS: dict[str, str | None] = {
|
||||
"auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol",
|
||||
"none": None,
|
||||
"websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
|
||||
"websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol",
|
||||
"wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
|
||||
}
|
||||
LIFESPAN: Dict[LifespanType, str] = {
|
||||
LIFESPAN: dict[str, str] = {
|
||||
"auto": "uvicorn.lifespan.on:LifespanOn",
|
||||
"on": "uvicorn.lifespan.on:LifespanOn",
|
||||
"off": "uvicorn.lifespan.off:LifespanOff",
|
||||
}
|
||||
LOOP_SETUPS: Dict[LoopSetupType, Optional[str]] = {
|
||||
LOOP_FACTORIES: dict[str, str | None] = {
|
||||
"none": None,
|
||||
"auto": "uvicorn.loops.auto:auto_loop_setup",
|
||||
"asyncio": "uvicorn.loops.asyncio:asyncio_setup",
|
||||
"uvloop": "uvicorn.loops.uvloop:uvloop_setup",
|
||||
"auto": "uvicorn.loops.auto:auto_loop_factory",
|
||||
"asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory",
|
||||
"uvloop": "uvicorn.loops.uvloop:uvloop_loop_factory",
|
||||
}
|
||||
INTERFACES: List[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"]
|
||||
INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"]
|
||||
|
||||
SSL_PROTOCOL_VERSION: int = ssl.PROTOCOL_TLS_SERVER
|
||||
|
||||
LOGGING_CONFIG: Dict[str, Any] = {
|
||||
LOGGING_CONFIG: dict[str, Any] = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
@ -108,13 +103,13 @@ logger = logging.getLogger("uvicorn.error")
|
||||
|
||||
|
||||
def create_ssl_context(
|
||||
certfile: Union[str, os.PathLike],
|
||||
keyfile: Optional[Union[str, os.PathLike]],
|
||||
password: Optional[str],
|
||||
certfile: str | os.PathLike[str],
|
||||
keyfile: str | os.PathLike[str] | None,
|
||||
password: str | None,
|
||||
ssl_version: int,
|
||||
cert_reqs: int,
|
||||
ca_certs: Optional[Union[str, os.PathLike]],
|
||||
ciphers: Optional[str],
|
||||
ca_certs: str | os.PathLike[str] | None,
|
||||
ciphers: str | None,
|
||||
) -> ssl.SSLContext:
|
||||
ctx = ssl.SSLContext(ssl_version)
|
||||
get_password = (lambda: password) if password else None
|
||||
@ -132,22 +127,20 @@ def is_dir(path: Path) -> bool:
|
||||
if not path.is_absolute():
|
||||
path = path.resolve()
|
||||
return path.is_dir()
|
||||
except OSError:
|
||||
except OSError: # pragma: full coverage
|
||||
return False
|
||||
|
||||
|
||||
def resolve_reload_patterns(
|
||||
patterns_list: List[str], directories_list: List[str]
|
||||
) -> Tuple[List[str], List[Path]]:
|
||||
directories: List[Path] = list(set(map(Path, directories_list.copy())))
|
||||
patterns: List[str] = patterns_list.copy()
|
||||
def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str]) -> tuple[list[str], list[Path]]:
|
||||
directories: list[Path] = list(set(map(Path, directories_list.copy())))
|
||||
patterns: list[str] = patterns_list.copy()
|
||||
|
||||
current_working_directory = Path.cwd()
|
||||
for pattern in patterns_list:
|
||||
# Special case for the .* pattern, otherwise this would only match
|
||||
# hidden directories which is probably undesired
|
||||
if pattern == ".*":
|
||||
continue
|
||||
continue # pragma: py-not-linux
|
||||
patterns.append(pattern)
|
||||
if is_dir(Path(pattern)):
|
||||
directories.append(Path(pattern))
|
||||
@ -159,15 +152,13 @@ def resolve_reload_patterns(
|
||||
directories = list(set(directories))
|
||||
directories = list(map(Path, directories))
|
||||
directories = list(map(lambda x: x.resolve(), directories))
|
||||
directories = list(
|
||||
{reload_path for reload_path in directories if is_dir(reload_path)}
|
||||
)
|
||||
directories = list({reload_path for reload_path in directories if is_dir(reload_path)})
|
||||
|
||||
children = []
|
||||
for j in range(len(directories)):
|
||||
for k in range(j + 1, len(directories)):
|
||||
for k in range(j + 1, len(directories)): # pragma: full coverage
|
||||
if directories[j] in directories[k].parents:
|
||||
children.append(directories[k]) # pragma: py-darwin
|
||||
children.append(directories[k])
|
||||
elif directories[k] in directories[j].parents:
|
||||
children.append(directories[j])
|
||||
|
||||
@ -176,7 +167,7 @@ def resolve_reload_patterns(
|
||||
return list(set(patterns)), directories
|
||||
|
||||
|
||||
def _normalize_dirs(dirs: Union[List[str], str, None]) -> List[str]:
|
||||
def _normalize_dirs(dirs: list[str] | str | None) -> list[str]:
|
||||
if dirs is None:
|
||||
return []
|
||||
if isinstance(dirs, str):
|
||||
@ -187,54 +178,58 @@ def _normalize_dirs(dirs: Union[List[str], str, None]) -> List[str]:
|
||||
class Config:
|
||||
def __init__(
|
||||
self,
|
||||
app: Union["ASGIApplication", Callable[..., Any], str],
|
||||
app: ASGIApplication | Callable[..., Any] | str,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 8000,
|
||||
uds: Optional[str] = None,
|
||||
fd: Optional[int] = None,
|
||||
loop: LoopSetupType = "auto",
|
||||
http: Union[Type[asyncio.Protocol], HTTPProtocolType] = "auto",
|
||||
ws: Union[Type[asyncio.Protocol], WSProtocolType] = "auto",
|
||||
uds: str | None = None,
|
||||
fd: int | None = None,
|
||||
loop: LoopFactoryType | str = "auto",
|
||||
http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto",
|
||||
ws: type[asyncio.Protocol] | WSProtocolType | str = "auto",
|
||||
ws_max_size: int = 16 * 1024 * 1024,
|
||||
ws_max_queue: int = 32,
|
||||
ws_ping_interval: Optional[float] = 20.0,
|
||||
ws_ping_timeout: Optional[float] = 20.0,
|
||||
ws_ping_interval: float | None = 20.0,
|
||||
ws_ping_timeout: float | None = 20.0,
|
||||
ws_per_message_deflate: bool = True,
|
||||
lifespan: LifespanType = "auto",
|
||||
env_file: "str | os.PathLike[str] | None" = None,
|
||||
log_config: Optional[Union[Dict[str, Any], str]] = LOGGING_CONFIG,
|
||||
log_level: Optional[Union[str, int]] = None,
|
||||
env_file: str | os.PathLike[str] | None = None,
|
||||
log_config: dict[str, Any] | str | os.PathLike[str] | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
|
||||
log_level: str | int | None = None,
|
||||
access_log: bool = True,
|
||||
use_colors: Optional[bool] = None,
|
||||
use_colors: bool | None = None,
|
||||
interface: InterfaceType = "auto",
|
||||
reload: bool = False,
|
||||
reload_dirs: Optional[Union[List[str], str]] = None,
|
||||
reload_dirs: list[str] | str | None = None,
|
||||
reload_delay: float = 0.25,
|
||||
reload_includes: Optional[Union[List[str], str]] = None,
|
||||
reload_excludes: Optional[Union[List[str], str]] = None,
|
||||
workers: Optional[int] = None,
|
||||
reload_includes: list[str] | str | None = None,
|
||||
reload_excludes: list[str] | str | None = None,
|
||||
workers: int | None = None,
|
||||
proxy_headers: bool = True,
|
||||
server_header: bool = True,
|
||||
date_header: bool = True,
|
||||
forwarded_allow_ips: Optional[Union[List[str], str]] = None,
|
||||
forwarded_allow_ips: list[str] | str | None = None,
|
||||
root_path: str = "",
|
||||
limit_concurrency: Optional[int] = None,
|
||||
limit_max_requests: Optional[int] = None,
|
||||
limit_concurrency: int | None = None,
|
||||
limit_max_requests: int | None = None,
|
||||
limit_max_requests_jitter: int = 0,
|
||||
backlog: int = 2048,
|
||||
timeout_keep_alive: int = 5,
|
||||
timeout_notify: int = 30,
|
||||
timeout_graceful_shutdown: Optional[int] = None,
|
||||
callback_notify: Optional[Callable[..., Awaitable[None]]] = None,
|
||||
ssl_keyfile: Optional[str] = None,
|
||||
ssl_certfile: "str | os.PathLike[str] | None" = None,
|
||||
ssl_keyfile_password: Optional[str] = None,
|
||||
timeout_graceful_shutdown: int | None = None,
|
||||
timeout_worker_healthcheck: int = 5,
|
||||
callback_notify: Callable[..., Awaitable[None]] | None = None,
|
||||
ssl_keyfile: str | os.PathLike[str] | None = None,
|
||||
ssl_certfile: str | os.PathLike[str] | None = None,
|
||||
ssl_keyfile_password: str | None = None,
|
||||
ssl_version: int = SSL_PROTOCOL_VERSION,
|
||||
ssl_cert_reqs: int = ssl.CERT_NONE,
|
||||
ssl_ca_certs: Optional[str] = None,
|
||||
ssl_ca_certs: str | os.PathLike[str] | None = None,
|
||||
ssl_ciphers: str = "TLSv1",
|
||||
headers: Optional[List[Tuple[str, str]]] = None,
|
||||
ssl_context_factory: Callable[[Config, Callable[[], ssl.SSLContext]], ssl.SSLContext] | None = None,
|
||||
headers: list[tuple[str, str]] | None = None,
|
||||
factory: bool = False,
|
||||
h11_max_incomplete_event_size: Optional[int] = None,
|
||||
h11_max_incomplete_event_size: int | None = None,
|
||||
reset_contextvars: bool = False,
|
||||
):
|
||||
self.app = app
|
||||
self.host = host
|
||||
@ -264,10 +259,12 @@ class Config:
|
||||
self.root_path = root_path
|
||||
self.limit_concurrency = limit_concurrency
|
||||
self.limit_max_requests = limit_max_requests
|
||||
self.limit_max_requests_jitter = limit_max_requests_jitter
|
||||
self.backlog = backlog
|
||||
self.timeout_keep_alive = timeout_keep_alive
|
||||
self.timeout_notify = timeout_notify
|
||||
self.timeout_graceful_shutdown = timeout_graceful_shutdown
|
||||
self.timeout_worker_healthcheck = timeout_worker_healthcheck
|
||||
self.callback_notify = callback_notify
|
||||
self.ssl_keyfile = ssl_keyfile
|
||||
self.ssl_certfile = ssl_certfile
|
||||
@ -276,25 +273,24 @@ class Config:
|
||||
self.ssl_cert_reqs = ssl_cert_reqs
|
||||
self.ssl_ca_certs = ssl_ca_certs
|
||||
self.ssl_ciphers = ssl_ciphers
|
||||
self.headers: List[Tuple[str, str]] = headers or []
|
||||
self.encoded_headers: List[Tuple[bytes, bytes]] = []
|
||||
self.ssl_context_factory = ssl_context_factory
|
||||
self.headers: list[tuple[str, str]] = headers or []
|
||||
self.encoded_headers: list[tuple[bytes, bytes]] = []
|
||||
self.factory = factory
|
||||
self.h11_max_incomplete_event_size = h11_max_incomplete_event_size
|
||||
self.reset_contextvars = reset_contextvars
|
||||
|
||||
self.loaded = False
|
||||
self.configure_logging()
|
||||
|
||||
self.reload_dirs: List[Path] = []
|
||||
self.reload_dirs_excludes: List[Path] = []
|
||||
self.reload_includes: List[str] = []
|
||||
self.reload_excludes: List[str] = []
|
||||
self.reload_dirs: list[Path] = []
|
||||
self.reload_dirs_excludes: list[Path] = []
|
||||
self.reload_includes: list[str] = []
|
||||
self.reload_excludes: list[str] = []
|
||||
|
||||
if (
|
||||
reload_dirs or reload_includes or reload_excludes
|
||||
) and not self.should_reload:
|
||||
if (reload_dirs or reload_includes or reload_excludes) and not self.should_reload:
|
||||
logger.warning(
|
||||
"Current configuration will not reload as not all conditions are met, "
|
||||
"please refer to documentation."
|
||||
"Current configuration will not reload as not all conditions are met, please refer to documentation."
|
||||
)
|
||||
|
||||
if self.should_reload:
|
||||
@ -302,30 +298,23 @@ class Config:
|
||||
reload_includes = _normalize_dirs(reload_includes)
|
||||
reload_excludes = _normalize_dirs(reload_excludes)
|
||||
|
||||
self.reload_includes, self.reload_dirs = resolve_reload_patterns(
|
||||
reload_includes, reload_dirs
|
||||
)
|
||||
self.reload_includes, self.reload_dirs = resolve_reload_patterns(reload_includes, reload_dirs)
|
||||
|
||||
self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(
|
||||
reload_excludes, []
|
||||
)
|
||||
self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(reload_excludes, [])
|
||||
|
||||
reload_dirs_tmp = self.reload_dirs.copy()
|
||||
|
||||
for directory in self.reload_dirs_excludes:
|
||||
for reload_directory in reload_dirs_tmp:
|
||||
if (
|
||||
directory == reload_directory
|
||||
or directory in reload_directory.parents
|
||||
):
|
||||
if directory == reload_directory or directory in reload_directory.parents:
|
||||
try:
|
||||
self.reload_dirs.remove(reload_directory)
|
||||
except ValueError:
|
||||
except ValueError: # pragma: full coverage
|
||||
pass
|
||||
|
||||
for pattern in self.reload_excludes:
|
||||
if pattern in self.reload_includes:
|
||||
self.reload_includes.remove(pattern)
|
||||
self.reload_includes.remove(pattern) # pragma: full coverage
|
||||
|
||||
if not self.reload_dirs:
|
||||
if reload_dirs:
|
||||
@ -334,7 +323,7 @@ class Config:
|
||||
+ "directories, watching current working directory.",
|
||||
reload_dirs,
|
||||
)
|
||||
self.reload_dirs = [Path(os.getcwd())]
|
||||
self.reload_dirs = [Path.cwd()]
|
||||
|
||||
logger.info(
|
||||
"Will watch for changes in these directories: %s",
|
||||
@ -350,20 +339,18 @@ class Config:
|
||||
if workers is None and "WEB_CONCURRENCY" in os.environ:
|
||||
self.workers = int(os.environ["WEB_CONCURRENCY"])
|
||||
|
||||
self.forwarded_allow_ips: Union[List[str], str]
|
||||
self.forwarded_allow_ips: list[str] | str
|
||||
if forwarded_allow_ips is None:
|
||||
self.forwarded_allow_ips = os.environ.get(
|
||||
"FORWARDED_ALLOW_IPS", "127.0.0.1"
|
||||
)
|
||||
self.forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1")
|
||||
else:
|
||||
self.forwarded_allow_ips = forwarded_allow_ips
|
||||
self.forwarded_allow_ips = forwarded_allow_ips # pragma: full coverage
|
||||
|
||||
if self.reload and self.workers > 1:
|
||||
logger.warning('"workers" flag is ignored when reloading is enabled.')
|
||||
|
||||
@property
|
||||
def asgi_version(self) -> Literal["2.0", "3.0"]:
|
||||
mapping: Dict[str, Literal["2.0", "3.0"]] = {
|
||||
mapping: dict[str, Literal["2.0", "3.0"]] = {
|
||||
"asgi2": "2.0",
|
||||
"asgi3": "3.0",
|
||||
"wsgi": "3.0",
|
||||
@ -372,7 +359,7 @@ class Config:
|
||||
|
||||
@property
|
||||
def is_ssl(self) -> bool:
|
||||
return bool(self.ssl_keyfile or self.ssl_certfile)
|
||||
return bool(self.ssl_keyfile or self.ssl_certfile or self.ssl_context_factory)
|
||||
|
||||
@property
|
||||
def use_subprocess(self) -> bool:
|
||||
@ -382,23 +369,25 @@ class Config:
|
||||
logging.addLevelName(TRACE_LOG_LEVEL, "TRACE")
|
||||
|
||||
if self.log_config is not None:
|
||||
if isinstance(self.log_config, os.PathLike):
|
||||
self.log_config = os.fspath(self.log_config)
|
||||
|
||||
if isinstance(self.log_config, dict):
|
||||
if self.use_colors in (True, False):
|
||||
self.log_config["formatters"]["default"][
|
||||
"use_colors"
|
||||
] = self.use_colors
|
||||
self.log_config["formatters"]["access"][
|
||||
"use_colors"
|
||||
] = self.use_colors
|
||||
self.log_config["formatters"]["default"]["use_colors"] = self.use_colors
|
||||
self.log_config["formatters"]["access"]["use_colors"] = self.use_colors
|
||||
logging.config.dictConfig(self.log_config)
|
||||
elif self.log_config.endswith(".json"):
|
||||
elif isinstance(self.log_config, str) and self.log_config.endswith(".json"):
|
||||
with open(self.log_config) as file:
|
||||
loaded_config = json.load(file)
|
||||
logging.config.dictConfig(loaded_config)
|
||||
elif self.log_config.endswith((".yaml", ".yml")):
|
||||
# Install the PyYAML package or the uvicorn[standard] optional
|
||||
# dependencies to enable this functionality.
|
||||
import yaml
|
||||
elif isinstance(self.log_config, str) and self.log_config.endswith((".yaml", ".yml")):
|
||||
try:
|
||||
import yaml
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Install the PyYAML package or uvicorn[standard] to use `--log-config` with YAML files."
|
||||
) from e
|
||||
|
||||
with open(self.log_config) as file:
|
||||
loaded_config = yaml.safe_load(file)
|
||||
@ -406,13 +395,11 @@ class Config:
|
||||
else:
|
||||
# See the note about fileConfig() here:
|
||||
# https://docs.python.org/3/library/logging.config.html#configuration-file-format
|
||||
logging.config.fileConfig(
|
||||
self.log_config, disable_existing_loggers=False
|
||||
)
|
||||
logging.config.fileConfig(self.log_config, disable_existing_loggers=False)
|
||||
|
||||
if self.log_level is not None:
|
||||
if isinstance(self.log_level, str):
|
||||
log_level = LOG_LEVELS[self.log_level]
|
||||
log_level = LOG_LEVELS[self.log_level.lower()]
|
||||
else:
|
||||
log_level = self.log_level
|
||||
logging.getLogger("uvicorn.error").setLevel(log_level)
|
||||
@ -422,12 +409,43 @@ class Config:
|
||||
logging.getLogger("uvicorn.access").handlers = []
|
||||
logging.getLogger("uvicorn.access").propagate = False
|
||||
|
||||
def load_app(self) -> Any:
|
||||
"""Import the app and return it. Exits on failure."""
|
||||
try:
|
||||
return import_from_string(self.app)
|
||||
except ImportFromStringError as exc:
|
||||
logger.error("Error loading ASGI app. %s" % exc)
|
||||
sys.exit(1)
|
||||
|
||||
def load(self) -> None:
|
||||
assert not self.loaded
|
||||
|
||||
if self.is_ssl:
|
||||
if self.ssl_context_factory is not None:
|
||||
|
||||
def default_factory() -> ssl.SSLContext:
|
||||
if not self.ssl_certfile:
|
||||
raise RuntimeError(
|
||||
"`default_ssl_context_factory()` requires `ssl_certfile` to be set on `Config`. "
|
||||
"Either pass `ssl_certfile` (and optionally `ssl_keyfile`) or build the `SSLContext` "
|
||||
"directly inside `ssl_context_factory` without calling the default factory."
|
||||
)
|
||||
return create_ssl_context(
|
||||
keyfile=self.ssl_keyfile,
|
||||
certfile=self.ssl_certfile,
|
||||
password=self.ssl_keyfile_password,
|
||||
ssl_version=self.ssl_version,
|
||||
cert_reqs=self.ssl_cert_reqs,
|
||||
ca_certs=self.ssl_ca_certs,
|
||||
ciphers=self.ssl_ciphers,
|
||||
)
|
||||
|
||||
context = self.ssl_context_factory(self, default_factory)
|
||||
if not isinstance(context, ssl.SSLContext):
|
||||
raise TypeError(f"`ssl_context_factory` must return an `ssl.SSLContext`, got {type(context).__name__}")
|
||||
self.ssl: ssl.SSLContext | None = context
|
||||
elif self.is_ssl:
|
||||
assert self.ssl_certfile
|
||||
self.ssl: Optional[ssl.SSLContext] = create_ssl_context(
|
||||
self.ssl = create_ssl_context(
|
||||
keyfile=self.ssl_keyfile,
|
||||
certfile=self.ssl_certfile,
|
||||
password=self.ssl_keyfile_password,
|
||||
@ -439,10 +457,7 @@ class Config:
|
||||
else:
|
||||
self.ssl = None
|
||||
|
||||
encoded_headers = [
|
||||
(key.lower().encode("latin1"), value.encode("latin1"))
|
||||
for key, value in self.headers
|
||||
]
|
||||
encoded_headers = [(key.lower().encode("latin1"), value.encode("latin1")) for key, value in self.headers]
|
||||
self.encoded_headers = (
|
||||
[(b"server", b"uvicorn")] + encoded_headers
|
||||
if b"server" not in dict(encoded_headers) and self.server_header
|
||||
@ -450,24 +465,20 @@ class Config:
|
||||
)
|
||||
|
||||
if isinstance(self.http, str):
|
||||
http_protocol_class = import_from_string(HTTP_PROTOCOLS[self.http])
|
||||
self.http_protocol_class: Type[asyncio.Protocol] = http_protocol_class
|
||||
http_protocol_class = import_from_string(HTTP_PROTOCOLS.get(self.http, self.http))
|
||||
self.http_protocol_class: type[asyncio.Protocol] = http_protocol_class
|
||||
else:
|
||||
self.http_protocol_class = self.http
|
||||
|
||||
if isinstance(self.ws, str):
|
||||
ws_protocol_class = import_from_string(WS_PROTOCOLS[self.ws])
|
||||
self.ws_protocol_class: Optional[Type[asyncio.Protocol]] = ws_protocol_class
|
||||
ws_protocol_class = import_from_string(WS_PROTOCOLS.get(self.ws, self.ws))
|
||||
self.ws_protocol_class: type[asyncio.Protocol] | None = ws_protocol_class
|
||||
else:
|
||||
self.ws_protocol_class = self.ws
|
||||
|
||||
self.lifespan_class = import_from_string(LIFESPAN[self.lifespan])
|
||||
|
||||
try:
|
||||
self.loaded_app = import_from_string(self.app)
|
||||
except ImportFromStringError as exc:
|
||||
logger.error("Error loading ASGI app. %s" % exc)
|
||||
sys.exit(1)
|
||||
self.loaded_app = self.load_app()
|
||||
|
||||
try:
|
||||
self.loaded_app = self.loaded_app()
|
||||
@ -478,18 +489,17 @@ class Config:
|
||||
else:
|
||||
if not self.factory:
|
||||
logger.warning(
|
||||
"ASGI app factory detected. Using it, "
|
||||
"but please consider setting the --factory flag explicitly."
|
||||
"ASGI app factory detected. Using it, but please consider setting the --factory flag explicitly."
|
||||
)
|
||||
|
||||
if self.interface == "auto":
|
||||
if inspect.isclass(self.loaded_app):
|
||||
use_asgi_3 = hasattr(self.loaded_app, "__await__")
|
||||
elif inspect.isfunction(self.loaded_app):
|
||||
use_asgi_3 = asyncio.iscoroutinefunction(self.loaded_app)
|
||||
use_asgi_3 = iscoroutinefunction(self.loaded_app)
|
||||
else:
|
||||
call = getattr(self.loaded_app, "__call__", None)
|
||||
use_asgi_3 = asyncio.iscoroutinefunction(call)
|
||||
use_asgi_3 = iscoroutinefunction(call)
|
||||
self.interface = "asgi3" if use_asgi_3 else "asgi2"
|
||||
|
||||
if self.interface == "wsgi":
|
||||
@ -501,53 +511,58 @@ class Config:
|
||||
if logger.getEffectiveLevel() <= TRACE_LOG_LEVEL:
|
||||
self.loaded_app = MessageLoggerMiddleware(self.loaded_app)
|
||||
if self.proxy_headers:
|
||||
self.loaded_app = ProxyHeadersMiddleware(
|
||||
self.loaded_app, trusted_hosts=self.forwarded_allow_ips
|
||||
)
|
||||
self.loaded_app = ProxyHeadersMiddleware(self.loaded_app, trusted_hosts=self.forwarded_allow_ips)
|
||||
|
||||
self.loaded = True
|
||||
|
||||
def setup_event_loop(self) -> None:
|
||||
loop_setup: Optional[Callable] = import_from_string(LOOP_SETUPS[self.loop])
|
||||
if loop_setup is not None:
|
||||
loop_setup(use_subprocess=self.use_subprocess)
|
||||
raise AttributeError(
|
||||
"The `setup_event_loop` method was replaced by `get_loop_factory` in uvicorn 0.36.0.\n"
|
||||
"None of those methods are supposed to be used directly. If you are doing it, please let me know here: "
|
||||
"https://github.com/Kludex/uvicorn/discussions/2706. Thank you, and sorry for the inconvenience."
|
||||
)
|
||||
|
||||
def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None:
|
||||
if self.loop in LOOP_FACTORIES:
|
||||
loop_factory: Callable[..., Any] | None = import_from_string(LOOP_FACTORIES[self.loop])
|
||||
else:
|
||||
try:
|
||||
return import_from_string(self.loop)
|
||||
except ImportFromStringError as exc:
|
||||
logger.error("Error loading custom loop setup function. %s" % exc)
|
||||
sys.exit(1)
|
||||
if loop_factory is None:
|
||||
return None
|
||||
return loop_factory(use_subprocess=self.use_subprocess)
|
||||
|
||||
def bind_socket(self) -> socket.socket:
|
||||
logger_args: List[Union[str, int]]
|
||||
if self.uds: # pragma: py-win32
|
||||
logger_args: list[str | int]
|
||||
if self.uds is not None: # pragma: py-win32
|
||||
path = self.uds
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.bind(path)
|
||||
uds_perms = 0o666
|
||||
os.chmod(self.uds, uds_perms)
|
||||
except OSError as exc:
|
||||
except OSError as exc: # pragma: full coverage
|
||||
logger.error(exc)
|
||||
sys.exit(1)
|
||||
|
||||
message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)"
|
||||
sock_name_format = "%s"
|
||||
color_message = (
|
||||
"Uvicorn running on "
|
||||
+ click.style(sock_name_format, bold=True)
|
||||
+ " (Press CTRL+C to quit)"
|
||||
)
|
||||
color_message = "Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)"
|
||||
logger_args = [self.uds]
|
||||
elif self.fd: # pragma: py-win32
|
||||
elif self.fd is not None: # pragma: py-win32
|
||||
sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
message = "Uvicorn running on socket %s (Press CTRL+C to quit)"
|
||||
fd_name_format = "%s"
|
||||
color_message = (
|
||||
"Uvicorn running on "
|
||||
+ click.style(fd_name_format, bold=True)
|
||||
+ " (Press CTRL+C to quit)"
|
||||
)
|
||||
color_message = "Uvicorn running on " + click.style(fd_name_format, bold=True) + " (Press CTRL+C to quit)"
|
||||
logger_args = [sock.getsockname()]
|
||||
else:
|
||||
family = socket.AF_INET
|
||||
addr_format = "%s://%s:%d"
|
||||
|
||||
if self.host and ":" in self.host: # pragma: py-win32
|
||||
if self.host and ":" in self.host: # pragma: full coverage
|
||||
# It's an IPv6 address.
|
||||
family = socket.AF_INET6
|
||||
addr_format = "%s://[%s]:%d"
|
||||
@ -556,16 +571,12 @@ class Config:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
sock.bind((self.host, self.port))
|
||||
except OSError as exc:
|
||||
except OSError as exc: # pragma: full coverage
|
||||
logger.error(exc)
|
||||
sys.exit(1)
|
||||
|
||||
message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
|
||||
color_message = (
|
||||
"Uvicorn running on "
|
||||
+ click.style(addr_format, bold=True)
|
||||
+ " (Press CTRL+C to quit)"
|
||||
)
|
||||
color_message = "Uvicorn running on " + click.style(addr_format, bold=True) + " (Press CTRL+C to quit)"
|
||||
protocol_name = "https" if self.is_ssl else "http"
|
||||
logger_args = [protocol_name, self.host, sock.getsockname()[1]]
|
||||
logger.info(message, *logger_args, extra={"color_message": color_message})
|
||||
|
||||
@ -12,9 +12,7 @@ def import_from_string(import_str: Any) -> Any:
|
||||
|
||||
module_str, _, attrs_str = import_str.partition(":")
|
||||
if not module_str or not attrs_str:
|
||||
message = (
|
||||
'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
||||
)
|
||||
message = 'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
||||
raise ImportFromStringError(message.format(import_str=import_str))
|
||||
|
||||
try:
|
||||
@ -31,8 +29,6 @@ def import_from_string(import_str: Any) -> Any:
|
||||
instance = getattr(instance, attr_str)
|
||||
except AttributeError:
|
||||
message = 'Attribute "{attrs_str}" not found in module "{module_str}".'
|
||||
raise ImportFromStringError(
|
||||
message.format(attrs_str=attrs_str, module_str=module_str)
|
||||
)
|
||||
raise ImportFromStringError(message.format(attrs_str=attrs_str, module_str=module_str))
|
||||
|
||||
return instance
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
from typing import Any, Dict
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from uvicorn import Config
|
||||
|
||||
@ -6,7 +8,7 @@ from uvicorn import Config
|
||||
class LifespanOff:
|
||||
def __init__(self, config: Config) -> None:
|
||||
self.should_exit = False
|
||||
self.state: Dict[str, Any] = {}
|
||||
self.state: dict[str, Any] = {}
|
||||
|
||||
async def startup(self) -> None:
|
||||
pass
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from asyncio import Queue
|
||||
from typing import Any, Dict, Union
|
||||
from typing import Any
|
||||
|
||||
from uvicorn import Config
|
||||
from uvicorn._types import (
|
||||
@ -14,13 +16,13 @@ from uvicorn._types import (
|
||||
LifespanStartupFailedEvent,
|
||||
)
|
||||
|
||||
LifespanReceiveMessage = Union[LifespanStartupEvent, LifespanShutdownEvent]
|
||||
LifespanSendMessage = Union[
|
||||
LifespanStartupFailedEvent,
|
||||
LifespanShutdownFailedEvent,
|
||||
LifespanStartupCompleteEvent,
|
||||
LifespanShutdownCompleteEvent,
|
||||
]
|
||||
LifespanReceiveMessage = LifespanStartupEvent | LifespanShutdownEvent
|
||||
LifespanSendMessage = (
|
||||
LifespanStartupFailedEvent
|
||||
| LifespanShutdownFailedEvent
|
||||
| LifespanStartupCompleteEvent
|
||||
| LifespanShutdownCompleteEvent
|
||||
)
|
||||
|
||||
|
||||
STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol."
|
||||
@ -35,12 +37,12 @@ class LifespanOn:
|
||||
self.logger = logging.getLogger("uvicorn.error")
|
||||
self.startup_event = asyncio.Event()
|
||||
self.shutdown_event = asyncio.Event()
|
||||
self.receive_queue: "Queue[LifespanReceiveMessage]" = asyncio.Queue()
|
||||
self.error_occured = False
|
||||
self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue()
|
||||
self.error_occurred = False
|
||||
self.startup_failed = False
|
||||
self.shutdown_failed = False
|
||||
self.should_exit = False
|
||||
self.state: Dict[str, Any] = {}
|
||||
self.state: dict[str, Any] = {}
|
||||
|
||||
async def startup(self) -> None:
|
||||
self.logger.info("Waiting for application startup.")
|
||||
@ -48,28 +50,26 @@ class LifespanOn:
|
||||
loop = asyncio.get_event_loop()
|
||||
main_lifespan_task = loop.create_task(self.main()) # noqa: F841
|
||||
# Keep a hard reference to prevent garbage collection
|
||||
# See https://github.com/encode/uvicorn/pull/972
|
||||
# See https://github.com/Kludex/uvicorn/pull/972
|
||||
startup_event: LifespanStartupEvent = {"type": "lifespan.startup"}
|
||||
await self.receive_queue.put(startup_event)
|
||||
await self.startup_event.wait()
|
||||
|
||||
if self.startup_failed or (self.error_occured and self.config.lifespan == "on"):
|
||||
if self.startup_failed or (self.error_occurred and self.config.lifespan == "on"):
|
||||
self.logger.error("Application startup failed. Exiting.")
|
||||
self.should_exit = True
|
||||
else:
|
||||
self.logger.info("Application startup complete.")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
if self.error_occured:
|
||||
if self.error_occurred:
|
||||
return
|
||||
self.logger.info("Waiting for application shutdown.")
|
||||
shutdown_event: LifespanShutdownEvent = {"type": "lifespan.shutdown"}
|
||||
await self.receive_queue.put(shutdown_event)
|
||||
await self.shutdown_event.wait()
|
||||
|
||||
if self.shutdown_failed or (
|
||||
self.error_occured and self.config.lifespan == "on"
|
||||
):
|
||||
if self.shutdown_failed or (self.error_occurred and self.config.lifespan == "on"):
|
||||
self.logger.error("Application shutdown failed. Exiting.")
|
||||
self.should_exit = True
|
||||
else:
|
||||
@ -86,7 +86,7 @@ class LifespanOn:
|
||||
await app(scope, self.receive, self.send)
|
||||
except BaseException as exc:
|
||||
self.asgi = None
|
||||
self.error_occured = True
|
||||
self.error_occurred = True
|
||||
if self.startup_failed or self.shutdown_failed:
|
||||
return
|
||||
if self.config.lifespan == "auto":
|
||||
@ -99,7 +99,7 @@ class LifespanOn:
|
||||
self.startup_event.set()
|
||||
self.shutdown_event.set()
|
||||
|
||||
async def send(self, message: "LifespanSendMessage") -> None:
|
||||
async def send(self, message: LifespanSendMessage) -> None:
|
||||
assert message["type"] in (
|
||||
"lifespan.startup.complete",
|
||||
"lifespan.startup.failed",
|
||||
@ -133,5 +133,5 @@ class LifespanOn:
|
||||
if message.get("message"):
|
||||
self.logger.error(message["message"])
|
||||
|
||||
async def receive(self) -> "LifespanReceiveMessage":
|
||||
async def receive(self) -> LifespanReceiveMessage:
|
||||
return await self.receive_queue.get()
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import http
|
||||
import logging
|
||||
import sys
|
||||
from copy import copy
|
||||
from typing import Literal, Optional
|
||||
from typing import Literal
|
||||
|
||||
import click
|
||||
|
||||
@ -14,7 +16,7 @@ class ColourizedFormatter(logging.Formatter):
|
||||
A custom log formatter class that:
|
||||
|
||||
* Outputs the LOG_LEVEL with an appropriate color.
|
||||
* If a log call includes an `extras={"color_message": ...}` it will be used
|
||||
* If a log call includes an `extra={"color_message": ...}` it will be used
|
||||
for formatting the output, instead of the plain text message.
|
||||
"""
|
||||
|
||||
@ -24,17 +26,15 @@ class ColourizedFormatter(logging.Formatter):
|
||||
logging.INFO: lambda level_name: click.style(str(level_name), fg="green"),
|
||||
logging.WARNING: lambda level_name: click.style(str(level_name), fg="yellow"),
|
||||
logging.ERROR: lambda level_name: click.style(str(level_name), fg="red"),
|
||||
logging.CRITICAL: lambda level_name: click.style(
|
||||
str(level_name), fg="bright_red"
|
||||
),
|
||||
logging.CRITICAL: lambda level_name: click.style(str(level_name), fg="bright_red"),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fmt: Optional[str] = None,
|
||||
datefmt: Optional[str] = None,
|
||||
fmt: str | None = None,
|
||||
datefmt: str | None = None,
|
||||
style: Literal["%", "{", "$"] = "%",
|
||||
use_colors: Optional[bool] = None,
|
||||
use_colors: bool | None = None,
|
||||
):
|
||||
if use_colors in (True, False):
|
||||
self.use_colors = use_colors
|
||||
@ -55,13 +55,13 @@ class ColourizedFormatter(logging.Formatter):
|
||||
def formatMessage(self, record: logging.LogRecord) -> str:
|
||||
recordcopy = copy(record)
|
||||
levelname = recordcopy.levelname
|
||||
seperator = " " * (8 - len(recordcopy.levelname))
|
||||
separator = " " * (8 - len(recordcopy.levelname))
|
||||
if self.use_colors:
|
||||
levelname = self.color_level_name(levelname, recordcopy.levelno)
|
||||
if "color_message" in recordcopy.__dict__:
|
||||
recordcopy.msg = recordcopy.__dict__["color_message"]
|
||||
recordcopy.__dict__["message"] = recordcopy.getMessage()
|
||||
recordcopy.__dict__["levelprefix"] = levelname + ":" + seperator
|
||||
recordcopy.__dict__["levelprefix"] = levelname + ":" + separator
|
||||
return super().formatMessage(recordcopy)
|
||||
|
||||
|
||||
@ -84,7 +84,7 @@ class AccessFormatter(ColourizedFormatter):
|
||||
status_phrase = http.HTTPStatus(status_code).phrase
|
||||
except ValueError:
|
||||
status_phrase = ""
|
||||
status_and_phrase = "%s %s" % (status_code, status_phrase)
|
||||
status_and_phrase = f"{status_code} {status_phrase}"
|
||||
if self.use_colors:
|
||||
|
||||
def default(code: int) -> str:
|
||||
@ -104,7 +104,7 @@ class AccessFormatter(ColourizedFormatter):
|
||||
status_code,
|
||||
) = recordcopy.args # type: ignore[misc]
|
||||
status_code = self.get_status_code(int(status_code)) # type: ignore[arg-type]
|
||||
request_line = "%s %s HTTP/%s" % (method, full_path, http_version)
|
||||
request_line = f"{method} {full_path} HTTP/{http_version}"
|
||||
if self.use_colors:
|
||||
request_line = click.style(request_line, bold=True)
|
||||
recordcopy.__dict__.update(
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
def asyncio_setup(use_subprocess: bool = False) -> None:
|
||||
if sys.platform == "win32" and use_subprocess:
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
def asyncio_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
|
||||
if sys.platform == "win32" and not use_subprocess:
|
||||
return asyncio.ProactorEventLoop
|
||||
return asyncio.SelectorEventLoop
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
def auto_loop_setup(use_subprocess: bool = False) -> None:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
|
||||
|
||||
def auto_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
|
||||
try:
|
||||
import uvloop # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
from uvicorn.loops.asyncio import asyncio_setup as loop_setup
|
||||
from uvicorn.loops.asyncio import asyncio_loop_factory as loop_factory
|
||||
|
||||
loop_setup(use_subprocess=use_subprocess)
|
||||
return loop_factory(use_subprocess=use_subprocess)
|
||||
else: # pragma: no cover
|
||||
from uvicorn.loops.uvloop import uvloop_setup
|
||||
from uvicorn.loops.uvloop import uvloop_loop_factory
|
||||
|
||||
uvloop_setup(use_subprocess=use_subprocess)
|
||||
return uvloop_loop_factory(use_subprocess=use_subprocess)
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
|
||||
import uvloop
|
||||
|
||||
|
||||
def uvloop_setup(use_subprocess: bool = False) -> None:
|
||||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||
def uvloop_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
|
||||
return uvloop.new_event_loop
|
||||
|
||||
224
uvicorn/main.py
224
uvicorn/main.py
@ -1,41 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import ssl
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from configparser import RawConfigParser
|
||||
from typing import IO, Any, get_args
|
||||
|
||||
import click
|
||||
|
||||
import uvicorn
|
||||
from uvicorn._types import ASGIApplication
|
||||
from uvicorn.config import (
|
||||
HTTP_PROTOCOLS,
|
||||
INTERFACES,
|
||||
LIFESPAN,
|
||||
LOG_LEVELS,
|
||||
LOGGING_CONFIG,
|
||||
LOOP_SETUPS,
|
||||
SSL_PROTOCOL_VERSION,
|
||||
WS_PROTOCOLS,
|
||||
Config,
|
||||
HTTPProtocolType,
|
||||
InterfaceType,
|
||||
LifespanType,
|
||||
LoopSetupType,
|
||||
LoopFactoryType,
|
||||
WSProtocolType,
|
||||
)
|
||||
from uvicorn.server import Server, ServerState # noqa: F401 # Used to be defined here.
|
||||
from uvicorn.server import Server
|
||||
from uvicorn.supervisors import ChangeReload, Multiprocess
|
||||
|
||||
LEVEL_CHOICES = click.Choice(list(LOG_LEVELS.keys()))
|
||||
HTTP_CHOICES = click.Choice(list(HTTP_PROTOCOLS.keys()))
|
||||
WS_CHOICES = click.Choice(list(WS_PROTOCOLS.keys()))
|
||||
LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys()))
|
||||
LOOP_CHOICES = click.Choice([key for key in LOOP_SETUPS.keys() if key != "none"])
|
||||
INTERFACE_CHOICES = click.Choice(INTERFACES)
|
||||
|
||||
|
||||
def _metavar_from_type(_type: Any) -> str:
|
||||
return f"[{'|'.join(key for key in get_args(_type) if key != 'none')}]"
|
||||
|
||||
|
||||
STARTUP_FAILURE = 3
|
||||
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
@ -45,12 +49,11 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
click.echo(
|
||||
"Running uvicorn %s with %s %s on %s"
|
||||
% (
|
||||
uvicorn.__version__,
|
||||
platform.python_implementation(),
|
||||
platform.python_version(),
|
||||
platform.system(),
|
||||
"Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032
|
||||
version=uvicorn.__version__,
|
||||
py_implementation=platform.python_implementation(),
|
||||
py_version=platform.python_version(),
|
||||
system=platform.system(),
|
||||
)
|
||||
)
|
||||
ctx.exit()
|
||||
@ -73,16 +76,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
show_default=True,
|
||||
)
|
||||
@click.option("--uds", type=str, default=None, help="Bind to a UNIX domain socket.")
|
||||
@click.option(
|
||||
"--fd", type=int, default=None, help="Bind to socket from this file descriptor."
|
||||
)
|
||||
@click.option("--fd", type=int, default=None, help="Bind to socket from this file descriptor.")
|
||||
@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.")
|
||||
@click.option(
|
||||
"--reload-dir",
|
||||
"reload_dirs",
|
||||
multiple=True,
|
||||
help="Set reload directories explicitly, instead of using the current working"
|
||||
" directory.",
|
||||
help="Set reload directories explicitly, instead of using the current working directory.",
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
@click.option(
|
||||
@ -107,8 +107,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
type=float,
|
||||
default=0.25,
|
||||
show_default=True,
|
||||
help="Delay between previous and next check if application needs to be."
|
||||
" Defaults to 0.25s.",
|
||||
help="Delay between previous and next check if application needs to be. Defaults to 0.25s.",
|
||||
)
|
||||
@click.option(
|
||||
"--workers",
|
||||
@ -119,21 +118,24 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
)
|
||||
@click.option(
|
||||
"--loop",
|
||||
type=LOOP_CHOICES,
|
||||
type=str,
|
||||
metavar=_metavar_from_type(LoopFactoryType),
|
||||
default="auto",
|
||||
help="Event loop implementation.",
|
||||
help="Event loop factory implementation.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--http",
|
||||
type=HTTP_CHOICES,
|
||||
type=str,
|
||||
metavar=_metavar_from_type(HTTPProtocolType),
|
||||
default="auto",
|
||||
help="HTTP protocol implementation.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--ws",
|
||||
type=WS_CHOICES,
|
||||
type=str,
|
||||
metavar=_metavar_from_type(WSProtocolType),
|
||||
default="auto",
|
||||
help="WebSocket protocol implementation.",
|
||||
show_default=True,
|
||||
@ -224,8 +226,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
"--proxy-headers/--no-proxy-headers",
|
||||
is_flag=True,
|
||||
default=True,
|
||||
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to "
|
||||
"populate remote address info.",
|
||||
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate url scheme and remote address info.",
|
||||
)
|
||||
@click.option(
|
||||
"--server-header/--no-server-header",
|
||||
@ -243,8 +244,10 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
"--forwarded-allow-ips",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Comma separated list of IPs to trust with proxy headers. Defaults to"
|
||||
" the $FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'.",
|
||||
help="Comma separated list of IP Addresses, IP Networks, or literals "
|
||||
"(e.g. UNIX Socket path) to trust with proxy headers. Defaults to the "
|
||||
"$FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. "
|
||||
"The literal '*' means trust everything.",
|
||||
)
|
||||
@click.option(
|
||||
"--root-path",
|
||||
@ -256,8 +259,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
"--limit-concurrency",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Maximum number of concurrent connections or tasks to allow, before issuing"
|
||||
" HTTP 503 responses.",
|
||||
help="Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses.",
|
||||
)
|
||||
@click.option(
|
||||
"--backlog",
|
||||
@ -271,11 +273,19 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
default=None,
|
||||
help="Maximum number of requests to service before terminating the process.",
|
||||
)
|
||||
@click.option(
|
||||
"--limit-max-requests-jitter",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Maximum jitter to add to limit_max_requests."
|
||||
" Staggers worker restarts to avoid all workers restarting simultaneously.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--timeout-keep-alive",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Close Keep-Alive connections if no new data is received within this timeout.",
|
||||
help="Close Keep-Alive connections if no new data is received within this timeout (in seconds).",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
@ -285,8 +295,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
help="Maximum number of seconds to wait for graceful shutdown.",
|
||||
)
|
||||
@click.option(
|
||||
"--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True
|
||||
"--timeout-worker-healthcheck",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Maximum number of seconds to wait for a worker to respond to a healthcheck.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option("--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True)
|
||||
@click.option(
|
||||
"--ssl-certfile",
|
||||
type=str,
|
||||
@ -357,6 +372,13 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
|
||||
default=None,
|
||||
help="For h11, the maximum number of bytes to buffer of an incomplete event.",
|
||||
)
|
||||
@click.option(
|
||||
"--reset-contextvars",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Run each ASGI request in a fresh contextvars.Context. Hides context set in the lifespan.",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option(
|
||||
"--factory",
|
||||
is_flag=True,
|
||||
@ -370,9 +392,9 @@ def main(
|
||||
port: int,
|
||||
uds: str,
|
||||
fd: int,
|
||||
loop: LoopSetupType,
|
||||
http: HTTPProtocolType,
|
||||
ws: WSProtocolType,
|
||||
loop: LoopFactoryType | str,
|
||||
http: HTTPProtocolType | str,
|
||||
ws: WSProtocolType | str,
|
||||
ws_max_size: int,
|
||||
ws_max_queue: int,
|
||||
ws_ping_interval: float,
|
||||
@ -381,9 +403,9 @@ def main(
|
||||
lifespan: LifespanType,
|
||||
interface: InterfaceType,
|
||||
reload: bool,
|
||||
reload_dirs: typing.List[str],
|
||||
reload_includes: typing.List[str],
|
||||
reload_excludes: typing.List[str],
|
||||
reload_dirs: list[str],
|
||||
reload_includes: list[str],
|
||||
reload_excludes: list[str],
|
||||
reload_delay: float,
|
||||
workers: int,
|
||||
env_file: str,
|
||||
@ -398,8 +420,10 @@ def main(
|
||||
limit_concurrency: int,
|
||||
backlog: int,
|
||||
limit_max_requests: int,
|
||||
limit_max_requests_jitter: int,
|
||||
timeout_keep_alive: int,
|
||||
timeout_graceful_shutdown: typing.Optional[int],
|
||||
timeout_graceful_shutdown: int | None,
|
||||
timeout_worker_healthcheck: int,
|
||||
ssl_keyfile: str,
|
||||
ssl_certfile: str,
|
||||
ssl_keyfile_password: str,
|
||||
@ -407,10 +431,11 @@ def main(
|
||||
ssl_cert_reqs: int,
|
||||
ssl_ca_certs: str,
|
||||
ssl_ciphers: str,
|
||||
headers: typing.List[str],
|
||||
headers: list[str],
|
||||
use_colors: bool,
|
||||
app_dir: str,
|
||||
h11_max_incomplete_event_size: typing.Optional[int],
|
||||
h11_max_incomplete_event_size: int | None,
|
||||
reset_contextvars: bool,
|
||||
factory: bool,
|
||||
) -> None:
|
||||
run(
|
||||
@ -447,8 +472,10 @@ def main(
|
||||
limit_concurrency=limit_concurrency,
|
||||
backlog=backlog,
|
||||
limit_max_requests=limit_max_requests,
|
||||
limit_max_requests_jitter=limit_max_requests_jitter,
|
||||
timeout_keep_alive=timeout_keep_alive,
|
||||
timeout_graceful_shutdown=timeout_graceful_shutdown,
|
||||
timeout_worker_healthcheck=timeout_worker_healthcheck,
|
||||
ssl_keyfile=ssl_keyfile,
|
||||
ssl_certfile=ssl_certfile,
|
||||
ssl_keyfile_password=ssl_keyfile_password,
|
||||
@ -461,60 +488,63 @@ def main(
|
||||
factory=factory,
|
||||
app_dir=app_dir,
|
||||
h11_max_incomplete_event_size=h11_max_incomplete_event_size,
|
||||
reset_contextvars=reset_contextvars,
|
||||
)
|
||||
|
||||
|
||||
def run(
|
||||
app: typing.Union["ASGIApplication", typing.Callable[..., typing.Any], str],
|
||||
app: ASGIApplication | Callable[..., Any] | str,
|
||||
*,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 8000,
|
||||
uds: typing.Optional[str] = None,
|
||||
fd: typing.Optional[int] = None,
|
||||
loop: LoopSetupType = "auto",
|
||||
http: typing.Union[typing.Type[asyncio.Protocol], HTTPProtocolType] = "auto",
|
||||
ws: typing.Union[typing.Type[asyncio.Protocol], WSProtocolType] = "auto",
|
||||
uds: str | None = None,
|
||||
fd: int | None = None,
|
||||
loop: LoopFactoryType | str = "auto",
|
||||
http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto",
|
||||
ws: type[asyncio.Protocol] | WSProtocolType | str = "auto",
|
||||
ws_max_size: int = 16777216,
|
||||
ws_max_queue: int = 32,
|
||||
ws_ping_interval: typing.Optional[float] = 20.0,
|
||||
ws_ping_timeout: typing.Optional[float] = 20.0,
|
||||
ws_ping_interval: float | None = 20.0,
|
||||
ws_ping_timeout: float | None = 20.0,
|
||||
ws_per_message_deflate: bool = True,
|
||||
lifespan: LifespanType = "auto",
|
||||
interface: InterfaceType = "auto",
|
||||
reload: bool = False,
|
||||
reload_dirs: typing.Optional[typing.Union[typing.List[str], str]] = None,
|
||||
reload_includes: typing.Optional[typing.Union[typing.List[str], str]] = None,
|
||||
reload_excludes: typing.Optional[typing.Union[typing.List[str], str]] = None,
|
||||
reload_dirs: list[str] | str | None = None,
|
||||
reload_includes: list[str] | str | None = None,
|
||||
reload_excludes: list[str] | str | None = None,
|
||||
reload_delay: float = 0.25,
|
||||
workers: typing.Optional[int] = None,
|
||||
env_file: "str | os.PathLike[str] | None" = None,
|
||||
log_config: typing.Optional[
|
||||
typing.Union[typing.Dict[str, typing.Any], str]
|
||||
] = LOGGING_CONFIG,
|
||||
log_level: typing.Optional[typing.Union[str, int]] = None,
|
||||
workers: int | None = None,
|
||||
env_file: str | os.PathLike[str] | None = None,
|
||||
log_config: dict[str, Any] | str | os.PathLike[str] | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
|
||||
log_level: str | int | None = None,
|
||||
access_log: bool = True,
|
||||
proxy_headers: bool = True,
|
||||
server_header: bool = True,
|
||||
date_header: bool = True,
|
||||
forwarded_allow_ips: typing.Optional[typing.Union[typing.List[str], str]] = None,
|
||||
forwarded_allow_ips: list[str] | str | None = None,
|
||||
root_path: str = "",
|
||||
limit_concurrency: typing.Optional[int] = None,
|
||||
limit_concurrency: int | None = None,
|
||||
backlog: int = 2048,
|
||||
limit_max_requests: typing.Optional[int] = None,
|
||||
limit_max_requests: int | None = None,
|
||||
limit_max_requests_jitter: int = 0,
|
||||
timeout_keep_alive: int = 5,
|
||||
timeout_graceful_shutdown: typing.Optional[int] = None,
|
||||
ssl_keyfile: typing.Optional[str] = None,
|
||||
ssl_certfile: "str | os.PathLike[str] | None" = None,
|
||||
ssl_keyfile_password: typing.Optional[str] = None,
|
||||
timeout_graceful_shutdown: int | None = None,
|
||||
timeout_worker_healthcheck: int = 5,
|
||||
ssl_keyfile: str | os.PathLike[str] | None = None,
|
||||
ssl_certfile: str | os.PathLike[str] | None = None,
|
||||
ssl_keyfile_password: str | None = None,
|
||||
ssl_version: int = SSL_PROTOCOL_VERSION,
|
||||
ssl_cert_reqs: int = ssl.CERT_NONE,
|
||||
ssl_ca_certs: typing.Optional[str] = None,
|
||||
ssl_ca_certs: str | os.PathLike[str] | None = None,
|
||||
ssl_ciphers: str = "TLSv1",
|
||||
headers: typing.Optional[typing.List[typing.Tuple[str, str]]] = None,
|
||||
use_colors: typing.Optional[bool] = None,
|
||||
app_dir: typing.Optional[str] = None,
|
||||
ssl_context_factory: Callable[[Config, Callable[[], ssl.SSLContext]], ssl.SSLContext] | None = None,
|
||||
headers: list[tuple[str, str]] | None = None,
|
||||
use_colors: bool | None = None,
|
||||
app_dir: str | None = None,
|
||||
factory: bool = False,
|
||||
h11_max_incomplete_event_size: typing.Optional[int] = None,
|
||||
h11_max_incomplete_event_size: int | None = None,
|
||||
reset_contextvars: bool = False,
|
||||
) -> None:
|
||||
if app_dir is not None:
|
||||
sys.path.insert(0, app_dir)
|
||||
@ -553,8 +583,10 @@ def run(
|
||||
limit_concurrency=limit_concurrency,
|
||||
backlog=backlog,
|
||||
limit_max_requests=limit_max_requests,
|
||||
limit_max_requests_jitter=limit_max_requests_jitter,
|
||||
timeout_keep_alive=timeout_keep_alive,
|
||||
timeout_graceful_shutdown=timeout_graceful_shutdown,
|
||||
timeout_worker_healthcheck=timeout_worker_healthcheck,
|
||||
ssl_keyfile=ssl_keyfile,
|
||||
ssl_certfile=ssl_certfile,
|
||||
ssl_keyfile_password=ssl_keyfile_password,
|
||||
@ -562,35 +594,51 @@ def run(
|
||||
ssl_cert_reqs=ssl_cert_reqs,
|
||||
ssl_ca_certs=ssl_ca_certs,
|
||||
ssl_ciphers=ssl_ciphers,
|
||||
ssl_context_factory=ssl_context_factory,
|
||||
headers=headers,
|
||||
use_colors=use_colors,
|
||||
factory=factory,
|
||||
h11_max_incomplete_event_size=h11_max_incomplete_event_size,
|
||||
reset_contextvars=reset_contextvars,
|
||||
)
|
||||
server = Server(config=config)
|
||||
|
||||
if (config.reload or config.workers > 1) and not isinstance(app, str):
|
||||
logger = logging.getLogger("uvicorn.error")
|
||||
logger.warning(
|
||||
"You must pass the application as an import string to enable 'reload' or "
|
||||
"'workers'."
|
||||
)
|
||||
logger.warning("You must pass the application as an import string to enable 'reload' or 'workers'.")
|
||||
sys.exit(1)
|
||||
|
||||
if config.should_reload:
|
||||
sock = config.bind_socket()
|
||||
ChangeReload(config, target=server.run, sockets=[sock]).run()
|
||||
elif config.workers > 1:
|
||||
sock = config.bind_socket()
|
||||
Multiprocess(config, target=server.run, sockets=[sock]).run()
|
||||
else:
|
||||
server.run()
|
||||
if config.uds and os.path.exists(config.uds):
|
||||
os.remove(config.uds) # pragma: py-win32
|
||||
config.load_app()
|
||||
server = Server(config=config)
|
||||
|
||||
try:
|
||||
if config.should_reload:
|
||||
sock = config.bind_socket()
|
||||
ChangeReload(config, target=server.run, sockets=[sock]).run()
|
||||
elif config.workers > 1:
|
||||
sock = config.bind_socket()
|
||||
Multiprocess(config, target=server.run, sockets=[sock]).run()
|
||||
else:
|
||||
server.run()
|
||||
except KeyboardInterrupt: # pragma: full coverage
|
||||
pass
|
||||
finally:
|
||||
if config.uds and os.path.exists(config.uds):
|
||||
os.remove(config.uds) # pragma: py-win32
|
||||
|
||||
if not server.started and not config.should_reload and config.workers == 1:
|
||||
sys.exit(STARTUP_FAILURE)
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "ServerState":
|
||||
warnings.warn(
|
||||
"uvicorn.main.ServerState is deprecated, use uvicorn.server.ServerState instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
return ServerState
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main() # pragma: no cover
|
||||
|
||||
@ -10,8 +10,6 @@ class ASGI2Middleware:
|
||||
def __init__(self, app: "ASGI2Application"):
|
||||
self.app = app
|
||||
|
||||
async def __call__(
|
||||
self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None:
|
||||
async def __call__(self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None:
|
||||
instance = self.app(scope)
|
||||
await instance(receive, send)
|
||||
|
||||
@ -1,84 +1,174 @@
|
||||
"""
|
||||
This middleware can be used when a known proxy is fronting the application,
|
||||
and is trusted to be properly setting the `X-Forwarded-Proto` and
|
||||
`X-Forwarded-For` headers with the connecting client information.
|
||||
from __future__ import annotations
|
||||
|
||||
Modifies the `client` and `scheme` information so that they reference
|
||||
the connecting client, rather that the connecting proxy.
|
||||
import ipaddress
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies
|
||||
"""
|
||||
from typing import List, Optional, Tuple, Union, cast
|
||||
|
||||
from uvicorn._types import (
|
||||
ASGI3Application,
|
||||
ASGIReceiveCallable,
|
||||
ASGISendCallable,
|
||||
HTTPScope,
|
||||
Scope,
|
||||
WebSocketScope,
|
||||
)
|
||||
from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
|
||||
|
||||
class ProxyHeadersMiddleware:
|
||||
def __init__(
|
||||
self,
|
||||
app: "ASGI3Application",
|
||||
trusted_hosts: Union[List[str], str] = "127.0.0.1",
|
||||
) -> None:
|
||||
"""Middleware for handling known proxy headers
|
||||
|
||||
This middleware can be used when a known proxy is fronting the application,
|
||||
and is trusted to be properly setting the `X-Forwarded-Proto` and
|
||||
`X-Forwarded-For` headers with the connecting client information.
|
||||
|
||||
Modifies the `client` and `scheme` information so that they reference
|
||||
the connecting client, rather that the connecting proxy.
|
||||
|
||||
References:
|
||||
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies>
|
||||
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For>
|
||||
"""
|
||||
|
||||
def __init__(self, app: ASGI3Application, trusted_hosts: list[str] | str = "127.0.0.1") -> None:
|
||||
self.app = app
|
||||
if isinstance(trusted_hosts, str):
|
||||
self.trusted_hosts = {item.strip() for item in trusted_hosts.split(",")}
|
||||
else:
|
||||
self.trusted_hosts = set(trusted_hosts)
|
||||
self.always_trust = "*" in self.trusted_hosts
|
||||
self.trusted_hosts = _TrustedHosts(trusted_hosts)
|
||||
|
||||
def get_trusted_client_host(
|
||||
self, x_forwarded_for_hosts: List[str]
|
||||
) -> Optional[str]:
|
||||
if self.always_trust:
|
||||
return x_forwarded_for_hosts[0]
|
||||
async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
if scope["type"] == "lifespan":
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
for host in reversed(x_forwarded_for_hosts):
|
||||
if host not in self.trusted_hosts:
|
||||
return host
|
||||
client_addr = scope.get("client")
|
||||
client_host = client_addr[0] if client_addr else None
|
||||
|
||||
return None
|
||||
if client_host in self.trusted_hosts:
|
||||
headers = dict(scope["headers"])
|
||||
|
||||
async def __call__(
|
||||
self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None:
|
||||
if scope["type"] in ("http", "websocket"):
|
||||
scope = cast(Union["HTTPScope", "WebSocketScope"], scope)
|
||||
client_addr: Optional[Tuple[str, int]] = scope.get("client")
|
||||
client_host = client_addr[0] if client_addr else None
|
||||
if b"x-forwarded-proto" in headers:
|
||||
x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip()
|
||||
|
||||
if self.always_trust or client_host in self.trusted_hosts:
|
||||
headers = dict(scope["headers"])
|
||||
|
||||
if b"x-forwarded-proto" in headers:
|
||||
# Determine if the incoming request was http or https based on
|
||||
# the X-Forwarded-Proto header.
|
||||
x_forwarded_proto = (
|
||||
headers[b"x-forwarded-proto"].decode("latin1").strip()
|
||||
)
|
||||
if x_forwarded_proto in {"http", "https", "ws", "wss"}:
|
||||
if scope["type"] == "websocket":
|
||||
scope["scheme"] = (
|
||||
"wss" if x_forwarded_proto == "https" else "ws"
|
||||
)
|
||||
scope["scheme"] = x_forwarded_proto.replace("http", "ws")
|
||||
else:
|
||||
scope["scheme"] = x_forwarded_proto
|
||||
|
||||
if b"x-forwarded-for" in headers:
|
||||
# Determine the client address from the last trusted IP in the
|
||||
# X-Forwarded-For header. We've lost the connecting client's port
|
||||
# information by now, so only include the host.
|
||||
x_forwarded_for = headers[b"x-forwarded-for"].decode("latin1")
|
||||
x_forwarded_for_hosts = [
|
||||
item.strip() for item in x_forwarded_for.split(",")
|
||||
]
|
||||
host = self.get_trusted_client_host(x_forwarded_for_hosts)
|
||||
port = 0
|
||||
scope["client"] = (host, port) # type: ignore[arg-type]
|
||||
if b"x-forwarded-for" in headers:
|
||||
x_forwarded_for = headers[b"x-forwarded-for"].decode("latin1")
|
||||
host, port = self.trusted_hosts.get_trusted_client_address(x_forwarded_for)
|
||||
|
||||
if host:
|
||||
# If the x-forwarded-for header is empty then host is an empty string.
|
||||
# Only set the client if we actually got something usable.
|
||||
# See: https://github.com/Kludex/uvicorn/issues/1068
|
||||
scope["client"] = (host, port)
|
||||
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
|
||||
def _parse_raw_hosts(value: str) -> list[str]:
|
||||
return [item.strip() for item in value.split(",")]
|
||||
|
||||
|
||||
def _parse_host_port(value: str) -> tuple[str, int]:
|
||||
"""Parse a forwarded host value into host and optional port.
|
||||
|
||||
Accepts bare IPs, IPv4 `host:port`, and bracketed IPv6 `[host]:port`.
|
||||
Any unrecognized or malformed value is treated conservatively and returned
|
||||
without a port so trust checks do not silently normalize arbitrary input.
|
||||
"""
|
||||
|
||||
if value.startswith("["):
|
||||
bracket_end = value.find("]")
|
||||
if bracket_end == -1:
|
||||
return value, 0
|
||||
|
||||
host = value[1:bracket_end]
|
||||
remainder = value[bracket_end + 1 :]
|
||||
if not remainder:
|
||||
return host, 0
|
||||
if not remainder.startswith(":"):
|
||||
return value, 0
|
||||
|
||||
try:
|
||||
return host, int(remainder[1:])
|
||||
except ValueError:
|
||||
return host, 0
|
||||
|
||||
if value.count(":") == 1:
|
||||
host, port = value.rsplit(":", 1)
|
||||
try:
|
||||
return host, int(port)
|
||||
except ValueError:
|
||||
return value, 0
|
||||
|
||||
return value, 0
|
||||
|
||||
|
||||
class _TrustedHosts:
|
||||
"""Container for trusted hosts and networks"""
|
||||
|
||||
def __init__(self, trusted_hosts: list[str] | str) -> None:
|
||||
self.always_trust: bool = trusted_hosts in ("*", ["*"])
|
||||
|
||||
self.trusted_literals: set[str] = set()
|
||||
self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set()
|
||||
self.trusted_networks: set[ipaddress.IPv4Network | ipaddress.IPv6Network] = set()
|
||||
|
||||
# Notes:
|
||||
# - We separate hosts from literals as there are many ways to write
|
||||
# an IPv6 Address so we need to compare by object.
|
||||
# - We don't convert IP Address to single host networks (e.g. /32 / 128) as
|
||||
# it more efficient to do an address lookup in a set than check for
|
||||
# membership in each network.
|
||||
# - We still allow literals as it might be possible that we receive a
|
||||
# something that isn't an IP Address e.g. a unix socket.
|
||||
|
||||
if not self.always_trust:
|
||||
if isinstance(trusted_hosts, str):
|
||||
trusted_hosts = _parse_raw_hosts(trusted_hosts)
|
||||
|
||||
for host in trusted_hosts:
|
||||
# Note: because we always convert invalid IP types to literals it
|
||||
# is not possible for the user to know they provided a malformed IP
|
||||
# type - this may lead to unexpected / difficult to debug behaviour.
|
||||
|
||||
if "/" in host:
|
||||
# Looks like a network
|
||||
try:
|
||||
self.trusted_networks.add(ipaddress.ip_network(host))
|
||||
except ValueError:
|
||||
# Was not a valid IP Network
|
||||
self.trusted_literals.add(host)
|
||||
else:
|
||||
try:
|
||||
self.trusted_hosts.add(ipaddress.ip_address(host))
|
||||
except ValueError:
|
||||
# Was not a valid IP Address
|
||||
self.trusted_literals.add(host)
|
||||
|
||||
def __contains__(self, host: str | None) -> bool:
|
||||
if self.always_trust:
|
||||
return True
|
||||
|
||||
if not host:
|
||||
return False
|
||||
|
||||
try:
|
||||
ip = ipaddress.ip_address(host)
|
||||
if ip in self.trusted_hosts:
|
||||
return True
|
||||
return any(ip in net for net in self.trusted_networks)
|
||||
|
||||
except ValueError:
|
||||
return host in self.trusted_literals
|
||||
|
||||
def get_trusted_client_address(self, x_forwarded_for: str) -> tuple[str, int]:
|
||||
"""Extract the client address from x_forwarded_for header.
|
||||
|
||||
In general this is the first "untrusted" host in the forwarded for list.
|
||||
"""
|
||||
x_forwarded_for_hosts = _parse_raw_hosts(x_forwarded_for)
|
||||
|
||||
if self.always_trust:
|
||||
return _parse_host_port(x_forwarded_for_hosts[0])
|
||||
|
||||
# Note: each proxy appends to the header list so check it in reverse order
|
||||
for host_port in reversed(x_forwarded_for_hosts):
|
||||
host, port = _parse_host_port(host_port)
|
||||
if host not in self:
|
||||
return host, port
|
||||
|
||||
# All hosts are trusted meaning that the client was also a trusted proxy
|
||||
# See https://github.com/Kludex/uvicorn/issues/1068#issuecomment-855371576
|
||||
return _parse_host_port(x_forwarded_for_hosts[0])
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import io
|
||||
import sys
|
||||
import warnings
|
||||
from collections import deque
|
||||
from typing import Deque, Iterable, Optional, Tuple
|
||||
from collections.abc import Iterable
|
||||
|
||||
from uvicorn._types import (
|
||||
ASGIReceiveCallable,
|
||||
@ -22,16 +24,18 @@ from uvicorn._types import (
|
||||
)
|
||||
|
||||
|
||||
def build_environ(
|
||||
scope: "HTTPScope", message: "ASGIReceiveEvent", body: io.BytesIO
|
||||
) -> Environ:
|
||||
def build_environ(scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO) -> Environ:
|
||||
"""
|
||||
Builds a scope and request message into a WSGI environ object.
|
||||
"""
|
||||
script_name = scope.get("root_path", "").encode("utf8").decode("latin1")
|
||||
path_info = scope["path"].encode("utf8").decode("latin1")
|
||||
if path_info.startswith(script_name):
|
||||
path_info = path_info[len(script_name) :]
|
||||
environ = {
|
||||
"REQUEST_METHOD": scope["method"],
|
||||
"SCRIPT_NAME": "",
|
||||
"PATH_INFO": scope["path"].encode("utf8").decode("latin1"),
|
||||
"SCRIPT_NAME": script_name,
|
||||
"PATH_INFO": path_info,
|
||||
"QUERY_STRING": scope["query_string"].decode("ascii"),
|
||||
"SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"],
|
||||
"wsgi.version": (1, 0),
|
||||
@ -78,8 +82,7 @@ def build_environ(
|
||||
class _WSGIMiddleware:
|
||||
def __init__(self, app: WSGIApp, workers: int = 10):
|
||||
warnings.warn(
|
||||
"Uvicorn's native WSGI implementation is deprecated, you "
|
||||
"should switch to a2wsgi (`pip install a2wsgi`).",
|
||||
"Uvicorn's native WSGI implementation is deprecated, you should switch to a2wsgi (`pip install a2wsgi`).",
|
||||
DeprecationWarning,
|
||||
)
|
||||
self.app = app
|
||||
@ -87,9 +90,9 @@ class _WSGIMiddleware:
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
scope: "HTTPScope",
|
||||
receive: "ASGIReceiveCallable",
|
||||
send: "ASGISendCallable",
|
||||
scope: HTTPScope,
|
||||
receive: ASGIReceiveCallable,
|
||||
send: ASGISendCallable,
|
||||
) -> None:
|
||||
assert scope["type"] == "http"
|
||||
instance = WSGIResponder(self.app, self.executor, scope)
|
||||
@ -101,7 +104,7 @@ class WSGIResponder:
|
||||
self,
|
||||
app: WSGIApp,
|
||||
executor: concurrent.futures.ThreadPoolExecutor,
|
||||
scope: "HTTPScope",
|
||||
scope: HTTPScope,
|
||||
):
|
||||
self.app = app
|
||||
self.executor = executor
|
||||
@ -109,21 +112,19 @@ class WSGIResponder:
|
||||
self.status = None
|
||||
self.response_headers = None
|
||||
self.send_event = asyncio.Event()
|
||||
self.send_queue: Deque[Optional["ASGISendEvent"]] = deque()
|
||||
self.send_queue: deque[ASGISendEvent | None] = deque()
|
||||
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
||||
self.response_started = False
|
||||
self.exc_info: Optional[ExcInfo] = None
|
||||
self.exc_info: ExcInfo | None = None
|
||||
|
||||
async def __call__(
|
||||
self, receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None:
|
||||
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
message: HTTPRequestEvent = await receive() # type: ignore[assignment]
|
||||
body = io.BytesIO(message.get("body", b""))
|
||||
more_body = message.get("more_body", False)
|
||||
if more_body:
|
||||
body.seek(0, io.SEEK_END)
|
||||
while more_body:
|
||||
body_message: "HTTPRequestEvent" = (
|
||||
body_message: HTTPRequestEvent = (
|
||||
await receive() # type: ignore[assignment]
|
||||
)
|
||||
body.write(body_message.get("body", b""))
|
||||
@ -131,9 +132,7 @@ class WSGIResponder:
|
||||
body.seek(0)
|
||||
environ = build_environ(self.scope, message, body)
|
||||
self.loop = asyncio.get_event_loop()
|
||||
wsgi = self.loop.run_in_executor(
|
||||
self.executor, self.wsgi, environ, self.start_response
|
||||
)
|
||||
wsgi = self.loop.run_in_executor(self.executor, self.wsgi, environ, self.start_response)
|
||||
sender = self.loop.create_task(self.sender(send))
|
||||
try:
|
||||
await asyncio.wait_for(wsgi, None)
|
||||
@ -144,32 +143,29 @@ class WSGIResponder:
|
||||
if self.exc_info is not None:
|
||||
raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2])
|
||||
|
||||
async def sender(self, send: "ASGISendCallable") -> None:
|
||||
async def sender(self, send: ASGISendCallable) -> None:
|
||||
while True:
|
||||
if self.send_queue:
|
||||
message = self.send_queue.popleft()
|
||||
if message is None:
|
||||
return
|
||||
await send(message)
|
||||
else:
|
||||
else: # pragma: no cover
|
||||
await self.send_event.wait()
|
||||
self.send_event.clear()
|
||||
|
||||
def start_response(
|
||||
self,
|
||||
status: str,
|
||||
response_headers: Iterable[Tuple[str, str]],
|
||||
exc_info: Optional[ExcInfo] = None,
|
||||
response_headers: Iterable[tuple[str, str]],
|
||||
exc_info: ExcInfo | None = None,
|
||||
) -> None:
|
||||
self.exc_info = exc_info
|
||||
if not self.response_started:
|
||||
self.response_started = True
|
||||
status_code_str, _ = status.split(" ", 1)
|
||||
status_code = int(status_code_str)
|
||||
headers = [
|
||||
(name.encode("ascii"), value.encode("ascii"))
|
||||
for name, value in response_headers
|
||||
]
|
||||
headers = [(name.encode("ascii"), value.encode("ascii")) for name, value in response_headers]
|
||||
http_response_start_event: HTTPResponseStartEvent = {
|
||||
"type": "http.response.start",
|
||||
"status": status_code,
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
from typing import Type
|
||||
from __future__ import annotations
|
||||
|
||||
AutoHTTPProtocol: Type[asyncio.Protocol]
|
||||
import asyncio
|
||||
|
||||
AutoHTTPProtocol: type[asyncio.Protocol]
|
||||
try:
|
||||
import httptools # noqa
|
||||
except ImportError: # pragma: no cover
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import asyncio
|
||||
|
||||
from uvicorn._types import (
|
||||
ASGIReceiveCallable,
|
||||
ASGISendCallable,
|
||||
HTTPResponseBodyEvent,
|
||||
HTTPResponseStartEvent,
|
||||
Scope,
|
||||
)
|
||||
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
||||
|
||||
CLOSE_HEADER = (b"connection", b"close")
|
||||
|
||||
@ -22,7 +16,7 @@ class FlowControl:
|
||||
self._is_writable_event.set()
|
||||
|
||||
async def drain(self) -> None:
|
||||
await self._is_writable_event.wait()
|
||||
await self._is_writable_event.wait() # pragma: full coverage
|
||||
|
||||
def pause_reading(self) -> None:
|
||||
if not self.read_paused:
|
||||
@ -35,32 +29,26 @@ class FlowControl:
|
||||
self._transport.resume_reading()
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
if not self.write_paused:
|
||||
if not self.write_paused: # pragma: full coverage
|
||||
self.write_paused = True
|
||||
self._is_writable_event.clear()
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
if self.write_paused:
|
||||
if self.write_paused: # pragma: full coverage
|
||||
self.write_paused = False
|
||||
self._is_writable_event.set()
|
||||
|
||||
|
||||
async def service_unavailable(
|
||||
scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable"
|
||||
) -> None:
|
||||
response_start: "HTTPResponseStartEvent" = {
|
||||
"type": "http.response.start",
|
||||
"status": 503,
|
||||
"headers": [
|
||||
(b"content-type", b"text/plain; charset=utf-8"),
|
||||
(b"connection", b"close"),
|
||||
],
|
||||
}
|
||||
await send(response_start)
|
||||
|
||||
response_body: "HTTPResponseBodyEvent" = {
|
||||
"type": "http.response.body",
|
||||
"body": b"Service Unavailable",
|
||||
"more_body": False,
|
||||
}
|
||||
await send(response_body)
|
||||
async def service_unavailable(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
||||
await send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": 503,
|
||||
"headers": [
|
||||
(b"content-type", b"text/plain; charset=utf-8"),
|
||||
(b"content-length", b"19"),
|
||||
(b"connection", b"close"),
|
||||
],
|
||||
}
|
||||
)
|
||||
await send({"type": "http.response.body", "body": b"Service Unavailable", "more_body": False})
|
||||
|
||||
@ -1,16 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import http
|
||||
import logging
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import unquote
|
||||
|
||||
import h11
|
||||
@ -27,19 +23,8 @@ from uvicorn._types import (
|
||||
)
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.logging import TRACE_LOG_LEVEL
|
||||
from uvicorn.protocols.http.flow_control import (
|
||||
CLOSE_HEADER,
|
||||
HIGH_WATER_LIMIT,
|
||||
FlowControl,
|
||||
service_unavailable,
|
||||
)
|
||||
from uvicorn.protocols.utils import (
|
||||
get_client_addr,
|
||||
get_local_addr,
|
||||
get_path_with_query_string,
|
||||
get_remote_addr,
|
||||
is_ssl,
|
||||
)
|
||||
from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
|
||||
from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
|
||||
@ -50,9 +35,7 @@ def _get_status_phrase(status_code: int) -> bytes:
|
||||
return b""
|
||||
|
||||
|
||||
STATUS_PHRASES = {
|
||||
status_code: _get_status_phrase(status_code) for status_code in range(100, 600)
|
||||
}
|
||||
STATUS_PHRASES = {status_code: _get_status_phrase(status_code) for status_code in range(100, 600)}
|
||||
|
||||
|
||||
class H11Protocol(asyncio.Protocol):
|
||||
@ -60,8 +43,8 @@ class H11Protocol(asyncio.Protocol):
|
||||
self,
|
||||
config: Config,
|
||||
server_state: ServerState,
|
||||
app_state: Dict[str, Any],
|
||||
_loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
app_state: dict[str, Any],
|
||||
_loop: asyncio.AbstractEventLoop | None = None,
|
||||
) -> None:
|
||||
if not config.loaded:
|
||||
config.load()
|
||||
@ -84,7 +67,7 @@ class H11Protocol(asyncio.Protocol):
|
||||
self.app_state = app_state
|
||||
|
||||
# Timeouts
|
||||
self.timeout_keep_alive_task: Optional[asyncio.TimerHandle] = None
|
||||
self.timeout_keep_alive_task: asyncio.TimerHandle | None = None
|
||||
self.timeout_keep_alive = config.timeout_keep_alive
|
||||
|
||||
# Shared server state
|
||||
@ -95,13 +78,13 @@ class H11Protocol(asyncio.Protocol):
|
||||
# Per-connection state
|
||||
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
||||
self.flow: FlowControl = None # type: ignore[assignment]
|
||||
self.server: Optional[Tuple[str, int]] = None
|
||||
self.client: Optional[Tuple[str, int]] = None
|
||||
self.scheme: Optional[Literal["http", "https"]] = None
|
||||
self.server: tuple[str, int | None] | None = None
|
||||
self.client: tuple[str, int] | None = None
|
||||
self.scheme: Literal["http", "https"] | None = None
|
||||
|
||||
# Per-request state
|
||||
self.scope: HTTPScope = None # type: ignore[assignment]
|
||||
self.headers: List[Tuple[bytes, bytes]] = None # type: ignore[assignment]
|
||||
self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment]
|
||||
self.cycle: RequestResponseCycle = None # type: ignore[assignment]
|
||||
|
||||
# Protocol interface
|
||||
@ -120,7 +103,7 @@ class H11Protocol(asyncio.Protocol):
|
||||
prefix = "%s:%d - " % self.client if self.client else ""
|
||||
self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix)
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
def connection_lost(self, exc: Exception | None) -> None:
|
||||
self.connections.discard(self)
|
||||
|
||||
if self.logger.level <= TRACE_LOG_LEVEL:
|
||||
@ -153,7 +136,7 @@ class H11Protocol(asyncio.Protocol):
|
||||
self.timeout_keep_alive_task.cancel()
|
||||
self.timeout_keep_alive_task = None
|
||||
|
||||
def _get_upgrade(self) -> Optional[bytes]:
|
||||
def _get_upgrade(self) -> bytes | None:
|
||||
connection = []
|
||||
upgrade = None
|
||||
for name, value in self.headers:
|
||||
@ -167,14 +150,24 @@ class H11Protocol(asyncio.Protocol):
|
||||
|
||||
def _should_upgrade_to_ws(self) -> bool:
|
||||
if self.ws_protocol_class is None:
|
||||
if self.config.ws == "auto":
|
||||
msg = "Unsupported upgrade request."
|
||||
self.logger.warning(msg)
|
||||
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
||||
self.logger.warning(msg)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _unsupported_upgrade_warning(self) -> None:
|
||||
msg = "Unsupported upgrade request."
|
||||
self.logger.warning(msg)
|
||||
if not self._should_upgrade_to_ws():
|
||||
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
||||
self.logger.warning(msg)
|
||||
|
||||
def _should_upgrade(self) -> bool:
|
||||
upgrade = self._get_upgrade()
|
||||
if upgrade == b"websocket" and self._should_upgrade_to_ws():
|
||||
return True
|
||||
if upgrade is not None:
|
||||
self._unsupported_upgrade_warning()
|
||||
return False
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
self._unset_keepalive_if_required()
|
||||
|
||||
@ -205,34 +198,31 @@ class H11Protocol(asyncio.Protocol):
|
||||
elif isinstance(event, h11.Request):
|
||||
self.headers = [(key.lower(), value) for key, value in event.headers]
|
||||
raw_path, _, query_string = event.target.partition(b"?")
|
||||
path = unquote(raw_path.decode("ascii"))
|
||||
full_path = self.root_path + path
|
||||
full_raw_path = self.root_path.encode("ascii") + raw_path
|
||||
self.scope = {
|
||||
"type": "http",
|
||||
"asgi": {
|
||||
"version": self.config.asgi_version,
|
||||
"spec_version": "2.3",
|
||||
},
|
||||
"asgi": {"version": self.config.asgi_version, "spec_version": "2.3"},
|
||||
"http_version": event.http_version.decode("ascii"),
|
||||
"server": self.server,
|
||||
"client": self.client,
|
||||
"scheme": self.scheme, # type: ignore[typeddict-item]
|
||||
"method": event.method.decode("ascii"),
|
||||
"root_path": self.root_path,
|
||||
"path": unquote(raw_path.decode("ascii")),
|
||||
"raw_path": raw_path,
|
||||
"path": full_path,
|
||||
"raw_path": full_raw_path,
|
||||
"query_string": query_string,
|
||||
"headers": self.headers,
|
||||
"state": self.app_state.copy(),
|
||||
}
|
||||
|
||||
upgrade = self._get_upgrade()
|
||||
if upgrade == b"websocket" and self._should_upgrade_to_ws():
|
||||
if self._should_upgrade():
|
||||
self.handle_websocket_upgrade(event)
|
||||
return
|
||||
|
||||
# Handle 503 responses when 'limit_concurrency' is exceeded.
|
||||
if self.limit_concurrency is not None and (
|
||||
len(self.connections) >= self.limit_concurrency
|
||||
or len(self.tasks) >= self.limit_concurrency
|
||||
len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency
|
||||
):
|
||||
app = service_unavailable
|
||||
message = "Exceeded concurrency limit."
|
||||
@ -240,6 +230,14 @@ class H11Protocol(asyncio.Protocol):
|
||||
else:
|
||||
app = self.app
|
||||
|
||||
# When starting to process a request, disable the keep-alive
|
||||
# timeout. Normally we disable this when receiving data from
|
||||
# client and set back when finishing processing its request.
|
||||
# However, for pipelined requests processing finishes after
|
||||
# already receiving the next request and thus the timer may
|
||||
# be set here, which we don't want.
|
||||
self._unset_keepalive_if_required()
|
||||
|
||||
self.cycle = RequestResponseCycle(
|
||||
scope=self.scope,
|
||||
conn=self.conn,
|
||||
@ -252,7 +250,16 @@ class H11Protocol(asyncio.Protocol):
|
||||
message_event=asyncio.Event(),
|
||||
on_response=self.on_response_complete,
|
||||
)
|
||||
task = self.loop.create_task(self.cycle.run_asgi(app))
|
||||
if self.config.reset_contextvars:
|
||||
# Opt-in workaround for https://github.com/python/cpython/issues/140947:
|
||||
# asyncio can leak context vars between tasks. Hides context set in the
|
||||
# lifespan or by external instrumentation.
|
||||
if sys.version_info >= (3, 11): # pragma: py-lt-311
|
||||
task = self.loop.create_task(self.cycle.run_asgi(app), context=contextvars.Context())
|
||||
else: # pragma: py-gte-311
|
||||
task = contextvars.Context().run(self.loop.create_task, self.cycle.run_asgi(app))
|
||||
else:
|
||||
task = self.loop.create_task(self.cycle.run_asgi(app))
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
self.tasks.add(task)
|
||||
|
||||
@ -271,9 +278,11 @@ class H11Protocol(asyncio.Protocol):
|
||||
continue
|
||||
self.cycle.more_body = False
|
||||
self.cycle.message_event.set()
|
||||
if self.conn.their_state == h11.MUST_CLOSE:
|
||||
break
|
||||
|
||||
def handle_websocket_upgrade(self, event: h11.Request) -> None:
|
||||
if self.logger.level <= TRACE_LOG_LEVEL:
|
||||
if self.logger.level <= TRACE_LOG_LEVEL: # pragma: full coverage
|
||||
prefix = "%s:%d - " % self.client if self.client else ""
|
||||
self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix)
|
||||
|
||||
@ -293,7 +302,7 @@ class H11Protocol(asyncio.Protocol):
|
||||
|
||||
def send_400_response(self, msg: str) -> None:
|
||||
reason = STATUS_PHRASES[400]
|
||||
headers: List[Tuple[bytes, bytes]] = [
|
||||
headers: list[tuple[bytes, bytes]] = [
|
||||
(b"content-type", b"text/plain; charset=utf-8"),
|
||||
(b"connection", b"close"),
|
||||
]
|
||||
@ -318,9 +327,7 @@ class H11Protocol(asyncio.Protocol):
|
||||
# Set a short Keep-Alive timeout.
|
||||
self._unset_keepalive_if_required()
|
||||
|
||||
self.timeout_keep_alive_task = self.loop.call_later(
|
||||
self.timeout_keep_alive, self.timeout_keep_alive_handler
|
||||
)
|
||||
self.timeout_keep_alive_task = self.loop.call_later(self.timeout_keep_alive, self.timeout_keep_alive_handler)
|
||||
|
||||
# Unpause data reads if needed.
|
||||
self.flow.resume_reading()
|
||||
@ -345,13 +352,13 @@ class H11Protocol(asyncio.Protocol):
|
||||
"""
|
||||
Called by the transport when the write buffer exceeds the high water mark.
|
||||
"""
|
||||
self.flow.pause_writing()
|
||||
self.flow.pause_writing() # pragma: full coverage
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
"""
|
||||
Called by the transport when the write buffer drops below the low water mark.
|
||||
"""
|
||||
self.flow.resume_writing()
|
||||
self.flow.resume_writing() # pragma: full coverage
|
||||
|
||||
def timeout_keep_alive_handler(self) -> None:
|
||||
"""
|
||||
@ -367,14 +374,14 @@ class H11Protocol(asyncio.Protocol):
|
||||
class RequestResponseCycle:
|
||||
def __init__(
|
||||
self,
|
||||
scope: "HTTPScope",
|
||||
scope: HTTPScope,
|
||||
conn: h11.Connection,
|
||||
transport: asyncio.Transport,
|
||||
flow: FlowControl,
|
||||
logger: logging.Logger,
|
||||
access_logger: logging.Logger,
|
||||
access_log: bool,
|
||||
default_headers: List[Tuple[bytes, bytes]],
|
||||
default_headers: list[tuple[bytes, bytes]],
|
||||
message_event: asyncio.Event,
|
||||
on_response: Callable[..., None],
|
||||
) -> None:
|
||||
@ -395,7 +402,7 @@ class RequestResponseCycle:
|
||||
self.waiting_for_100_continue = conn.they_are_waiting_for_100_continue
|
||||
|
||||
# Request state
|
||||
self.body = b""
|
||||
self.body = bytearray()
|
||||
self.more_body = True
|
||||
|
||||
# Response state
|
||||
@ -403,7 +410,7 @@ class RequestResponseCycle:
|
||||
self.response_complete = False
|
||||
|
||||
# ASGI exception wrapper
|
||||
async def run_asgi(self, app: "ASGI3Application") -> None:
|
||||
async def run_asgi(self, app: ASGI3Application) -> None:
|
||||
try:
|
||||
result = await app( # type: ignore[func-returns-value]
|
||||
self.scope, self.receive, self.send
|
||||
@ -432,7 +439,7 @@ class RequestResponseCycle:
|
||||
self.on_response = lambda: None
|
||||
|
||||
async def send_500_response(self) -> None:
|
||||
response_start_event: "HTTPResponseStartEvent" = {
|
||||
response_start_event: HTTPResponseStartEvent = {
|
||||
"type": "http.response.start",
|
||||
"status": 500,
|
||||
"headers": [
|
||||
@ -441,7 +448,7 @@ class RequestResponseCycle:
|
||||
],
|
||||
}
|
||||
await self.send(response_start_event)
|
||||
response_body_event: "HTTPResponseBodyEvent" = {
|
||||
response_body_event: HTTPResponseBodyEvent = {
|
||||
"type": "http.response.body",
|
||||
"body": b"Internal Server Error",
|
||||
"more_body": False,
|
||||
@ -449,21 +456,17 @@ class RequestResponseCycle:
|
||||
await self.send(response_body_event)
|
||||
|
||||
# ASGI interface
|
||||
async def send(self, message: "ASGISendEvent") -> None:
|
||||
message_type = message["type"]
|
||||
|
||||
async def send(self, message: ASGISendEvent) -> None:
|
||||
if self.flow.write_paused and not self.disconnected:
|
||||
await self.flow.drain()
|
||||
await self.flow.drain() # pragma: full coverage
|
||||
|
||||
if self.disconnected:
|
||||
return
|
||||
return # pragma: full coverage
|
||||
|
||||
if not self.response_started:
|
||||
# Sending response status line and headers
|
||||
if message_type != "http.response.start":
|
||||
msg = "Expected ASGI message 'http.response.start', but got '%s'."
|
||||
raise RuntimeError(msg % message_type)
|
||||
message = cast("HTTPResponseStartEvent", message)
|
||||
if message["type"] != "http.response.start":
|
||||
raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.")
|
||||
|
||||
self.response_started = True
|
||||
self.waiting_for_100_continue = False
|
||||
@ -492,10 +495,8 @@ class RequestResponseCycle:
|
||||
|
||||
elif not self.response_complete:
|
||||
# Sending response body
|
||||
if message_type != "http.response.body":
|
||||
msg = "Expected ASGI message 'http.response.body', but got '%s'."
|
||||
raise RuntimeError(msg % message_type)
|
||||
message = cast("HTTPResponseBodyEvent", message)
|
||||
if message["type"] != "http.response.body":
|
||||
raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.")
|
||||
|
||||
body = message.get("body", b"")
|
||||
more_body = message.get("more_body", False)
|
||||
@ -514,8 +515,7 @@ class RequestResponseCycle:
|
||||
|
||||
else:
|
||||
# Response already sent
|
||||
msg = "Unexpected ASGI message '%s' sent, after response already completed."
|
||||
raise RuntimeError(msg % message_type)
|
||||
raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.")
|
||||
|
||||
if self.response_complete:
|
||||
if self.conn.our_state is h11.MUST_CLOSE or not self.keep_alive:
|
||||
@ -523,12 +523,10 @@ class RequestResponseCycle:
|
||||
self.transport.close()
|
||||
self.on_response()
|
||||
|
||||
async def receive(self) -> "ASGIReceiveEvent":
|
||||
async def receive(self) -> ASGIReceiveEvent:
|
||||
if self.waiting_for_100_continue and not self.transport.is_closing():
|
||||
headers: List[Tuple[str, str]] = []
|
||||
event = h11.InformationalResponse(
|
||||
status_code=100, headers=headers, reason="Continue"
|
||||
)
|
||||
headers: list[tuple[str, str]] = []
|
||||
event = h11.InformationalResponse(status_code=100, headers=headers, reason="Continue")
|
||||
output = self.conn.send(event=event)
|
||||
self.transport.write(output)
|
||||
self.waiting_for_100_continue = False
|
||||
@ -541,10 +539,6 @@ class RequestResponseCycle:
|
||||
if self.disconnected or self.response_complete:
|
||||
return {"type": "http.disconnect"}
|
||||
|
||||
message: "HTTPRequestEvent" = {
|
||||
"type": "http.request",
|
||||
"body": self.body,
|
||||
"more_body": self.more_body,
|
||||
}
|
||||
self.body = b""
|
||||
message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body}
|
||||
self.body = bytearray()
|
||||
return message
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import http
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import urllib
|
||||
from asyncio.events import TimerHandle
|
||||
from collections import deque
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Deque,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal
|
||||
|
||||
import httptools
|
||||
|
||||
@ -24,31 +18,17 @@ from uvicorn._types import (
|
||||
ASGI3Application,
|
||||
ASGIReceiveEvent,
|
||||
ASGISendEvent,
|
||||
HTTPDisconnectEvent,
|
||||
HTTPRequestEvent,
|
||||
HTTPResponseBodyEvent,
|
||||
HTTPResponseStartEvent,
|
||||
HTTPScope,
|
||||
)
|
||||
from uvicorn.config import Config
|
||||
from uvicorn.logging import TRACE_LOG_LEVEL
|
||||
from uvicorn.protocols.http.flow_control import (
|
||||
CLOSE_HEADER,
|
||||
HIGH_WATER_LIMIT,
|
||||
FlowControl,
|
||||
service_unavailable,
|
||||
)
|
||||
from uvicorn.protocols.utils import (
|
||||
get_client_addr,
|
||||
get_local_addr,
|
||||
get_path_with_query_string,
|
||||
get_remote_addr,
|
||||
is_ssl,
|
||||
)
|
||||
from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
|
||||
from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
|
||||
from uvicorn.server import ServerState
|
||||
|
||||
HEADER_RE = re.compile(b'[\x00-\x1F\x7F()<>@,;:[]={} \t\\"]')
|
||||
HEADER_VALUE_RE = re.compile(b"[\x00-\x1F\x7F]")
|
||||
HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:\\[\\]={} \t\\\\"]')
|
||||
HEADER_VALUE_RE = re.compile(b"[\x00-\x08\x0a-\x1f\x7f]")
|
||||
|
||||
|
||||
def _get_status_line(status_code: int) -> bytes:
|
||||
@ -59,9 +39,7 @@ def _get_status_line(status_code: int) -> bytes:
|
||||
return b"".join([b"HTTP/1.1 ", str(status_code).encode(), b" ", phrase, b"\r\n"])
|
||||
|
||||
|
||||
STATUS_LINE = {
|
||||
status_code: _get_status_line(status_code) for status_code in range(100, 600)
|
||||
}
|
||||
STATUS_LINE = {status_code: _get_status_line(status_code) for status_code in range(100, 600)}
|
||||
|
||||
|
||||
class HttpToolsProtocol(asyncio.Protocol):
|
||||
@ -69,8 +47,8 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
self,
|
||||
config: Config,
|
||||
server_state: ServerState,
|
||||
app_state: Dict[str, Any],
|
||||
_loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
app_state: dict[str, Any],
|
||||
_loop: asyncio.AbstractEventLoop | None = None,
|
||||
) -> None:
|
||||
if not config.loaded:
|
||||
config.load()
|
||||
@ -82,13 +60,21 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
self.access_logger = logging.getLogger("uvicorn.access")
|
||||
self.access_log = self.access_logger.hasHandlers()
|
||||
self.parser = httptools.HttpRequestParser(self)
|
||||
|
||||
try:
|
||||
# Enable dangerous leniencies to allow server to a response on the first request from a pipelined request.
|
||||
self.parser.set_dangerous_leniencies(lenient_data_after_close=True)
|
||||
except AttributeError: # pragma: no cover
|
||||
# httptools < 0.6.3
|
||||
pass
|
||||
|
||||
self.ws_protocol_class = config.ws_protocol_class
|
||||
self.root_path = config.root_path
|
||||
self.limit_concurrency = config.limit_concurrency
|
||||
self.app_state = app_state
|
||||
|
||||
# Timeouts
|
||||
self.timeout_keep_alive_task: Optional[TimerHandle] = None
|
||||
self.timeout_keep_alive_task: TimerHandle | None = None
|
||||
self.timeout_keep_alive = config.timeout_keep_alive
|
||||
|
||||
# Global state
|
||||
@ -99,14 +85,14 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
# Per-connection state
|
||||
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
||||
self.flow: FlowControl = None # type: ignore[assignment]
|
||||
self.server: Optional[Tuple[str, int]] = None
|
||||
self.client: Optional[Tuple[str, int]] = None
|
||||
self.scheme: Optional[Literal["http", "https"]] = None
|
||||
self.pipeline: Deque[Tuple[RequestResponseCycle, ASGI3Application]] = deque()
|
||||
self.server: tuple[str, int | None] | None = None
|
||||
self.client: tuple[str, int] | None = None
|
||||
self.scheme: Literal["http", "https"] | None = None
|
||||
self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque()
|
||||
|
||||
# Per-request state
|
||||
self.scope: HTTPScope = None # type: ignore[assignment]
|
||||
self.headers: List[Tuple[bytes, bytes]] = None # type: ignore[assignment]
|
||||
self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment]
|
||||
self.expect_100_continue = False
|
||||
self.cycle: RequestResponseCycle = None # type: ignore[assignment]
|
||||
|
||||
@ -126,7 +112,7 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
prefix = "%s:%d - " % self.client if self.client else ""
|
||||
self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix)
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
def connection_lost(self, exc: Exception | None) -> None:
|
||||
self.connections.discard(self)
|
||||
|
||||
if self.logger.level <= TRACE_LOG_LEVEL:
|
||||
@ -153,7 +139,7 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
self.timeout_keep_alive_task.cancel()
|
||||
self.timeout_keep_alive_task = None
|
||||
|
||||
def _get_upgrade(self) -> Optional[bytes]:
|
||||
def _get_upgrade(self) -> bytes | None:
|
||||
connection = []
|
||||
upgrade = None
|
||||
for name, value in self.headers:
|
||||
@ -163,21 +149,22 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
upgrade = value.lower()
|
||||
if b"upgrade" in connection:
|
||||
return upgrade
|
||||
return None
|
||||
return None # pragma: full coverage
|
||||
|
||||
def _should_upgrade_to_ws(self, upgrade: Optional[bytes]) -> bool:
|
||||
if upgrade == b"websocket" and self.ws_protocol_class is not None:
|
||||
return True
|
||||
if self.config.ws == "auto":
|
||||
msg = "Unsupported upgrade request."
|
||||
self.logger.warning(msg)
|
||||
def _should_upgrade_to_ws(self) -> bool:
|
||||
if self.ws_protocol_class is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _unsupported_upgrade_warning(self) -> None:
|
||||
self.logger.warning("Unsupported upgrade request.")
|
||||
if not self._should_upgrade_to_ws():
|
||||
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
||||
self.logger.warning(msg)
|
||||
return False
|
||||
|
||||
def _should_upgrade(self) -> bool:
|
||||
upgrade = self._get_upgrade()
|
||||
return self._should_upgrade_to_ws(upgrade)
|
||||
return upgrade == b"websocket" and self._should_upgrade_to_ws()
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
self._unset_keepalive_if_required()
|
||||
@ -190,9 +177,10 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
self.send_400_response(msg)
|
||||
return
|
||||
except httptools.HttpParserUpgrade:
|
||||
upgrade = self._get_upgrade()
|
||||
if self._should_upgrade_to_ws(upgrade):
|
||||
if self._should_upgrade():
|
||||
self.handle_websocket_upgrade()
|
||||
else:
|
||||
self._unsupported_upgrade_warning()
|
||||
|
||||
def handle_websocket_upgrade(self) -> None:
|
||||
if self.logger.level <= TRACE_LOG_LEVEL:
|
||||
@ -217,7 +205,7 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
def send_400_response(self, msg: str) -> None:
|
||||
content = [STATUS_LINE[400]]
|
||||
for name, value in self.server_state.default_headers:
|
||||
content.extend([name, b": ", value, b"\r\n"])
|
||||
content.extend([name, b": ", value, b"\r\n"]) # pragma: full coverage
|
||||
content.extend(
|
||||
[
|
||||
b"content-type: text/plain; charset=utf-8\r\n",
|
||||
@ -269,14 +257,15 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
path = raw_path.decode("ascii")
|
||||
if "%" in path:
|
||||
path = urllib.parse.unquote(path)
|
||||
self.scope["path"] = path
|
||||
self.scope["raw_path"] = raw_path
|
||||
full_path = self.root_path + path
|
||||
full_raw_path = self.root_path.encode("ascii") + raw_path
|
||||
self.scope["path"] = full_path
|
||||
self.scope["raw_path"] = full_raw_path
|
||||
self.scope["query_string"] = parsed_url.query or b""
|
||||
|
||||
# Handle 503 responses when 'limit_concurrency' is exceeded.
|
||||
if self.limit_concurrency is not None and (
|
||||
len(self.connections) >= self.limit_concurrency
|
||||
or len(self.tasks) >= self.limit_concurrency
|
||||
len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency
|
||||
):
|
||||
app = service_unavailable
|
||||
message = "Exceeded concurrency limit."
|
||||
@ -300,18 +289,28 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
)
|
||||
if existing_cycle is None or existing_cycle.response_complete:
|
||||
# Standard case - start processing the request.
|
||||
task = self.loop.create_task(self.cycle.run_asgi(app))
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
self.tasks.add(task)
|
||||
self._start_asgi_task(self.cycle, app)
|
||||
else:
|
||||
# Pipelined HTTP requests need to be queued up.
|
||||
self.flow.pause_reading()
|
||||
self.pipeline.appendleft((self.cycle, app))
|
||||
|
||||
def _start_asgi_task(self, cycle: RequestResponseCycle, app: ASGI3Application) -> None:
|
||||
if self.config.reset_contextvars:
|
||||
# Opt-in workaround for https://github.com/python/cpython/issues/140947:
|
||||
# asyncio can leak context vars between tasks. Hides context set in the
|
||||
# lifespan or by external instrumentation.
|
||||
if sys.version_info >= (3, 11): # pragma: py-lt-311
|
||||
task = self.loop.create_task(cycle.run_asgi(app), context=contextvars.Context())
|
||||
else: # pragma: py-gte-311
|
||||
task = contextvars.Context().run(self.loop.create_task, cycle.run_asgi(app))
|
||||
else:
|
||||
task = self.loop.create_task(cycle.run_asgi(app))
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
self.tasks.add(task)
|
||||
|
||||
def on_body(self, body: bytes) -> None:
|
||||
if (
|
||||
self.parser.should_upgrade() and self._should_upgrade()
|
||||
) or self.cycle.response_complete:
|
||||
if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete:
|
||||
return
|
||||
self.cycle.body += body
|
||||
if len(self.cycle.body) > HIGH_WATER_LIMIT:
|
||||
@ -319,9 +318,7 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
self.cycle.message_event.set()
|
||||
|
||||
def on_message_complete(self) -> None:
|
||||
if (
|
||||
self.parser.should_upgrade() and self._should_upgrade()
|
||||
) or self.cycle.response_complete:
|
||||
if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete:
|
||||
return
|
||||
self.cycle.more_body = False
|
||||
self.cycle.message_event.set()
|
||||
@ -333,22 +330,20 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
if self.transport.is_closing():
|
||||
return
|
||||
|
||||
# Set a short Keep-Alive timeout.
|
||||
self._unset_keepalive_if_required()
|
||||
|
||||
self.timeout_keep_alive_task = self.loop.call_later(
|
||||
self.timeout_keep_alive, self.timeout_keep_alive_handler
|
||||
)
|
||||
|
||||
# Unpause data reads if needed.
|
||||
self.flow.resume_reading()
|
||||
|
||||
# Unblock any pipelined events.
|
||||
# Unblock any pipelined events. If there are none, arm the
|
||||
# Keep-Alive timeout instead.
|
||||
if self.pipeline:
|
||||
cycle, app = self.pipeline.pop()
|
||||
task = self.loop.create_task(cycle.run_asgi(app))
|
||||
task.add_done_callback(self.tasks.discard)
|
||||
self.tasks.add(task)
|
||||
self._start_asgi_task(cycle, app)
|
||||
else:
|
||||
self.timeout_keep_alive_task = self.loop.call_later(
|
||||
self.timeout_keep_alive, self.timeout_keep_alive_handler
|
||||
)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""
|
||||
@ -363,13 +358,13 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
"""
|
||||
Called by the transport when the write buffer exceeds the high water mark.
|
||||
"""
|
||||
self.flow.pause_writing()
|
||||
self.flow.pause_writing() # pragma: full coverage
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
"""
|
||||
Called by the transport when the write buffer drops below the low water mark.
|
||||
"""
|
||||
self.flow.resume_writing()
|
||||
self.flow.resume_writing() # pragma: full coverage
|
||||
|
||||
def timeout_keep_alive_handler(self) -> None:
|
||||
"""
|
||||
@ -383,13 +378,13 @@ class HttpToolsProtocol(asyncio.Protocol):
|
||||
class RequestResponseCycle:
|
||||
def __init__(
|
||||
self,
|
||||
scope: "HTTPScope",
|
||||
scope: HTTPScope,
|
||||
transport: asyncio.Transport,
|
||||
flow: FlowControl,
|
||||
logger: logging.Logger,
|
||||
access_logger: logging.Logger,
|
||||
access_log: bool,
|
||||
default_headers: List[Tuple[bytes, bytes]],
|
||||
default_headers: list[tuple[bytes, bytes]],
|
||||
message_event: asyncio.Event,
|
||||
expect_100_continue: bool,
|
||||
keep_alive: bool,
|
||||
@ -411,17 +406,17 @@ class RequestResponseCycle:
|
||||
self.waiting_for_100_continue = expect_100_continue
|
||||
|
||||
# Request state
|
||||
self.body = b""
|
||||
self.body = bytearray()
|
||||
self.more_body = True
|
||||
|
||||
# Response state
|
||||
self.response_started = False
|
||||
self.response_complete = False
|
||||
self.chunked_encoding: Optional[bool] = None
|
||||
self.chunked_encoding: bool | None = None
|
||||
self.expected_content_length = 0
|
||||
|
||||
# ASGI exception wrapper
|
||||
async def run_asgi(self, app: "ASGI3Application") -> None:
|
||||
async def run_asgi(self, app: ASGI3Application) -> None:
|
||||
try:
|
||||
result = await app( # type: ignore[func-returns-value]
|
||||
self.scope, self.receive, self.send
|
||||
@ -450,38 +445,31 @@ class RequestResponseCycle:
|
||||
self.on_response = lambda: None
|
||||
|
||||
async def send_500_response(self) -> None:
|
||||
response_start_event: "HTTPResponseStartEvent" = {
|
||||
"type": "http.response.start",
|
||||
"status": 500,
|
||||
"headers": [
|
||||
(b"content-type", b"text/plain; charset=utf-8"),
|
||||
(b"connection", b"close"),
|
||||
],
|
||||
}
|
||||
await self.send(response_start_event)
|
||||
response_body_event: "HTTPResponseBodyEvent" = {
|
||||
"type": "http.response.body",
|
||||
"body": b"Internal Server Error",
|
||||
"more_body": False,
|
||||
}
|
||||
await self.send(response_body_event)
|
||||
await self.send(
|
||||
{
|
||||
"type": "http.response.start",
|
||||
"status": 500,
|
||||
"headers": [
|
||||
(b"content-type", b"text/plain; charset=utf-8"),
|
||||
(b"content-length", b"21"),
|
||||
(b"connection", b"close"),
|
||||
],
|
||||
}
|
||||
)
|
||||
await self.send({"type": "http.response.body", "body": b"Internal Server Error", "more_body": False})
|
||||
|
||||
# ASGI interface
|
||||
async def send(self, message: "ASGISendEvent") -> None:
|
||||
message_type = message["type"]
|
||||
|
||||
async def send(self, message: ASGISendEvent) -> None:
|
||||
if self.flow.write_paused and not self.disconnected:
|
||||
await self.flow.drain()
|
||||
await self.flow.drain() # pragma: full coverage
|
||||
|
||||
if self.disconnected:
|
||||
return
|
||||
return # pragma: full coverage
|
||||
|
||||
if not self.response_started:
|
||||
# Sending response status line and headers
|
||||
if message_type != "http.response.start":
|
||||
msg = "Expected ASGI message 'http.response.start', but got '%s'."
|
||||
raise RuntimeError(msg % message_type)
|
||||
message = cast("HTTPResponseStartEvent", message)
|
||||
if message["type"] != "http.response.start":
|
||||
raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.")
|
||||
|
||||
self.response_started = True
|
||||
self.waiting_for_100_continue = False
|
||||
@ -507,7 +495,7 @@ class RequestResponseCycle:
|
||||
|
||||
for name, value in headers:
|
||||
if HEADER_RE.search(name):
|
||||
raise RuntimeError("Invalid HTTP header name.")
|
||||
raise RuntimeError("Invalid HTTP header name.") # pragma: full coverage
|
||||
if HEADER_VALUE_RE.search(value):
|
||||
raise RuntimeError("Invalid HTTP header value.")
|
||||
|
||||
@ -522,11 +510,7 @@ class RequestResponseCycle:
|
||||
self.keep_alive = False
|
||||
content.extend([name, b": ", value, b"\r\n"])
|
||||
|
||||
if (
|
||||
self.chunked_encoding is None
|
||||
and self.scope["method"] != "HEAD"
|
||||
and status_code not in (204, 304)
|
||||
):
|
||||
if self.chunked_encoding is None and self.scope["method"] != "HEAD" and status_code not in (204, 304):
|
||||
# Neither content-length nor transfer-encoding specified
|
||||
self.chunked_encoding = True
|
||||
content.append(b"transfer-encoding: chunked\r\n")
|
||||
@ -536,11 +520,10 @@ class RequestResponseCycle:
|
||||
|
||||
elif not self.response_complete:
|
||||
# Sending response body
|
||||
if message_type != "http.response.body":
|
||||
msg = "Expected ASGI message 'http.response.body', but got '%s'."
|
||||
raise RuntimeError(msg % message_type)
|
||||
if message["type"] != "http.response.body":
|
||||
raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.")
|
||||
|
||||
body = cast(bytes, message.get("body", b""))
|
||||
body = message.get("body", b"")
|
||||
more_body = message.get("more_body", False)
|
||||
|
||||
# Write response body
|
||||
@ -574,10 +557,9 @@ class RequestResponseCycle:
|
||||
|
||||
else:
|
||||
# Response already sent
|
||||
msg = "Unexpected ASGI message '%s' sent, after response already completed."
|
||||
raise RuntimeError(msg % message_type)
|
||||
raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.")
|
||||
|
||||
async def receive(self) -> "ASGIReceiveEvent":
|
||||
async def receive(self) -> ASGIReceiveEvent:
|
||||
if self.waiting_for_100_continue and not self.transport.is_closing():
|
||||
self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||||
self.waiting_for_100_continue = False
|
||||
@ -587,15 +569,8 @@ class RequestResponseCycle:
|
||||
await self.message_event.wait()
|
||||
self.message_event.clear()
|
||||
|
||||
message: "Union[HTTPDisconnectEvent, HTTPRequestEvent]"
|
||||
if self.disconnected or self.response_complete:
|
||||
message = {"type": "http.disconnect"}
|
||||
else:
|
||||
message = {
|
||||
"type": "http.request",
|
||||
"body": self.body,
|
||||
"more_body": self.more_body,
|
||||
}
|
||||
self.body = b""
|
||||
|
||||
return {"type": "http.disconnect"}
|
||||
message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body}
|
||||
self.body = bytearray()
|
||||
return message
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user