Compare commits

..

13 Commits

Author SHA1 Message Date
Thomas Grainger
577c0570eb
ignore coverage files that show up in reloader tests 2024-10-01 12:06:29 +01:00
Thomas Grainger
47924f253f
enable sigterm hook 2024-10-01 12:05:48 +01:00
Thomas Grainger
bb6dc58eec
try to collect coverage 2024-10-01 11:25:33 +01:00
Thomas Grainger
d5a1f59be4
test unsupported 2024-10-01 10:58:59 +01:00
Thomas Grainger
77a4a23da4
change box to dataclass to avoid coverage requirement 2024-10-01 10:54:08 +01:00
Thomas Grainger
4166deed90
add coverage pragmas 2024-10-01 10:48:00 +01:00
Thomas Grainger
66afb6229a
remove print and sleep 2024-10-01 10:39:16 +01:00
Thomas Grainger
d9c117a2f3
add cli usage 2024-10-01 10:26:06 +01:00
Thomas Grainger
0af6480dd6
mypy fix 2 2024-10-01 10:19:45 +01:00
Thomas Grainger
2a9b20a7fa
mypy fix 2024-10-01 10:16:04 +01:00
Thomas Grainger
4a0785af65
add a test 2024-10-01 10:12:27 +01:00
Thomas Grainger
1550e7a10d
fix it so it actually runs 2024-10-01 10:12:10 +01:00
Thomas Grainger
3e2854ac30
add socket-load-balance flag 2024-09-30 15:30:39 +01:00
100 changed files with 2596 additions and 7574 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: encode

View File

@ -1,9 +1,11 @@
# 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/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. 💬
- 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.

View File

@ -1,11 +1,9 @@
version: 2
updates:
- package-ecosystem: "uv"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "monthly"
cooldown:
default-days: 7
groups:
python-packages:
patterns:
@ -14,9 +12,3 @@ updates:
directory: "/"
schedule:
interval: monthly
cooldown:
default-days: 7
groups:
github-actions:
patterns:
- "*"

View File

@ -1,36 +0,0 @@
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

View File

@ -1,115 +0,0 @@
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) }}

View File

@ -4,126 +4,26 @@ on:
push:
tags:
- '*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
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.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
publish:
name: "Publish release"
runs-on: "ubuntu-latest"
environment:
name: pypi
url: https://pypi.org/project/uvicorn
name: deploy
steps:
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: "actions/checkout@v4"
- uses: "actions/setup-python@v5"
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
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 }}

39
.github/workflows/test-suite.yml vendored Normal file
View File

@ -0,0 +1,39 @@
---
name: Test Suite
on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
jobs:
tests:
name: "Python ${{ matrix.python-version }} ${{ matrix.os }}"
runs-on: "${{ matrix.os }}"
timeout-minutes: 30
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@v5"
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

View File

@ -1,25 +0,0 @@
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
View File

@ -1,6 +1,5 @@
.cache
.coverage
.coverage.*
.mypy_cache/
__pycache__/
uvicorn.egg-info/
@ -8,4 +7,3 @@ venv/
htmlcov/
site/
dist/
.codspeed/

View File

@ -1 +0,0 @@
docs/release-notes.md

616
CHANGELOG.md Normal file
View File

@ -0,0 +1,616 @@
# Change Log
## 0.31.0 (2024-09-27)
### 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 (2024-08-13)
### Fixed
- Don't warn when upgrade is not WebSocket and depedencies are installed (#2360)
## 0.30.5 (2024-08-02)
### Fixed
- Don't close connection before receiving body on H11 (#2408)
## 0.30.4 (2024-07-31)
### Fixed
- Close connection when `h11` sets client state to `MUST_CLOSE` (#2375)
## 0.30.3 (2024-07-20)
### Fixed
- Suppress `KeyboardInterrupt` from CLI and programmatic usage (#2384)
- `ClientDisconnect` inherits from `OSError` instead of `IOError` (#2393)
## 0.30.2 (2024-07-20)
### 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 (2024-06-02)
### Fixed
- Allow horizontal tabs `\t` in response header values (#2345)
## 0.30.0 (2024-05-28)
### 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 (2024-03-19)
### Added
- Cooperative signal handling (#1600)
## 0.28.1 (2024-03-19)
### Fixed
- Revert raise `ClientDisconnected` on HTTP (#2276)
## 0.28.0 (2024-03-09)
### 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 (2024-02-10)
- Fix spurious LocalProtocolError errors when processing pipelined requests (#2243)
## 0.27.0.post1 (2024-01-29)
### Fixed
- Fix nav overrides for newer version of Mkdocs Material (#2233)
## 0.27.0 (2024-01-22)
### Added
- Raise `ClientDisconnect(IOError)` on `send()` when client disconnected (#2218)
- Bump ASGI WebSocket spec version to 2.4 (#2221)
## 0.26.0 (2024-01-16)
### 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 (2023-12-17)
### 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 (2023-11-06)
### Fixed
- Revert mkdocs-material from 9.1.21 to 9.2.6 (#2148)
## 0.24.0 (2023-11-04)
### Added
- Support Python 3.12 (#2145)
- Allow setting `app` via environment variable `UVICORN_APP` (#2106)
## 0.23.2 (2023-07-31)
### Fixed
- Maintain the same behavior of `websockets` from 10.4 on 11.0 (#2061)
## 0.23.1 (2023-07-18)
### Fixed
- Add `typing_extensions` for Python 3.10 and lower (#2053)
## 0.23.0 (2023-07-10)
### 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 (2023-04-28)
### 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 (2023-03-16)
### Fixed
- Reset lifespan state on each request (#1903)
## 0.21.0 (2023-03-09)
### 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 (2022-11-20)
### 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 (2022-10-19)
### 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 (2022-08-24)
### 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 (2022-06-27)
### Fixed
- Add default `log_config` on `uvicorn.run()` (#1541)
- Revert `logging` file name modification (#1543)
## 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)
## 0.18.0 (2022-06-23)
### 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 (2022-03-11)
### Changed
- Change `httptools` range to `>=0.4.0` (#1400)
## 0.17.5 (2022-02-16)
### 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 (2022-02-04)
### Fixed
- Replace `create_server` by `create_unix_server` (#1362)
## 0.17.3 (2022-02-03)
### Fixed
- Drop wsproto version checking. (#1359)
## 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)
- 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 (2022-01-28)
### 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 (2022-01-24)
### Fixed
- Add the `python_requires` version specifier (#1328)
## 0.17.0 (2022-01-14)
### 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 (2021-12-08)
### 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)
- Add explicit casting on click options (#1217)
- Allow WebSocket close event to receive reason being None from ASGI app. (#1259)
- 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)
- The option `--reload-dirs` was splitting a string into single character directories. (#1267)
- Only second SIGINT is able to forcefully shutdown the server (#1269)
- Allow app-dir parameter on the run() function (#1271)
## 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)
- 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 the need of `httptools` on minimal installation (#1135)
- Fix ping parameters annotation in Config class (#1127)
## 0.14.0 (2021-06-01)
### 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 (2021-02-20)
### 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 (2020-12-29)
### 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 (2020-12-12)
### Fixed
- Log full exception traceback in case of invalid HTTP request. (#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`. (#704 and #881)
- Ensure connection `scope` doesn't leak in logs when using JSON log formatters. (#859 and #884)
## 0.13.0 (2020-12-08)
### 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 (2020-11-21)
### 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 (2020-10-19)
### 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 (2020-09-30)
### 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 (2020-09-28)
### 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 (2020-07-30)
* 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 (2020-28-07)
* 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
* 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`.

View File

@ -1,23 +0,0 @@
# 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

View File

@ -1,5 +1,5 @@
<p align="center">
<img width="320" height="320" src="https://raw.githubusercontent.com/tomchristie/uvicorn/main/docs/uvicorn.png" alt='uvicorn'>
<img width="320" height="320" src="https://raw.githubusercontent.com/tomchristie/uvicorn/master/docs/uvicorn.png" alt='uvicorn'>
</p>
<p align="center">
@ -8,16 +8,11 @@
---
[![Build Status](https://github.com/Kludex/uvicorn/workflows/Test%20Suite/badge.svg)](https://github.com/Kludex/uvicorn/actions)
[![Build Status](https://github.com/encode/uvicorn/workflows/Test%20Suite/badge.svg)](https://github.com/encode/uvicorn/actions)
[![Package version](https://badge.fury.io/py/uvicorn.svg)](https://pypi.python.org/pypi/uvicorn)
[![Supported Python Version](https://img.shields.io/pypi/pyversions/uvicorn.svg?color=%2334D058)](https://pypi.org/project/uvicorn)
[![Discord](https://img.shields.io/discord/1051468649518616576?logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/RxKUF5JuHs)
---
**Documentation**: [https://uvicorn.dev](https://uvicorn.dev)
**Source Code**: [https://www.github.com/Kludex/uvicorn](https://www.github.com/Kludex/uvicorn)
**Documentation**: [https://www.uvicorn.org](https://www.uvicorn.org)
---
@ -142,7 +137,7 @@ $ hypercorn app:App
---
<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/>&mdash; 🦄 &mdash;</p>
<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/>&mdash; 🦄 &mdash;</p>
[asgi]: https://asgi.readthedocs.io/en/latest/
[daphne]: https://github.com/django/daphne

View File

@ -1,279 +0,0 @@
## 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 doesnt 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.

View File

@ -1,75 +0,0 @@
# 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
```

View File

@ -1,109 +0,0 @@
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.

View File

@ -1,319 +0,0 @@
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.

View File

@ -1,107 +0,0 @@
**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.

View File

@ -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/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)
- 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)
- 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://discord.com/invite/SWU73HffbV)
or at the [github discussions tab](https://github.com/Kludex/uvicorn/discussions).
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).
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/Kludex/uvicorn) on GitHub.
[Uvicorn repository](https://github.com/encode/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/Kludex/uvicorn/main/docs/img/gh-actions-fail.png" alt='Failing PR commit status'>
<img src="https://raw.githubusercontent.com/encode/uvicorn/master/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/Kludex/uvicorn/main/docs/img/gh-actions-fail-check.png" alt='Failing GitHub action lint job'>
<img src="https://raw.githubusercontent.com/encode/uvicorn/master/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.
@ -157,20 +157,23 @@ 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/Kludex/uvicorn/compare/) `main` with the tag of the latest release, and list all entries that are of interest to our users:
- [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:
- 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/Kludex/uvicorn/pull/1107).
For an example, see [#1006](https://github.com/encode/uvicorn/pull/1107).
Once the release PR is merged, create a
[new release](https://github.com/Kludex/uvicorn/releases/new) including:
[new release](https://github.com/encode/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.

View File

@ -1,57 +0,0 @@
.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);
}

View File

@ -1,3 +1,5 @@
# 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:
@ -23,11 +25,122 @@ The `--reload` and `--workers` arguments are **mutually exclusive**.
To see the complete set of available options, use `uvicorn --help`:
```bash
{{ 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 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.
--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.
--socket-load-balance Use kernel support for socket load balancing
--help Show this message and exit.
```
See the [settings documentation](../settings.md) for more details on the supported options for running uvicorn.
See the [settings documentation](settings.md) for more details on the supported options for running uvicorn.
## Running programmatically
@ -82,7 +195,7 @@ The default process manager monitors the status of child processes and automatic
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.
- `SIGHUP`: Work processeses 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.
@ -141,6 +254,28 @@ 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.
@ -149,12 +284,11 @@ In managed environments such as `Heroku`, you won't typically need to configure
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.
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).
It also includes some basic configuration to forward websocket connections. For more info on this, check [Nginx recommendations][nginx_websocket].
```conf
http {
@ -208,9 +342,9 @@ Content Delivery Networks can also be a low-effort way to provide HTTPS terminat
## 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/).
The recommended way to get them is using [Let's Encrypt][letsencrypt].
For local development with https, it's possible to use [mkcert](https://github.com/FiloSottile/mkcert)
For local development with https, it's possible to use [mkcert][mkcert]
to generate a valid certificate and private key.
```bash
@ -225,35 +359,9 @@ It's also possible to use certificates with uvicorn's worker for gunicorn.
$ 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.
[nginx_websocket]: https://nginx.org/en/docs/http/websocket.html
[letsencrypt]: https://letsencrypt.org/
[mkcert]: https://github.com/FiloSottile/mkcert
## Proxies and Forwarded Headers
@ -269,12 +377,12 @@ Uvicorn can use these headers to correctly set the client and protocol in the re
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`.
or Literals (e.g. `/path/to/socket.sock`). When running from CLI these are configured using `--forwarded-trust-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).
For more information, check [`ProxyHeadersMiddleware`](https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py).
### Client Port

View File

@ -1,153 +0,0 @@
# 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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,10 +1,3 @@
<style>
.md-typeset h1,
.md-content__button {
display: none;
}
</style>
<p align="center">
<img width="320" height="320" src="../../uvicorn.png" alt='uvicorn'>
</p>
@ -14,8 +7,8 @@
</p>
<p align="center">
<a href="https://github.com/Kludex/uvicorn/actions">
<img src="https://github.com/Kludex/uvicorn/workflows/Test%20Suite/badge.svg" alt="Test Suite">
<a href="https://github.com/encode/uvicorn/actions">
<img src="https://github.com/encode/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">
@ -23,58 +16,53 @@
<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>
---
**Documentation**: [https://uvicorn.dev](https://uvicorn.dev)
# Introduction
**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.
Uvicorn is an ASGI web server implementation for Python.
Until recently Python has lacked a minimal low-level server/application interface for
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.
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.
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
**Uvicorn** is available on [PyPI](https://pypi.org/project/uvicorn/) so installation is as simple as:
Install using `pip`:
=== "pip"
```bash
pip install uvicorn
```
```shell
$ pip install uvicorn
```
=== "uv"
```bash
uv add uvicorn
```
This will install uvicorn with minimal (pure Python) dependencies.
See the [installation documentation](installation.md) for more information.
```shell
$ pip install 'uvicorn[standard]'
```
---
This will install uvicorn with "Cython-based" dependencies (where possible) and other "optional extras".
Let's create a simple ASGI application to run with Uvicorn:
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:
```python title="main.py"
async def app(scope, receive, send):
@ -84,8 +72,7 @@ async def app(scope, receive, send):
'type': 'http.response.start',
'status': 200,
'headers': [
(b'content-type', b'text/plain'),
(b'content-length', b'13'),
[b'content-type', b'text/plain'],
],
})
await send({
@ -94,10 +81,10 @@ async def app(scope, receive, send):
})
```
Then we can run it with Uvicorn:
Run the server:
```shell
uvicorn main:app
$ uvicorn main:app
```
---
@ -108,8 +95,118 @@ The uvicorn command line tool is the easiest way to run your application.
### Command line options
```bash
{{ 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 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.
--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.
--socket-load-balance Use kernel support for socket load balancing
--help Show this message and exit.
```
For more information, see the [settings documentation](settings.md).
@ -177,7 +274,7 @@ if __name__ == "__main__":
python -m pip install uvicorn-worker
```
[Gunicorn](https://gunicorn.org/) is a mature, fully featured server and process manager.
[Gunicorn][gunicorn] 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
@ -192,9 +289,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](https://pypy.org/) compatible configuration use `uvicorn.workers.UvicornH11Worker`.
For a [PyPy][pypy] compatible configuration use `uvicorn.workers.UvicornH11Worker`.
For more information, see the [deployment documentation](deployment/index.md).
For more information, see the [deployment documentation](deployment.md).
### Application factories
@ -207,5 +304,296 @@ 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 doesnt 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.
### 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.
[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://github.com/pgjones/hypercorn
[uvloop_docs]: https://uvloop.readthedocs.io/
[httptools_vs_h11]: https://github.com/python-hyper/h11/issues/9

View File

@ -1,75 +0,0 @@
**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.

View File

@ -1,12 +0,0 @@
{% 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 %}

View File

@ -45,14 +45,9 @@
{% endfor %}
</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>
<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>
</nav>

View File

@ -1,18 +0,0 @@
<!-- 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>

View File

@ -1,8 +1,6 @@
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
@ -10,7 +8,9 @@ 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."""
"""
Called on each page after the markdown is converted to HTML.
"""
html = add_hyperlink_to_pull_request(html, page, config, files)
return html
@ -18,25 +18,9 @@ def on_page_content(html: str, page: Page, config: Config, files: Files) -> str:
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 we find "(#\\d+)" it will be added an hyperlink to https://github.com/encode/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()
return re.sub(r"\(#(\d+)\)", r"(<a href='https://github.com/encode/uvicorn/pull/\1'>#\1</a>)", html)

View File

@ -1,806 +1 @@
---
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`.
--8<-- "CHANGELOG.md"

View File

@ -83,9 +83,6 @@ 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.

View File

@ -2,57 +2,35 @@
Use the following options to configure Uvicorn, when running from the command line.
## Configuration Methods
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.
There are three ways to configure Uvicorn:
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`.
1. **Command Line**: Use command line options when running Uvicorn directly.
```bash
uvicorn main:app --host 0.0.0.0 --port 8000
```
!!! note
CLI options and the arguments for `uvicorn.run()` take precedence over environment variables.
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.
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. If set to 0, an available port will be picked. **Default:** *8000*.
* `--port <int>` - Bind to a socket with this port. **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. **Default:** *False*.
* `--reload` - Enable auto-reload. Uvicorn supports two versions of auto-reloading behavior enabled by this option. There are important differences between them.
* `--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
@ -62,7 +40,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`.
@ -74,12 +52,7 @@ Using Uvicorn with watchfiles will enable the following options (which are other
## Production
* `--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.
* `--workers <int>` - Use multiple worker processes. Defaults to the `$WEB_CONCURRENCY` environment variable if available, or 1.
## Logging
@ -87,24 +60,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. If not set, colors 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, in case this is not set it 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. 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*.
* `--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*
* `--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 <str>` - Select ASGI3, ASGI2, or WSGI as the application interface.
* `--interface` - 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'*.
@ -114,12 +87,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. **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.
* `--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 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.
* `--date-header` / `--no-date-header` - Enable/Disable default `Date` header.
!!! note
The `--no-date-header` flag doesn't have effect on the `websockets` implementation.
@ -131,23 +104,20 @@ 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. **Default:** *ssl.PROTOCOL_TLS_SERVER*.
* `--ssl-cert-reqs <int>` - Whether client certificate is required. **Default:** *ssl.CERT_NONE*.
* `--ssl-version <int>` - The SSL version to use.
* `--ssl-cert-reqs <int>` - Whether client certificate is required.
* `--ssl-ca-certs <str>` - The CA certificates file.
* `--ssl-ciphers <str>` - The ciphers to use. **Default:** *"TLSv1"*.
* `--ssl-ciphers <str>` - The ciphers to use.
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.
* `--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*.
* `--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 (in seconds). **Default:** *5*.
* `--timeout-keep-alive <int>` - Close Keep-Alive connections if no new data is received within this timeout. **Default:** *5*.
* `--timeout-graceful-shutdown <int>` - Maximum number of seconds to wait for graceful shutdown. After this timeout, the server will start terminating requests.

BIN
docs/sponsors/fastapi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,12 +1,5 @@
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
@ -25,108 +18,30 @@ theme:
toggle:
icon: "material/lightbulb-outline"
name: "Switch to light mode"
icon:
repo: fontawesome/brands/github
features:
- 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
- content.code.copy
# https://www.mkdocs.org/user-guide/configuration/#validation
validation:
omitted_files: warn
absolute_links: warn
unrecognized_links: warn
repo_name: encode/uvicorn
repo_url: https://github.com/encode/uvicorn
edit_uri: ""
nav:
- Welcome: index.md
- Installation: installation.md
- Introduction: index.md
- Settings: settings.md
- Deployment: deployment.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

View File

@ -8,25 +8,23 @@ dynamic = ["version"]
description = "The lightning-fast ASGI server."
readme = "README.md"
license = "BSD-3-Clause"
license-files = ["LICENSE.md"]
requires-python = ">=3.10"
requires-python = ">=3.8"
authors = [
{ name = "Tom Christie", email = "tom@tomchristie.com" },
]
maintainers = [
{ name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" },
{ 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",
@ -39,66 +37,33 @@ dependencies = [
[project.optional-dependencies]
standard = [
"colorama>=0.4; sys_platform == 'win32'",
"httptools>=0.6.3",
"colorama>=0.4;sys_platform == 'win32'",
"httptools>=0.5.0",
"python-dotenv>=0.13",
"PyYAML>=5.1",
"uvloop>=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')",
"watchfiles>=0.20",
"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",
"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://uvicorn.dev/release-notes"
Changelog = "https://github.com/encode/uvicorn/blob/master/CHANGELOG.md"
Funding = "https://github.com/sponsors/encode"
Homepage = "https://uvicorn.dev/"
Source = "https://github.com/Kludex/uvicorn"
Homepage = "https://www.uvicorn.org/"
Source = "https://github.com/encode/uvicorn"
[tool.hatch.version]
path = "uvicorn/__init__.py"
[tool.hatch.build.targets.sdist]
include = ["/uvicorn", "/tests"]
include = [
"/uvicorn",
"/tests",
"/requirements.txt",
]
[tool.ruff]
line-length = 120
@ -114,30 +79,33 @@ combine-as-imports = true
warn_unused_ignores = true
warn_redundant_casts = true
show_error_codes = true
disallow_untyped_defs = false
disallow_untyped_defs = true
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 -n 8"
addopts = "-rxXs --strict-config --strict-markers"
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: 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"
"ignore: remove second argument of ws_handler:DeprecationWarning:websockets"
]
[tool.coverage.run]
parallel = true
source_pkgs = ["uvicorn", "tests"]
plugins = ["coverage_conditional_plugin"]
omit = ["uvicorn/workers.py", "uvicorn/__main__.py", "uvicorn/_compat.py", "tests/benchmarks/*"]
omit = ["uvicorn/workers.py", "uvicorn/__main__.py"]
concurrency = ["multiprocessing", "thread"]
parallel = true
sigterm = true
[tool.coverage.report]
precision = 2
@ -165,8 +133,9 @@ exclude_lines = [
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)"

32
requirements.txt Normal file
View File

@ -0,0 +1,32 @@
-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.10.6
wsproto==1.2.0
websockets==13.1
# Packaging
build==1.2.1
twine==5.1.1
# Testing
ruff==0.5.0
pytest==8.2.2
pytest-mock==3.14.0
mypy==1.10.1
types-click==7.1.8
types-pyyaml==6.0.12.20240311
trustme==1.1.0
cryptography==43.0.1
coverage==7.5.4
coverage-conditional-plugin==0.9.0
httpx==0.27.0
watchgod==0.8.2
# Documentation
mkdocs==1.6.0
mkdocs-material==9.5.27

View File

@ -1,7 +1,13 @@
#!/bin/sh -e
if [ -d 'venv' ] ; then
PREFIX="venv/bin/"
else
PREFIX=""
fi
set -x
uv build
uv run twine check dist/*
uv run mkdocs build
${PREFIX}python -m build
${PREFIX}twine check dist/*
${PREFIX}mkdocs build

View File

@ -1,10 +1,16 @@
#!/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
uv run ruff format --check --diff $SOURCE_FILES
uv run mypy $SOURCE_FILES
uv run ruff check $SOURCE_FILES
${PREFIX}ruff format --check --diff $SOURCE_FILES
${PREFIX}mypy $SOURCE_FILES
${PREFIX}ruff check $SOURCE_FILES
${PREFIX}python -m tools.cli_usage --check

View File

@ -1,8 +1,12 @@
#!/bin/sh -e
export PREFIX=""
if [ -d 'venv' ]; then
export PREFIX="venv/bin/"
fi
export SOURCE_FILES="uvicorn tests"
set -x
uv run coverage combine
uv run coverage report
${PREFIX}coverage combine
${PREFIX}coverage report

View File

@ -1,5 +1,10 @@
#!/bin/sh -e
PREFIX=""
if [ -d "venv" ] ; then
PREFIX="venv/bin/"
fi
set -x
uv run mkdocs "$@"
${PREFIX}mkdocs "$@"

View File

@ -1,5 +1,19 @@
#!/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
uv sync --frozen
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"

View File

@ -1,8 +1,14 @@
#!/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
uv run ruff format $SOURCE_FILES
uv run ruff check --fix $SOURCE_FILES
${PREFIX}ruff format $SOURCE_FILES
${PREFIX}ruff check --fix $SOURCE_FILES
${PREFIX}python -m tools.cli_usage

26
scripts/publish Executable file
View File

@ -0,0 +1,26 @@
#!/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

View File

@ -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 docs/release-notes.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 CHANGELOG.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!"

View File

@ -1,14 +1,17 @@
#!/bin/sh
export PREFIX=""
if [ -d 'venv' ]; then
export PREFIX="venv/bin/"
fi
set -ex
if [ -z $GITHUB_ACTIONS ]; then
scripts/check
fi
export COVERAGE_PROCESS_START=$(pwd)/pyproject.toml
uv run coverage run --debug config -m pytest "$@"
${PREFIX}coverage run --debug config -m pytest "$@"
if [ -z $GITHUB_ACTIONS ]; then
scripts/coverage

View File

@ -1,174 +0,0 @@
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]

View File

@ -1,115 +0,0 @@
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()

View File

@ -1,64 +0,0 @@
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()

View File

@ -1,40 +0,0 @@
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

View File

@ -9,6 +9,8 @@ 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
@ -212,6 +214,27 @@ 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:
@ -233,9 +256,9 @@ def unused_tcp_port() -> int:
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",
id="websockets",
),
]
)

View File

@ -1,7 +0,0 @@
from __future__ import annotations
import asyncio
class CustomLoop(asyncio.SelectorEventLoop):
pass

View File

@ -4,31 +4,35 @@ import contextlib
import logging
import socket
import sys
from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, TypeAlias
import typing
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._types import ASGIReceiveCallable, ASGISendCallable, Scope
if TYPE_CHECKING:
if typing.TYPE_CHECKING:
import sys
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol
if sys.version_info >= (3, 10): # pragma: no cover
from typing import TypeAlias
else: # pragma: no cover
from typing_extensions import TypeAlias
WSProtocol: TypeAlias = "type[WebSocketProtocol | _WSProtocol]"
pytestmark = pytest.mark.anyio
@contextlib.contextmanager
def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> Iterator[pytest.LogCaptureFixture]:
def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> typing.Iterator[pytest.LogCaptureFixture]:
logger = logging.getLogger(logger_name)
logger.propagate, old_propagate = False, logger.propagate
logger.addHandler(caplog.handler)
@ -45,7 +49,7 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
await send({"type": "http.response.body", "body": b"", "more_body": False})
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int):
async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
config = Config(
app=app,
log_level="trace",
@ -87,8 +91,8 @@ async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging
async def test_trace_logging_on_ws_protocol(
ws_protocol_cls: WSProtocol,
caplog: pytest.LogCaptureFixture,
logging_config: dict[str, Any],
caplog,
logging_config,
unused_tcp_port: int,
):
async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@ -100,9 +104,9 @@ async def test_trace_logging_on_ws_protocol(
elif message["type"] == "websocket.disconnect":
break
async def open_connection(url: str):
async def open_connection(url):
async with websockets.client.connect(url) as websocket:
return websocket.state is State.OPEN
return websocket.open
config = Config(
app=websocket_app,
@ -122,9 +126,7 @@ async def test_trace_logging_on_ws_protocol(
@pytest.mark.parametrize("use_colors", [(True), (False), (None)])
async def test_access_logging(
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
):
async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, 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):
@ -138,7 +140,7 @@ async def test_access_logging(
@pytest.mark.parametrize("use_colors", [(True), (False)])
async def test_default_logging(
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config: dict[str, Any], unused_tcp_port: int
use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, 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"):

View File

@ -2,14 +2,13 @@ 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: pytest.LogCaptureFixture) -> None:
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
async def test_message_logger(caplog):
async def app(scope, receive, send):
await receive()
await send({"type": "http.response.start", "status": 200, "headers": []})
await send({"type": "http.response.body", "body": b"", "more_body": False})
@ -31,8 +30,8 @@ async def test_message_logger(caplog: pytest.LogCaptureFixture) -> None:
@pytest.mark.anyio
async def test_message_logger_exc(caplog: pytest.LogCaptureFixture) -> None:
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
async def test_message_logger_exc(caplog):
async def app(scope, receive, send):
raise RuntimeError()
with caplog_for_logger(caplog, "uvicorn.asgi"):

View File

@ -1,10 +1,9 @@
from __future__ import annotations
import contextlib
import ipaddress
from typing import TYPE_CHECKING
import httpx
import httpx._transports.asgi
import pytest
import websockets.client
@ -31,9 +30,6 @@ async def default_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISend
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")
@ -60,7 +56,6 @@ def make_httpx_client(
# 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 = [
@ -70,7 +65,7 @@ _TRUSTED_IPv6_ADDRESSES = [
"::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*"
_TRUSTED_LITERALS = "some-literal , unix:///foo/bar , /foo/bar"
@pytest.mark.parametrize(
@ -127,7 +122,6 @@ _TRUSTED_LITERALS = "some-literal , unix:///foo/bar , /foo/bar, garba*gewith*"
(_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),
@ -142,7 +136,6 @@ _TRUSTED_LITERALS = "some-literal , unix:///foo/bar , /foo/bar, garba*gewith*"
(_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),
@ -152,7 +145,6 @@ _TRUSTED_LITERALS = "some-literal , unix:///foo/bar , /foo/bar, garba*gewith*"
(_TRUSTED_EVERYTHING, "unix:///another/path", True),
(_TRUSTED_EVERYTHING, "/another/path", True),
(_TRUSTED_EVERYTHING, "", True),
(_TRUSTED_EVERYTHING_LIST, "", True),
## Trust IPv4 Addresses
## -----------------------------
# Test IPv4 Addresses
@ -358,14 +350,14 @@ def test_forwarded_hosts(init_hosts: str | list[str], test_host: str, expected:
(["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
# https://github.com/encode/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", "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
# https://github.com/encode/uvicorn/issues/1068#issuecomment-855371576
(["127.0.0.1", "1.2.3.4"], "https://1.2.3.4:0"),
],
)
@ -430,31 +422,6 @@ async def test_proxy_headers_multiple_proxies(trusted_hosts: str | list[str], ex
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_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 == expected
@pytest.mark.anyio
async def test_proxy_headers_invalid_x_forwarded_for() -> None:
async with make_httpx_client("*") as client:
@ -470,38 +437,6 @@ async def test_proxy_headers_invalid_x_forwarded_for() -> None:
assert response.text == "https://1.2.3.4:0"
@pytest.mark.anyio
@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",
@ -526,7 +461,6 @@ async def test_proxy_headers_websocket_x_forwarded_proto(
host, port = scope["client"]
await send({"type": "websocket.accept"})
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(
@ -548,7 +482,7 @@ async def test_proxy_headers_websocket_x_forwarded_proto(
@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
# https://github.com/encode/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)

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import io
import sys
from collections.abc import AsyncGenerator, Callable
from typing import AsyncGenerator, Callable
import a2wsgi
import httpx
@ -72,7 +72,7 @@ async def test_wsgi_post(wsgi_middleware: Callable) -> None:
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

View File

@ -1,12 +1,10 @@
from __future__ import annotations
import asyncio
import logging
import socket
import threading
import time
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, TypeAlias
from typing import TYPE_CHECKING, Any
import pytest
@ -16,8 +14,8 @@ from uvicorn._types import ASGIApplication, ASGIReceiveCallable, ASGISendCallabl
from uvicorn.config import WS_PROTOCOLS, Config
from uvicorn.lifespan.off import LifespanOff
from uvicorn.lifespan.on import LifespanOn
from uvicorn.main import ServerState
from uvicorn.protocols.http.h11_impl import H11Protocol
from uvicorn.server import ServerState
try:
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
@ -27,12 +25,19 @@ except ModuleNotFoundError: # pragma: no cover
skip_if_no_httptools = pytest.mark.skipif(True, reason="httptools is not installed")
if TYPE_CHECKING:
import sys
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
if sys.version_info >= (3, 10): # pragma: no cover
from typing import TypeAlias
else: # pragma: no cover
from typing_extensions import TypeAlias
HTTPProtocol: TypeAlias = "type[HttpToolsProtocol | H11Protocol]"
WSProtocol: TypeAlias = "type[WebSocketProtocol | _WSProtocol]"
pytestmark = pytest.mark.anyio
@ -168,9 +173,7 @@ UPGRADE_REQUEST_ERROR_FIELD = b"\r\n".join(
class MockTransport:
def __init__(
self, sockname: tuple[str, int] | None = None, peername: tuple[str, int] | None = None, sslcontext: bool = False
):
def __init__(self, sockname=None, peername=None, sslcontext=False):
self.sockname = ("127.0.0.1", 8000) if sockname is None else sockname
self.peername = ("127.0.0.1", 8001) if peername is None else peername
self.sslcontext = sslcontext
@ -178,10 +181,14 @@ class MockTransport:
self.buffer = b""
self.read_paused = False
def get_extra_info(self, key: Any):
return {"sockname": self.sockname, "peername": self.peername, "sslcontext": self.sslcontext}.get(key)
def get_extra_info(self, key):
return {
"sockname": self.sockname,
"peername": self.peername,
"sslcontext": self.sslcontext,
}.get(key)
def write(self, data: bytes):
def write(self, data):
assert not self.closed
self.buffer += data
@ -201,14 +208,12 @@ class MockTransport:
def clear_buffer(self):
self.buffer = b""
def set_protocol(self, protocol: asyncio.Protocol):
def set_protocol(self, protocol):
pass
class MockTimerHandle:
def __init__(
self, loop_later_list: list[MockTimerHandle], delay: float, callback: Callable[[], None], args: tuple[Any, ...]
):
def __init__(self, loop_later_list, delay, callback, args):
self.loop_later_list = loop_later_list
self.delay = delay
self.callback = callback
@ -223,14 +228,14 @@ class MockTimerHandle:
class MockLoop:
def __init__(self):
self._tasks: list[asyncio.Task[Any]] = []
self._later: list[MockTimerHandle] = []
self._tasks = []
self._later = []
def create_task(self, coroutine: Any) -> Any:
def create_task(self, coroutine):
self._tasks.insert(0, coroutine)
return MockTask()
def call_later(self, delay: float, callback: Callable[[], None], *args: Any) -> MockTimerHandle:
def call_later(self, delay, callback, *args):
handle = MockTimerHandle(self._later, delay, callback, args)
self._later.insert(0, handle)
return handle
@ -238,8 +243,8 @@ class MockLoop:
async def run_one(self):
return await self._tasks.pop()
def run_later(self, with_delay: float) -> None:
later: list[MockTimerHandle] = []
def run_later(self, with_delay):
later = []
for timer_handle in self._later:
if with_delay >= timer_handle.delay:
timer_handle.callback(*timer_handle.args)
@ -249,35 +254,32 @@ class MockLoop:
class MockTask:
def add_done_callback(self, callback: Callable[[], None]):
def add_done_callback(self, callback):
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 get_connected_protocol(
app: ASGIApplication,
http_protocol_cls: type[HTTPProtocol],
http_protocol_cls: HTTPProtocol,
lifespan: LifespanOff | LifespanOn | None = None,
**kwargs: Any,
) -> MockProtocol:
):
loop = MockLoop()
transport = MockTransport()
config = Config(app=app, **kwargs)
lifespan = lifespan or 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]
protocol = http_protocol_cls(
config=config,
server_state=server_state,
app_state=lifespan.state,
_loop=loop, # type: ignore
)
protocol.connection_made(transport) # type: ignore
return protocol
async def test_get_request(http_protocol_cls: type[HTTPProtocol]):
async def test_get_request(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -296,7 +298,7 @@ async def test_get_request(http_protocol_cls: type[HTTPProtocol]):
pytest.param("µ", id="allow_non_ascii_char"),
],
)
async def test_header_value_allowed_characters(http_protocol_cls: type[HTTPProtocol], char: str):
async def test_header_value_allowed_characters(http_protocol_cls: HTTPProtocol, char: str):
app = Response("Hello, world", media_type="text/plain", headers={"key": f"<{char}>"})
protocol = get_connected_protocol(app, http_protocol_cls)
protocol.data_received(SIMPLE_GET_REQUEST)
@ -306,44 +308,8 @@ async def test_header_value_allowed_characters(http_protocol_cls: type[HTTPProto
assert b"Hello, world" in protocol.transport.buffer
@pytest.mark.parametrize(
"name",
[
pytest.param("bad header", id="reject_space"),
pytest.param("bad\x00header", id="reject_null"),
pytest.param("bad(header", id="reject_open_paren"),
pytest.param("bad)header", id="reject_close_paren"),
pytest.param("bad<header", id="reject_less_than"),
pytest.param("bad>header", id="reject_greater_than"),
pytest.param("bad@header", id="reject_at"),
pytest.param("bad,header", id="reject_comma"),
pytest.param("bad;header", id="reject_semicolon"),
pytest.param("bad:header", id="reject_colon"),
pytest.param("bad[header", id="reject_open_bracket"),
pytest.param("bad]header", id="reject_close_bracket"),
pytest.param("bad{header", id="reject_open_brace"),
pytest.param("bad}header", id="reject_close_brace"),
pytest.param("bad=header", id="reject_equals"),
pytest.param('bad"header', id="reject_double_quote"),
pytest.param("bad\\header", id="reject_backslash"),
pytest.param("bad\theader", id="reject_tab"),
pytest.param("bad\x7fheader", id="reject_del"),
],
)
async def test_invalid_header_name(http_protocol_cls: type[HTTPProtocol], name: str):
app = Response("Hello, world", media_type="text/plain", headers={name: "value"})
protocol = get_connected_protocol(app, http_protocol_cls)
protocol.data_received(SIMPLE_GET_REQUEST)
await protocol.loop.run_one()
# No 500 is sent because `response_started` is set before header validation,
# so the error handler just closes the connection.
assert b"HTTP/1.1 500 Internal Server Error" not in protocol.transport.buffer
assert name.encode() not in protocol.transport.buffer
assert protocol.transport.is_closing()
@pytest.mark.parametrize("path", ["/", "/?foo", "/?foo=bar", "/?foo=bar&baz=1"])
async def test_request_logging(path: str, http_protocol_cls: type[HTTPProtocol], caplog: pytest.LogCaptureFixture):
async def test_request_logging(path: str, http_protocol_cls: HTTPProtocol, caplog: pytest.LogCaptureFixture):
get_request_with_query_string = b"\r\n".join(
[f"GET {path} HTTP/1.1".encode("ascii"), b"Host: example.org", b"", b""]
)
@ -358,7 +324,7 @@ async def test_request_logging(path: str, http_protocol_cls: type[HTTPProtocol],
assert f'"GET {path} HTTP/1.1" 200' in caplog.records[0].message
async def test_head_request(http_protocol_cls: type[HTTPProtocol]):
async def test_head_request(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -368,7 +334,7 @@ async def test_head_request(http_protocol_cls: type[HTTPProtocol]):
assert b"Hello, world" not in protocol.transport.buffer
async def test_post_request(http_protocol_cls: type[HTTPProtocol]):
async def test_post_request(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
body = b""
more_body = True
@ -387,7 +353,7 @@ async def test_post_request(http_protocol_cls: type[HTTPProtocol]):
assert b'Body: {"hello": "world"}' in protocol.transport.buffer
async def test_keepalive(http_protocol_cls: type[HTTPProtocol]):
async def test_keepalive(http_protocol_cls: HTTPProtocol):
app = Response(b"", status_code=204)
protocol = get_connected_protocol(app, http_protocol_cls)
@ -398,7 +364,7 @@ async def test_keepalive(http_protocol_cls: type[HTTPProtocol]):
assert not protocol.transport.is_closing()
async def test_keepalive_timeout(http_protocol_cls: type[HTTPProtocol]):
async def test_keepalive_timeout(http_protocol_cls: HTTPProtocol):
app = Response(b"", status_code=204)
protocol = get_connected_protocol(app, http_protocol_cls)
@ -412,7 +378,9 @@ async def test_keepalive_timeout(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_keepalive_timeout_with_pipelined_requests(http_protocol_cls: type[HTTPProtocol]):
async def test_keepalive_timeout_with_pipelined_requests(
http_protocol_cls: HTTPProtocol,
):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -435,7 +403,7 @@ async def test_keepalive_timeout_with_pipelined_requests(http_protocol_cls: type
assert protocol.timeout_keep_alive_task is not None
async def test_close(http_protocol_cls: type[HTTPProtocol]):
async def test_close(http_protocol_cls: HTTPProtocol):
app = Response(b"", status_code=204, headers={"connection": "close"})
protocol = get_connected_protocol(app, http_protocol_cls)
@ -445,7 +413,7 @@ async def test_close(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_chunked_encoding(http_protocol_cls: type[HTTPProtocol]):
async def test_chunked_encoding(http_protocol_cls: HTTPProtocol):
app = Response(b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"})
protocol = get_connected_protocol(app, http_protocol_cls)
@ -456,7 +424,7 @@ async def test_chunked_encoding(http_protocol_cls: type[HTTPProtocol]):
assert not protocol.transport.is_closing()
async def test_chunked_encoding_empty_body(http_protocol_cls: type[HTTPProtocol]):
async def test_chunked_encoding_empty_body(http_protocol_cls: HTTPProtocol):
app = Response(b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"})
protocol = get_connected_protocol(app, http_protocol_cls)
@ -467,7 +435,9 @@ async def test_chunked_encoding_empty_body(http_protocol_cls: type[HTTPProtocol]
assert not protocol.transport.is_closing()
async def test_chunked_encoding_head_request(http_protocol_cls: type[HTTPProtocol]):
async def test_chunked_encoding_head_request(
http_protocol_cls: HTTPProtocol,
):
app = Response(b"Hello, world!", status_code=200, headers={"transfer-encoding": "chunked"})
protocol = get_connected_protocol(app, http_protocol_cls)
@ -477,7 +447,7 @@ async def test_chunked_encoding_head_request(http_protocol_cls: type[HTTPProtoco
assert not protocol.transport.is_closing()
async def test_pipelined_requests(http_protocol_cls: type[HTTPProtocol]):
async def test_pipelined_requests(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -498,7 +468,7 @@ async def test_pipelined_requests(http_protocol_cls: type[HTTPProtocol]):
protocol.transport.clear_buffer()
async def test_undersized_request(http_protocol_cls: type[HTTPProtocol]):
async def test_undersized_request(http_protocol_cls: HTTPProtocol):
app = Response(b"xxx", headers={"content-length": "10"})
protocol = get_connected_protocol(app, http_protocol_cls)
@ -507,7 +477,7 @@ async def test_undersized_request(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_oversized_request(http_protocol_cls: type[HTTPProtocol]):
async def test_oversized_request(http_protocol_cls: HTTPProtocol):
app = Response(b"xxx" * 20, headers={"content-length": "10"})
protocol = get_connected_protocol(app, http_protocol_cls)
@ -516,7 +486,7 @@ async def test_oversized_request(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_large_post_request(http_protocol_cls: type[HTTPProtocol]):
async def test_large_post_request(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -526,7 +496,7 @@ async def test_large_post_request(http_protocol_cls: type[HTTPProtocol]):
assert not protocol.transport.read_paused
async def test_invalid_http(http_protocol_cls: type[HTTPProtocol]):
async def test_invalid_http(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -534,7 +504,7 @@ async def test_invalid_http(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_app_exception(http_protocol_cls: type[HTTPProtocol]):
async def test_app_exception(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
raise Exception()
@ -545,7 +515,7 @@ async def test_app_exception(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_exception_during_response(http_protocol_cls: type[HTTPProtocol]):
async def test_exception_during_response(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
await send({"type": "http.response.start", "status": 200})
await send({"type": "http.response.body", "body": b"1", "more_body": True})
@ -558,7 +528,7 @@ async def test_exception_during_response(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_no_response_returned(http_protocol_cls: type[HTTPProtocol]):
async def test_no_response_returned(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): ...
protocol = get_connected_protocol(app, http_protocol_cls)
@ -568,7 +538,7 @@ async def test_no_response_returned(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_partial_response_returned(http_protocol_cls: type[HTTPProtocol]):
async def test_partial_response_returned(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
await send({"type": "http.response.start", "status": 200})
@ -579,7 +549,7 @@ async def test_partial_response_returned(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_response_header_splitting(http_protocol_cls: type[HTTPProtocol]):
async def test_response_header_splitting(http_protocol_cls: HTTPProtocol):
app = Response(b"", headers={"key": "value\r\nCookie: smuggled=value"})
protocol = get_connected_protocol(app, http_protocol_cls)
@ -590,7 +560,7 @@ async def test_response_header_splitting(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_duplicate_start_message(http_protocol_cls: type[HTTPProtocol]):
async def test_duplicate_start_message(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
await send({"type": "http.response.start", "status": 200})
await send({"type": "http.response.start", "status": 200})
@ -602,7 +572,7 @@ async def test_duplicate_start_message(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_missing_start_message(http_protocol_cls: type[HTTPProtocol]):
async def test_missing_start_message(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
await send({"type": "http.response.body", "body": b""})
@ -613,7 +583,7 @@ async def test_missing_start_message(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_message_after_body_complete(http_protocol_cls: type[HTTPProtocol]):
async def test_message_after_body_complete(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
await send({"type": "http.response.start", "status": 200})
await send({"type": "http.response.body", "body": b""})
@ -626,7 +596,7 @@ async def test_message_after_body_complete(http_protocol_cls: type[HTTPProtocol]
assert protocol.transport.is_closing()
async def test_value_returned(http_protocol_cls: type[HTTPProtocol]):
async def test_value_returned(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
await send({"type": "http.response.start", "status": 200})
await send({"type": "http.response.body", "body": b""})
@ -639,7 +609,7 @@ async def test_value_returned(http_protocol_cls: type[HTTPProtocol]):
assert protocol.transport.is_closing()
async def test_early_disconnect(http_protocol_cls: type[HTTPProtocol]):
async def test_early_disconnect(http_protocol_cls: HTTPProtocol):
got_disconnect_event = False
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@ -660,7 +630,7 @@ async def test_early_disconnect(http_protocol_cls: type[HTTPProtocol]):
assert got_disconnect_event
async def test_early_response(http_protocol_cls: type[HTTPProtocol]):
async def test_early_response(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -671,7 +641,7 @@ async def test_early_response(http_protocol_cls: type[HTTPProtocol]):
assert not protocol.transport.is_closing()
async def test_read_after_response(http_protocol_cls: type[HTTPProtocol]):
async def test_read_after_response(http_protocol_cls: HTTPProtocol):
message_after_response = None
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@ -688,7 +658,7 @@ async def test_read_after_response(http_protocol_cls: type[HTTPProtocol]):
assert message_after_response == {"type": "http.disconnect"}
async def test_http10_request(http_protocol_cls: type[HTTPProtocol]):
async def test_http10_request(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "http"
content = "Version: %s" % scope["http_version"]
@ -702,7 +672,7 @@ async def test_http10_request(http_protocol_cls: type[HTTPProtocol]):
assert b"Version: 1.0" in protocol.transport.buffer
async def test_root_path(http_protocol_cls: type[HTTPProtocol]):
async def test_root_path(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "http"
root_path = scope.get("root_path", "")
@ -717,7 +687,7 @@ async def test_root_path(http_protocol_cls: type[HTTPProtocol]):
assert b"root_path=/app path=/app/" in protocol.transport.buffer
async def test_raw_path(http_protocol_cls: type[HTTPProtocol]):
async def test_raw_path(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "http"
path = scope["path"]
@ -734,7 +704,7 @@ async def test_raw_path(http_protocol_cls: type[HTTPProtocol]):
assert b"Done" in protocol.transport.buffer
async def test_max_concurrency(http_protocol_cls: type[HTTPProtocol]):
async def test_max_concurrency(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls, limit_concurrency=1)
@ -755,27 +725,27 @@ async def test_max_concurrency(http_protocol_cls: type[HTTPProtocol]):
)
async def test_shutdown_during_request(http_protocol_cls: type[HTTPProtocol]):
async def test_shutdown_during_request(http_protocol_cls: HTTPProtocol):
app = Response(b"", status_code=204)
protocol = get_connected_protocol(app, http_protocol_cls)
protocol.data_received(SIMPLE_GET_REQUEST)
protocol.shutdown() # type: ignore[attr-defined]
protocol.shutdown()
await protocol.loop.run_one()
assert b"HTTP/1.1 204 No Content" in protocol.transport.buffer
assert protocol.transport.is_closing()
async def test_shutdown_during_idle(http_protocol_cls: type[HTTPProtocol]):
async def test_shutdown_during_idle(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
protocol.shutdown() # type: ignore[attr-defined]
protocol.shutdown()
assert protocol.transport.buffer == b""
assert protocol.transport.is_closing()
async def test_100_continue_sent_when_body_consumed(http_protocol_cls: type[HTTPProtocol]):
async def test_100_continue_sent_when_body_consumed(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
body = b""
more_body = True
@ -807,7 +777,7 @@ async def test_100_continue_sent_when_body_consumed(http_protocol_cls: type[HTTP
async def test_100_continue_not_sent_when_body_not_consumed(
http_protocol_cls: type[HTTPProtocol],
http_protocol_cls: HTTPProtocol,
):
app = Response(b"", status_code=204)
@ -829,7 +799,7 @@ async def test_100_continue_not_sent_when_body_not_consumed(
assert b"HTTP/1.1 204 No Content" in protocol.transport.buffer
async def test_supported_upgrade_request(http_protocol_cls: type[HTTPProtocol]):
async def test_supported_upgrade_request(http_protocol_cls: HTTPProtocol):
pytest.importorskip("wsproto")
app = Response("Hello, world", media_type="text/plain")
@ -839,7 +809,7 @@ async def test_supported_upgrade_request(http_protocol_cls: type[HTTPProtocol]):
assert b"HTTP/1.1 426 " in protocol.transport.buffer
async def test_unsupported_ws_upgrade_request(http_protocol_cls: type[HTTPProtocol]):
async def test_unsupported_ws_upgrade_request(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls, ws="none")
@ -850,7 +820,7 @@ async def test_unsupported_ws_upgrade_request(http_protocol_cls: type[HTTPProtoc
async def test_unsupported_ws_upgrade_request_warn_on_auto(
caplog: pytest.LogCaptureFixture, http_protocol_cls: type[HTTPProtocol]
caplog: pytest.LogCaptureFixture, http_protocol_cls: HTTPProtocol
):
app = Response("Hello, world", media_type="text/plain")
@ -866,7 +836,7 @@ async def test_unsupported_ws_upgrade_request_warn_on_auto(
assert msg in warnings
async def test_http2_upgrade_request(http_protocol_cls: type[HTTPProtocol], ws_protocol_cls: type[WSProtocol]):
async def test_http2_upgrade_request(http_protocol_cls: HTTPProtocol, ws_protocol_cls: WSProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls, ws=ws_protocol_cls)
@ -890,14 +860,14 @@ def asgi2app(scope: Scope):
@pytest.mark.parametrize(
"asgi2or3_app, expected_scopes",
[
(asgi3app, {"version": "3.0", "spec_version": "2.3"}),
(asgi2app, {"version": "2.0", "spec_version": "2.3"}),
(asgi3app, {"version": "3.0", "spec_version": "2.4"}),
(asgi2app, {"version": "2.0", "spec_version": "2.4"}),
],
)
async def test_scopes(
asgi2or3_app: ASGIApplication,
expected_scopes: dict[str, str],
http_protocol_cls: type[HTTPProtocol],
http_protocol_cls: HTTPProtocol,
):
protocol = get_connected_protocol(asgi2or3_app, http_protocol_cls)
protocol.data_received(SIMPLE_GET_REQUEST)
@ -914,7 +884,7 @@ async def test_scopes(
],
)
async def test_invalid_http_request(
request_line: str, http_protocol_cls: type[HTTPProtocol], caplog: pytest.LogCaptureFixture
request_line: str, http_protocol_cls: HTTPProtocol, caplog: pytest.LogCaptureFixture
):
app = Response("Hello, world", media_type="text/plain")
request = INVALID_REQUEST_TEMPLATE % request_line
@ -944,7 +914,7 @@ def test_fragmentation(unused_tcp_port: int):
def send_fragmented_req(path: str):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("127.0.0.1", unused_tcp_port))
d = (f"GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n").encode()
d = (f"GET {path} HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: close\r\n\r\n").encode()
split = len(path) // 2
sock.sendall(d[:split])
time.sleep(0.01)
@ -1037,7 +1007,7 @@ async def test_huge_headers_h11_max_incomplete():
assert b"Hello, world" in protocol.transport.buffer
async def test_return_close_header(http_protocol_cls: type[HTTPProtocol]):
async def test_return_close_header(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -1051,7 +1021,7 @@ async def test_return_close_header(http_protocol_cls: type[HTTPProtocol]):
assert b"connection: close" in protocol.transport.buffer.lower()
async def test_close_connection_with_multiple_requests(http_protocol_cls: type[HTTPProtocol]):
async def test_close_connection_with_multiple_requests(http_protocol_cls: HTTPProtocol):
app = Response("Hello, world", media_type="text/plain")
protocol = get_connected_protocol(app, http_protocol_cls)
@ -1065,7 +1035,7 @@ async def test_close_connection_with_multiple_requests(http_protocol_cls: type[H
assert b"connection: close" in protocol.transport.buffer.lower()
async def test_close_connection_with_post_request(http_protocol_cls: type[HTTPProtocol]):
async def test_close_connection_with_post_request(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
body = b""
more_body = True
@ -1084,7 +1054,7 @@ async def test_close_connection_with_post_request(http_protocol_cls: type[HTTPPr
assert b"Body: {'hello': 'world'}" in protocol.transport.buffer
async def test_iterator_headers(http_protocol_cls: type[HTTPProtocol]):
async def test_iterator_headers(http_protocol_cls: HTTPProtocol):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
headers = iter([(b"x-test-header", b"test value")])
await send({"type": "http.response.start", "status": 200, "headers": headers})
@ -1096,7 +1066,7 @@ async def test_iterator_headers(http_protocol_cls: type[HTTPProtocol]):
assert b"x-test-header: test value" in protocol.transport.buffer
async def test_lifespan_state(http_protocol_cls: type[HTTPProtocol]):
async def test_lifespan_state(http_protocol_cls: HTTPProtocol):
expected_states = [{"a": 123, "b": [1]}, {"a": 123, "b": [1, 2]}]
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@ -1125,7 +1095,7 @@ async def test_lifespan_state(http_protocol_cls: type[HTTPProtocol]):
async def test_header_upgrade_is_not_websocket_depend_installed(
caplog: pytest.LogCaptureFixture, http_protocol_cls: type[HTTPProtocol]
caplog: pytest.LogCaptureFixture, http_protocol_cls: HTTPProtocol
):
caplog.set_level(logging.WARNING, logger="uvicorn.error")
app = Response("Hello, world", media_type="text/plain")
@ -1141,7 +1111,7 @@ async def test_header_upgrade_is_not_websocket_depend_installed(
async def test_header_upgrade_is_websocket_depend_not_installed(
caplog: pytest.LogCaptureFixture, http_protocol_cls: type[HTTPProtocol]
caplog: pytest.LogCaptureFixture, http_protocol_cls: HTTPProtocol
):
caplog.set_level(logging.WARNING, logger="uvicorn.error")
app = Response("Hello, world", media_type="text/plain")

View File

@ -1,8 +1,5 @@
from __future__ import annotations
import socket
from asyncio import Transport
from typing import Any
import pytest
@ -10,12 +7,7 @@ from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_remote_
class MockSocket:
def __init__(
self,
family: socket.AddressFamily,
peername: tuple[str, int] | None = None,
sockname: tuple[str, int] | str | None = None,
):
def __init__(self, family, peername=None, sockname=None):
self.peername = peername
self.sockname = sockname
self.family = family
@ -28,11 +20,11 @@ class MockSocket:
class MockTransport(Transport):
def __init__(self, info: dict[str, Any]) -> None:
def __init__(self, info):
self.info = info
def get_extra_info(self, name: str, default: Any = None) -> Any:
return self.info.get(name)
def get_extra_info(self, info_type):
return self.info.get(info_type)
def test_get_local_addr_with_socket():
@ -45,8 +37,9 @@ def test_get_local_addr_with_socket():
transport = MockTransport({"socket": MockSocket(family=socket.AF_INET, sockname=("123.45.6.7", 123))})
assert get_local_addr(transport) == ("123.45.6.7", 123)
transport = MockTransport({"socket": MockSocket(family=socket.AF_INET, sockname="/tmp/test.sock")})
assert get_local_addr(transport) == ("/tmp/test.sock", None)
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)
def test_get_remote_addr_with_socket():
@ -66,14 +59,11 @@ def test_get_remote_addr_with_socket():
def test_get_local_addr():
transport = MockTransport({"sockname": "path/to/unix-domain-socket"})
assert get_local_addr(transport) == ("path/to/unix-domain-socket", None)
assert get_local_addr(transport) is 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})
@ -88,5 +78,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: Any, expected_client: str):
def test_get_client_addr(scope, expected_client):
assert get_client_addr(scope) == expected_client

View File

@ -1,16 +1,16 @@
from __future__ import annotations
import asyncio
import typing
from copy import deepcopy
from typing import TYPE_CHECKING, Any, TypeAlias, TypedDict
import httpx
import pytest
import websockets
import websockets.client
import websockets.exceptions
from typing_extensions import TypedDict
from websockets.extensions.permessage_deflate import ClientPerMessageDeflateFactory
from websockets.frames import Opcode
from websockets.typing import Subprotocol
from tests.response import Response
@ -28,7 +28,6 @@ from uvicorn._types import (
)
from uvicorn.config import Config
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
try:
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol
@ -37,14 +36,20 @@ try:
except ModuleNotFoundError: # pragma: no cover
skip_if_no_wsproto = pytest.mark.skipif(True, reason="wsproto is not installed.")
if TYPE_CHECKING:
if typing.TYPE_CHECKING:
import sys
from uvicorn.protocols.http.h11_impl import H11Protocol
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol
if sys.version_info >= (3, 10): # pragma: no cover
from typing import TypeAlias
else: # pragma: no cover
from typing_extensions import TypeAlias
HTTPProtocol: TypeAlias = "type[H11Protocol | HttpToolsProtocol]"
WSProtocol: TypeAlias = "type[_WSProtocol | WebSocketProtocol]"
KeepaliveWSProtocol: TypeAlias = "type[_WSProtocol | WebSocketsSansIOProtocol]"
pytestmark = pytest.mark.anyio
@ -104,14 +109,17 @@ async def test_invalid_upgrade(ws_protocol_cls: WSProtocol, http_protocol_cls: H
pass # ok, wsproto 0.13
else:
assert response.status_code == 400
assert response.text.lower().strip().rstrip(".") in [
"missing sec-websocket-key header",
"missing sec-websocket-version header", # websockets
"missing or empty sec-websocket-key header", # wsproto
"failed to open a websocket connection: missing sec-websocket-key header",
"failed to open a websocket connection: missing or empty sec-websocket-key header",
"failed to open a websocket connection: missing sec-websocket-key header; 'sec-websocket-key'",
]
assert (
response.text.lower().strip().rstrip(".")
in [
"missing sec-websocket-key header",
"missing sec-websocket-version header", # websockets
"missing or empty sec-websocket-key header", # wsproto
"failed to open a websocket connection: missing " "sec-websocket-key header",
"failed to open a websocket connection: missing or empty " "sec-websocket-key header",
"failed to open a websocket connection: missing sec-websocket-key header; 'sec-websocket-key'",
]
)
async def test_accept_connection(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int):
@ -209,8 +217,8 @@ async def test_headers(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProto
async def websocket_connect(self, message: WebSocketConnectEvent):
headers = self.scope.get("headers")
headers = dict(headers) # type: ignore
assert headers[b"host"].startswith(b"127.0.0.1")
assert headers[b"username"] == bytes("abraão", "utf-8")
assert headers[b"host"].startswith(b"127.0.0.1") # type: ignore
assert headers[b"username"] == bytes("abraão", "utf-8") # type: ignore
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
@ -454,27 +462,6 @@ async def test_asgi_return_value(ws_protocol_cls: WSProtocol, http_protocol_cls:
assert websocket.close_code == 1006
async def test_close_transport_on_asgi_return(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
"""The ASGI callable should call the `websocket.close` event.
If it doesn't, the server should still send a close frame to the client.
"""
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
message = await receive()
if message["type"] == "websocket.connect":
await send({"type": "websocket.accept"})
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
with pytest.raises(websockets.exceptions.ConnectionClosed):
await websocket.recv()
assert websocket.close_code == 1006
@pytest.mark.parametrize("code", [None, 1000, 1001])
@pytest.mark.parametrize("reason", [None, "test", False], ids=["none_as_reason", "normal_reason", "without_reason"])
async def test_app_close(
@ -614,9 +601,12 @@ async def test_connection_lost_before_handshake_complete(
await send_accept_task.wait()
disconnect_message = await receive() # type: ignore
response: httpx.Response | None = None
async def websocket_session(uri: str):
nonlocal response
async with httpx.AsyncClient() as client:
await client.get(
response = await client.get(
f"http://127.0.0.1:{unused_tcp_port}",
headers={
"upgrade": "websocket",
@ -633,6 +623,9 @@ async def test_connection_lost_before_handshake_complete(
send_accept_task.set()
await asyncio.sleep(0.1)
assert response is not None
assert response.status_code == 500, response.text
assert response.text == "Internal Server Error"
assert disconnect_message == {"type": "websocket.disconnect", "code": 1006}
await task
@ -753,61 +746,6 @@ async def test_send_binary_data_to_server_bigger_than_default_on_websockets(
assert ws.close_code == expected_result
async def test_fragmented_message_exceeding_max_size(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
"""Stream non-FIN fragments past `ws_max_size` - the server must close with 1009."""
class App(WebSocketResponse):
async def websocket_connect(self, message: WebSocketConnectEvent):
await self.send({"type": "websocket.accept"})
config = Config(
app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", ws_max_size=2048, port=unused_tcp_port
)
async with run_server(config):
async with websockets.connect(f"ws://127.0.0.1:{unused_tcp_port}") as ws:
payload = b"A" * 1024
with pytest.raises(websockets.exceptions.ConnectionClosed) as exc_info:
await ws.write_frame(False, Opcode.BINARY, payload)
for _ in range(63): # 64 KiB total, well past 2 KiB budget
await ws.write_frame(False, Opcode.CONT, payload)
await ws.recv()
assert exc_info.value.rcvd is not None
assert exc_info.value.rcvd.code == 1009
async def test_fragmented_message_reassembly(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
"""Server reassembles a fragmented message and delivers it to the app intact."""
received: list[bytes] = []
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "websocket"
connect = await receive()
assert connect["type"] == "websocket.connect"
await send({"type": "websocket.accept"})
message = await receive()
assert message["type"] == "websocket.receive"
payload = message.get("bytes")
assert payload is not None
received.append(payload)
await send({"type": "websocket.close"})
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
async with websockets.connect(f"ws://127.0.0.1:{unused_tcp_port}") as ws:
payload = b"A" * 512
await ws.write_frame(False, Opcode.BINARY, payload)
for _ in range(4):
await ws.write_frame(False, Opcode.CONT, payload)
await ws.write_frame(True, Opcode.CONT, payload)
assert received == [b"A" * 512 * 6]
async def test_server_reject_connection(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
@ -826,7 +764,7 @@ async def test_server_reject_connection(
# -- At this point websockets' recv() is unusable. --
# This doesn't raise `TypeError`:
# See https://github.com/Kludex/uvicorn/issues/244
# See https://github.com/encode/uvicorn/issues/244
disconnected_message = await receive()
async def websocket_session(url: str):
@ -941,8 +879,6 @@ async def test_server_reject_connection_with_invalid_status(
response = await wsresponse(url)
assert response.status_code == 500
assert response.content == b"Internal Server Error"
assert response.headers["content-length"] == "21"
assert response.headers["connection"] == "close"
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@ -984,9 +920,6 @@ async def test_server_reject_connection_with_body_nolength(
async def test_server_reject_connection_with_invalid_msg(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "websocket"
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@ -1018,9 +951,6 @@ async def test_server_reject_connection_with_invalid_msg(
async def test_server_reject_connection_with_missing_body(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "websocket"
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@ -1056,8 +986,6 @@ async def test_server_multiple_websocket_http_response_start_events(
The server should raise an exception if it sends multiple
websocket.http.response.start events.
"""
if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
exception_message: str | None = None
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@ -1092,7 +1020,7 @@ async def test_server_multiple_websocket_http_response_start_events(
await websocket_session(f"ws://127.0.0.1:{unused_tcp_port}")
assert exception_message == (
"Expected ASGI message 'websocket.http.response.body' but got 'websocket.http.response.start'."
"Expected ASGI message 'websocket.http.response.body' but got " "'websocket.http.response.start'."
)
@ -1218,12 +1146,12 @@ async def test_multiple_server_header(
async def test_lifespan_state(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int):
expected_states: list[dict[str, Any]] = [
expected_states: list[dict[str, typing.Any]] = [
{"a": 123, "b": [1]},
{"a": 123, "b": [1, 2]},
]
actual_states: list[dict[str, Any]] = []
actual_states: list[dict[str, typing.Any]] = []
async def lifespan_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
message = await receive()
@ -1260,118 +1188,3 @@ async def test_lifespan_state(ws_protocol_cls: WSProtocol, http_protocol_cls: HT
assert is_open
assert expected_states == actual_states
@pytest.fixture(
params=[
pytest.param(
"uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
marks=skip_if_no_wsproto,
id="wsproto",
),
pytest.param(
"uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", id="websockets-sansio"
),
]
)
def keepalive_ws_protocol_cls(request: pytest.FixtureRequest):
from uvicorn.importer import import_from_string
return import_from_string(request.param)
async def test_server_keepalive_ping_pong(
keepalive_ws_protocol_cls: KeepaliveWSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
while True:
message = await receive()
if message["type"] == "websocket.connect":
await send({"type": "websocket.accept"})
elif message["type"] == "websocket.disconnect":
break
config = Config(
app=app,
ws=keepalive_ws_protocol_cls,
http=http_protocol_cls,
lifespan="off",
ws_ping_interval=0.1,
ws_ping_timeout=5.0,
port=unused_tcp_port,
)
async with run_server(config) as server:
# The websockets client auto-responds to ping frames, keeping the connection alive.
async with websockets.connect(f"ws://127.0.0.1:{unused_tcp_port}", ping_interval=None):
protocol = list(server.server_state.connections)[0]
assert isinstance(protocol, (_WSProtocol, WebSocketsSansIOProtocol))
# Wait until the server sends at least one keepalive ping, then
# sleep past the timeout window and ensure the connection stays open.
# This verifies that the client answered the ping without depending
# on clock granularity for the measured RTT.
async def ping_sent() -> None:
while protocol.ping_sent_at == 0.0:
await asyncio.sleep(0.05)
await asyncio.wait_for(ping_sent(), timeout=5.0)
await asyncio.sleep(0.2)
assert not protocol.transport.is_closing()
async def test_server_keepalive_ping_timeout(
keepalive_ws_protocol_cls: KeepaliveWSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
while True:
message = await receive()
if message["type"] == "websocket.connect":
await send({"type": "websocket.accept"})
elif message["type"] == "websocket.disconnect":
break
config = Config(
app=app,
ws=keepalive_ws_protocol_cls,
http=http_protocol_cls,
lifespan="off",
ws_ping_interval=0.1,
ws_ping_timeout=0.1,
log_level="trace",
port=unused_tcp_port,
)
async with run_server(config):
async with websockets.connect(f"ws://127.0.0.1:{unused_tcp_port}", ping_interval=None) as websocket:
# Swallow outgoing pong frames so the server's ping never gets ack'd.
websocket.transport.write = lambda data: None # type: ignore[method-assign]
with pytest.raises(websockets.exceptions.ConnectionClosedError) as exc_info:
await asyncio.wait_for(websocket.recv(), timeout=1)
assert exc_info.value.rcvd is not None
assert exc_info.value.rcvd.code == 1011
assert exc_info.value.rcvd.reason == "keepalive ping timeout"
async def test_server_keepalive_disabled(
keepalive_ws_protocol_cls: KeepaliveWSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
while True:
message = await receive()
if message["type"] == "websocket.connect":
await send({"type": "websocket.accept"})
elif message["type"] == "websocket.disconnect":
break
config = Config(
app=app,
ws=keepalive_ws_protocol_cls,
http=http_protocol_cls,
lifespan="off",
ws_ping_interval=None,
port=unused_tcp_port,
)
async with run_server(config) as server:
async with websockets.connect(f"ws://127.0.0.1:{unused_tcp_port}", ping_interval=None):
protocol = list(server.server_state.connections)[0]
assert isinstance(protocol, (_WSProtocol, WebSocketsSansIOProtocol))
assert protocol.ping_timer is None

View File

@ -1,18 +1,22 @@
from __future__ import annotations
import dataclasses
import functools
import multiprocessing.managers
import os
import signal
import socket
import sys
import threading
import time
from collections.abc import Callable
from typing import Any
from typing import Any, Callable, Generic, TypeVar
import httpx
import pytest
from uvicorn import Config
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
from uvicorn.server import Server
from uvicorn.supervisors import Multiprocess
from uvicorn.supervisors.multiprocess import Process
@ -30,8 +34,12 @@ def new_console_in_windows(test_function: Callable[[], Any]) -> Callable[[], Any
name = test_function.__name__
subprocess.check_call(
[sys.executable, "-c", f"from {module} import {name}; {name}.__wrapped__()"],
creationflags=subprocess.CREATE_NO_WINDOW,
[
sys.executable,
"-c",
f"from {module} import {name}; {name}.__wrapped__()",
],
creationflags=subprocess.CREATE_NO_WINDOW, # type: ignore[attr-defined]
)
return new_function
@ -84,10 +92,9 @@ def test_multiprocess_health_check() -> None:
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)
time.sleep(1)
for p in supervisor.processes:
assert p.is_alive()
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()
@ -130,13 +137,7 @@ def test_multiprocess_sighup() -> None:
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)
time.sleep(1)
assert pids != [p.pid for p in supervisor.processes]
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()
@ -173,3 +174,71 @@ def test_multiprocess_sigttou() -> None:
assert len(supervisor.processes) == 1
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()
T = TypeVar("T")
@dataclasses.dataclass
class Box(Generic[T]):
v: T
async def lb_app(
d: multiprocessing.managers.DictProxy,
started: threading.Event,
scope: Scope,
receive: ASGIReceiveCallable,
send: ASGISendCallable,
) -> None: # pragma: py-darwin pragma: py-win32
if scope["type"] == "lifespan":
await receive()
scope["state"]["count"] = box = Box(0)
await send({"type": "lifespan.startup.complete"})
started.set()
await receive()
d[os.getpid()] = box.v
await send({"type": "lifespan.shutdown.complete"})
return
scope["state"]["count"].v += 1
headers = [(b"content-type", b"text/plain")]
await send({"type": "http.response.start", "status": 200, "headers": headers})
await send({"type": "http.response.body", "body": b"hello"})
@pytest.mark.skipif(
not ((sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB")),
reason="unsupported",
)
def test_multiprocess_socket_balance() -> None: # pragma: py-darwin pragma: py-win32
with multiprocessing.Manager() as m:
started = m.Event()
d = m.dict()
app = functools.partial(lb_app, d, started)
config = Config(app=app, workers=2, socket_load_balance=True, port=0, interface="asgi3")
server = Server(config=config)
with config.bind_socket() as sock:
port = sock.getsockname()[1]
try:
supervisor = Multiprocess(config, target=server.run, sockets=[sock])
threading.Thread(target=supervisor.run, daemon=True).start()
if not started.wait(timeout=5): # pragma: no cover
raise TimeoutError
with httpx.Client():
for i in range(100):
httpx.get(f"http://localhost:{port}/").raise_for_status()
finally:
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()
min_conn, max_conn = sorted(d.values())
assert (max_conn - min_conn) < 25
def test_multiprocess_not_supported(monkeypatch):
monkeypatch.delattr(socket, "SO_REUSEPORT")
config = Config(app=app, workers=2, socket_load_balance=True, port=0, interface="asgi3")
with config.bind_socket() as sock:
supervisor = Multiprocess(config, target=run, sockets=[sock])
with pytest.raises(RuntimeError, match="socket_load_balance not supported"):
supervisor.run()

View File

@ -1,15 +1,14 @@
from __future__ import annotations
import logging
import platform
import signal
import socket
import sys
from collections.abc import Callable, Generator
from pathlib import Path
from threading import Thread
from time import sleep
import pytest
from pytest_mock import MockerFixture
from tests.utils import as_cwd
from uvicorn.config import Config
@ -21,39 +20,30 @@ try:
except ImportError: # pragma: no cover
WatchFilesReload = 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")
try:
from uvicorn.supervisors.watchgodreload import WatchGodReload
except ImportError: # pragma: no cover
WatchGodReload = None # type: ignore[misc,assignment]
def run(sockets: list[socket.socket] | None) -> None:
# 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):
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: type[BaseReload] | None):
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
@ -62,15 +52,17 @@ class TestBaseReload:
def _setup_reloader(self, config: Config) -> BaseReload:
config.reload_delay = 0 # save time
reloader = self.reloader_class(config, target=run, sockets=[])
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=[])
assert config.should_reload
reloader.startup()
return reloader
def _reload_tester(
self, touch_soon: Callable[[Path], None], reloader: BaseReload, *files: Path
) -> list[Path] | None:
def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> list[Path] | None:
reloader.restart()
if WatchFilesReload is not None and isinstance(reloader, WatchFilesReload):
touch_soon(*files)
@ -81,7 +73,7 @@ class TestBaseReload:
file.touch()
return next(reloader)
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_reloader_should_initialize(self) -> None:
"""
A basic sanity check.
@ -94,8 +86,8 @@ class TestBaseReload:
reloader = self._setup_reloader(config)
reloader.shutdown()
@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]):
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
file = self.reload_path / "main.py"
with as_cwd(self.reload_path):
@ -107,8 +99,8 @@ class TestBaseReload:
reloader.shutdown()
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon: Callable[[Path], None]):
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon) -> None:
file = self.reload_path / "app" / "sub" / "sub.py"
with as_cwd(self.reload_path):
@ -119,8 +111,8 @@ class TestBaseReload:
reloader.shutdown()
@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]):
@pytest.mark.parametrize("reloader_class", [WatchFilesReload, WatchGodReload])
def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self, touch_soon) -> None:
sub_dir = self.reload_path / "app" / "sub"
sub_file = sub_dir / "sub.py"
@ -136,12 +128,8 @@ class TestBaseReload:
reloader.shutdown()
@pytest.mark.parametrize(
"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: Callable[[Path], None]
): # pragma: py-not-linux
@pytest.mark.parametrize("reloader_class, result", [(StatReload, False), (WatchFilesReload, True)])
def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_soon) -> None:
file = self.reload_path / "app" / "js" / "main.js"
with as_cwd(self.reload_path):
@ -152,10 +140,14 @@ class TestBaseReload:
reloader.shutdown()
@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: Callable[[Path], None]
): # pragma: py-not-linux
@pytest.mark.parametrize(
"reloader_class",
[
pytest.param(WatchFilesReload, marks=skip_if_m1),
WatchGodReload,
],
)
def test_should_not_reload_when_exclude_pattern_match_file_is_changed(self, touch_soon) -> None:
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"
@ -165,7 +157,7 @@ class TestBaseReload:
app="tests.test_config:asgi_app",
reload=True,
reload_includes=["*"],
reload_excludes=["*.js"],
reload_excludes=["*.js", ".coverage.*"],
)
reloader = self._setup_reloader(config)
@ -175,8 +167,8 @@ class TestBaseReload:
reloader.shutdown()
@pytest.mark.parametrize("reloader_class", [StatReload, WatchFilesReload])
def test_should_not_reload_when_dot_file_is_changed(self, touch_soon: Callable[[Path], None]):
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
file = self.reload_path / ".dotted"
with as_cwd(self.reload_path):
@ -187,10 +179,8 @@ class TestBaseReload:
reloader.shutdown()
@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
@pytest.mark.parametrize("reloader_class", [StatReload, WatchGodReload, WatchFilesReload])
def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> None:
app_dir = self.reload_path / "app"
app_file = app_dir / "src" / "main.py"
app_first_dir = self.reload_path / "app_first"
@ -211,11 +201,13 @@ class TestBaseReload:
@pytest.mark.parametrize(
"reloader_class",
[StatReload, pytest.param(WatchFilesReload, marks=skip_non_linux)],
[
StatReload,
WatchGodReload,
pytest.param(WatchFilesReload, marks=skip_if_m1),
],
)
def test_should_not_reload_when_only_subdirectory_is_watched(
self, touch_soon: Callable[[Path], None]
): # pragma: py-not-linux
def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon) -> None:
app_dir = self.reload_path / "app"
app_dir_file = self.reload_path / "app" / "src" / "main.py"
root_file = self.reload_path / "main.py"
@ -232,8 +224,14 @@ class TestBaseReload:
reloader.shutdown()
@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
@pytest.mark.parametrize(
"reloader_class",
[
pytest.param(WatchFilesReload, marks=skip_if_m1),
WatchGodReload,
],
)
def test_override_defaults(self, touch_soon) -> None:
dotted_file = self.reload_path / ".dotted"
dotted_dir_file = self.reload_path / ".dotted_dir" / "file.txt"
python_file = self.reload_path / "main.py"
@ -244,7 +242,7 @@ class TestBaseReload:
reload=True,
# We need to add *.txt otherwise no regular files will match
reload_includes=[".*", "*.txt"],
reload_excludes=["*.py"],
reload_excludes=["*.py", ".coverage.*"],
)
reloader = self._setup_reloader(config)
@ -254,8 +252,14 @@ 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
@pytest.mark.parametrize(
"reloader_class",
[
pytest.param(WatchFilesReload, marks=skip_if_m1),
WatchGodReload,
],
)
def test_explicit_paths(self, touch_soon) -> None:
dotted_file = self.reload_path / ".dotted"
non_dotted_file = self.reload_path / "ext" / "ext.jpg"
python_file = self.reload_path / "main.py"
@ -303,19 +307,50 @@ 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_cwd(mocker: MockerFixture, reload_directory_structure: Path):
def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
app_dir = reload_directory_structure / "app"
app_first_dir = reload_directory_structure / "app_first"
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(),)
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(),)
@pytest.mark.skipif(WatchFilesReload is None, reason="watchfiles not available")
def test_should_watch_multiple_dirs(mocker: MockerFixture, reload_directory_structure: Path):
def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structure):
mock_watch = mocker.patch("uvicorn.supervisors.watchfilesreload.watch")
app_dir = reload_directory_structure / "app"
app_first_dir = reload_directory_structure / "app_first"
@ -329,10 +364,11 @@ def test_should_watch_multiple_dirs(mocker: MockerFixture, reload_directory_stru
assert set(mock_watch.call_args[0]) == {
app_dir,
app_first_dir,
Path.cwd(),
}
def test_display_path_relative(tmp_path: Path):
def test_display_path_relative(tmp_path):
with as_cwd(tmp_path):
p = tmp_path / "app" / "foobar.py"
# accept windows paths as wells as posix
@ -344,8 +380,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: Path):
calls: list[str] = []
def test_base_reloader_run(tmp_path):
calls = []
step = 0
class CustomReload(BaseReload):
@ -375,7 +411,7 @@ def test_base_reloader_run(tmp_path: Path):
assert calls == ["startup", "restart", "shutdown"]
def test_base_reloader_should_exit(tmp_path: Path):
def test_base_reloader_should_exit(tmp_path):
config = Config(app="tests.test_config:asgi_app", reload=True)
reloader = BaseReload(config, target=run, sockets=[])
assert not reloader.should_exit.is_set()

View File

@ -1,14 +1,13 @@
import asyncio
import contextlib
import importlib
import pytest
from uvicorn.config import Config
from uvicorn.loops.auto import auto_loop_factory
from uvicorn.loops.auto import auto_loop_setup
from uvicorn.main import ServerState
from uvicorn.protocols.http.auto import AutoHTTPProtocol
from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol
from uvicorn.server import ServerState
try:
importlib.import_module("uvloop")
@ -34,17 +33,19 @@ async def app(scope, receive, send):
def test_loop_auto():
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)
auto_loop_setup()
policy = asyncio.get_event_loop_policy()
assert isinstance(policy, asyncio.events.BaseDefaultEventLoopPolicy)
assert type(policy).__module__.startswith(expected_loop)
@pytest.mark.anyio
async def test_http_auto():
config = Config(app=app)
server_state = ServerState()
protocol = AutoHTTPProtocol(config=config, server_state=server_state, app_state={})
protocol = AutoHTTPProtocol( # type: ignore[call-arg]
config=config, server_state=server_state, app_state={}
)
assert type(protocol).__name__ == expected_http

View File

@ -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
@ -131,7 +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

View File

@ -1,28 +0,0 @@
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

View File

@ -7,20 +7,25 @@ import logging
import os
import socket
import sys
from collections.abc import Callable, Iterator
from contextlib import closing
import typing
from pathlib import Path
from typing import IO, Any, Literal
from typing import Any, Literal
from unittest.mock import MagicMock
import pytest
import yaml
from pytest_mock import MockerFixture
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 tests.utils import as_cwd
from uvicorn._types import (
ASGIApplication,
ASGIReceiveCallable,
ASGISendCallable,
Environ,
Scope,
StartResponse,
)
from uvicorn.config import Config
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from uvicorn.middleware.wsgi import WSGIMiddleware
from uvicorn.protocols.http.h11_impl import H11Protocol
@ -286,7 +291,7 @@ def test_ssl_config_combined(tls_certificate_key_and_chain_path: str) -> None:
assert config.is_ssl is True
def asgi2_app(scope: Scope) -> Callable:
def asgi2_app(scope: Scope) -> typing.Callable:
async def asgi(receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: # pragma: nocover
pass
@ -366,39 +371,10 @@ def test_log_config_yaml(
mocked_logging_config_module.dictConfig.assert_called_once_with(logging_config)
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],
config_file: str | configparser.RawConfigParser | typing.IO[Any],
) -> None:
"""
Test that one can load a configparser config from disk.
@ -410,14 +386,14 @@ def test_log_config_file(
@pytest.fixture(params=[0, 1])
def web_concurrency(request: pytest.FixtureRequest) -> Iterator[int]:
def web_concurrency(request: pytest.FixtureRequest) -> typing.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) -> Iterator[str]:
def forwarded_allow_ips(request: pytest.FixtureRequest) -> typing.Iterator[str]:
yield request.param
if os.getenv("FORWARDED_ALLOW_IPS"):
del os.environ["FORWARDED_ALLOW_IPS"]
@ -433,7 +409,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}\nFORWARDED_ALLOW_IPS={forwarded_allow_ips}\n"
content = f"WEB_CONCURRENCY={web_concurrency}\n" f"FORWARDED_ALLOW_IPS={forwarded_allow_ips}\n"
fp.write_text(content)
with caplog.at_level(logging.INFO):
config = Config(app=asgi_app, env_file=fp)
@ -491,13 +467,6 @@ def test_config_log_effective_level(log_level: int, uvicorn_logger_level: int) -
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()
@ -553,37 +522,6 @@ def test_bind_fd_works_with_reload_or_workers(reload: bool, workers: int): # pr
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",
[
@ -607,56 +545,3 @@ def test_warn_when_using_reload_and_workers(caplog: pytest.LogCaptureFixture) ->
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 (
'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()

View File

@ -98,7 +98,7 @@ def test_lifespan_auto_with_error():
lifespan = LifespanOn(config)
await lifespan.startup()
assert lifespan.error_occurred
assert lifespan.error_occured
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_occurred
assert lifespan.error_occured
assert lifespan.should_exit
await lifespan.shutdown()
@ -143,7 +143,7 @@ def test_lifespan_with_failed_startup(mode, raise_exception, caplog):
await lifespan.startup()
assert lifespan.startup_failed
assert lifespan.error_occurred is raise_exception
assert lifespan.error_occured is raise_exception
assert lifespan.should_exit
await lifespan.shutdown()
@ -171,7 +171,7 @@ def test_lifespan_scope_asgi3app():
await lifespan.startup()
assert not lifespan.startup_failed
assert not lifespan.error_occurred
assert not lifespan.error_occured
assert not lifespan.should_exit
await lifespan.shutdown()
@ -228,7 +228,7 @@ 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_occurred is raise_exception
assert lifespan.error_occured is raise_exception
assert lifespan.should_exit
loop = asyncio.new_event_loop()

View File

@ -1,20 +1,15 @@
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
@ -84,65 +79,10 @@ 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"
@ -173,12 +113,3 @@ async def test_exit_on_create_server_with_invalid_host() -> None:
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

View File

@ -2,28 +2,15 @@ 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
from typing import Callable, ContextManager, Generator
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
@ -50,12 +37,6 @@ 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]
@ -64,12 +45,11 @@ else: # pragma: py-win32
signal_captures = [capture_signal_sync, capture_signal_async]
@pytest.mark.anyio
@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,
exception_signal: signal.Signals, capture_signal: Callable[[signal.Signals], ContextManager[None]]
): # pragma: py-win32
"""Test interrupting a Server that is run explicitly inside asyncio"""
@ -78,186 +58,10 @@ async def test_server_interrupt(
await asyncio.sleep(0.01)
signal.raise_signal(exception_signal)
server = Server(Config(app=dummy_app, loop="asyncio", port=unused_tcp_port))
server = Server(Config(app=dummy_app, loop="asyncio"))
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) == {}

View File

@ -1,17 +1,9 @@
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"
@ -100,108 +92,3 @@ 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

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import socket
from unittest.mock import patch
from uvicorn._subprocess import SpawnProcess, get_subprocess, subprocess_started
from uvicorn._subprocess import SocketSharePickle, SpawnProcess, get_subprocess, subprocess_started
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
from uvicorn.config import Config
@ -36,7 +36,7 @@ def test_subprocess_started() -> None:
with patch("tests.test_subprocess.server_run") as mock_run:
with patch.object(config, "configure_logging") as mock_config_logging:
subprocess_started(config, server_run, [fdsock], None)
subprocess_started(config, server_run, [SocketSharePickle(fdsock)], None)
mock_run.assert_called_once()
mock_config_logging.assert_called_once()

View File

@ -3,7 +3,6 @@ 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
@ -16,8 +15,7 @@ from uvicorn import Config, Server
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))
while not server.started:
await asyncio.sleep(0.05)
await asyncio.sleep(0.1)
try:
yield server
finally:
@ -46,11 +44,3 @@ 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

68
tools/cli_usage.py Normal file
View File

@ -0,0 +1,68 @@
"""
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)

2126
uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
from uvicorn.config import Config
from uvicorn.main import Server, main, run
__version__ = "0.47.0"
__version__ = "0.31.0"
__all__ = ["main", "run", "Config", "Server"]

View File

@ -1,91 +0,0 @@
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,
}
)

View File

@ -7,10 +7,10 @@ from __future__ import annotations
import multiprocessing
import os
import socket
import sys
from collections.abc import Callable
from multiprocessing.context import SpawnProcess
from socket import socket
from typing import Callable
from uvicorn.config import Config
@ -18,10 +18,42 @@ multiprocessing.allow_connection_pickling()
spawn = multiprocessing.get_context("spawn")
class SocketSharePickle:
def __init__(self, sock: socket.socket):
self._sock = sock
def get(self) -> socket.socket:
return self._sock
class SocketShareRebind:
def __init__(self, sock: socket.socket):
if not (sys.platform == "linux" and hasattr(socket, "SO_REUSEPORT")) or hasattr(socket, "SO_REUSEPORT_LB"):
raise RuntimeError("socket_load_balance not supported")
else: # pragma: py-darwin pragma: py-win32
sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1)
self._family = sock.family
self._type = sock.type
self._proto = sock.proto
self._sockname = sock.getsockname()
def get(self) -> socket.socket: # pragma: py-darwin pragma: py-win32
try:
sock = socket.socket(family=self._family, type=self._type, proto=self._proto)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, getattr(socket, "SO_REUSEPORT_LB", socket.SO_REUSEPORT), 1)
sock.bind(self._sockname)
return sock
except BaseException: # pragma: no cover
sock.close()
raise
def get_subprocess(
config: Config,
target: Callable[..., None],
sockets: list[socket],
sockets: list[socket.socket],
) -> SpawnProcess:
"""
Called in the parent process, to instantiate a new child process instance.
@ -41,10 +73,15 @@ def get_subprocess(
except (AttributeError, OSError):
stdin_fileno = None
socket_shares: list[SocketShareRebind] | list[SocketSharePickle]
if config.socket_load_balance: # pragma: py-darwin pragma: py-win32
socket_shares = [SocketShareRebind(s) for s in sockets]
else:
socket_shares = [SocketSharePickle(s) for s in sockets]
kwargs = {
"config": config,
"target": target,
"sockets": sockets,
"sockets": socket_shares,
"stdin_fileno": stdin_fileno,
}
@ -54,7 +91,7 @@ def get_subprocess(
def subprocess_started(
config: Config,
target: Callable[..., None],
sockets: list[socket],
sockets: list[SocketSharePickle] | list[SocketShareRebind],
stdin_fileno: int | None,
) -> None:
"""
@ -77,8 +114,8 @@ def subprocess_started(
try:
# Now we can call into `Server.run(sockets=sockets)`
target(sockets=sockets)
target(sockets=[s.get() for s in sockets])
except KeyboardInterrupt: # pragma: no cover
# suppress the exception to avoid a traceback from subprocess.Popen
# supress the exception to avoid a traceback from subprocess.Popen
# the parent already expects us to end, so no vital information is lost
pass

View File

@ -32,8 +32,20 @@ from __future__ import annotations
import sys
import types
from collections.abc import Awaitable, Callable, Iterable, MutableMapping
from typing import Any, Literal, Protocol, TypedDict
from typing import (
Any,
Awaitable,
Callable,
Iterable,
Literal,
MutableMapping,
Optional,
Protocol,
Tuple,
Type,
TypedDict,
Union,
)
if sys.version_info >= (3, 11): # pragma: py-lt-311
from typing import NotRequired
@ -42,9 +54,9 @@ else: # pragma: py-gte-311
# WSGI
Environ = MutableMapping[str, Any]
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]
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]]
# ASGI
@ -93,8 +105,8 @@ class LifespanScope(TypedDict):
state: NotRequired[dict[str, Any]]
WWWScope = HTTPScope | WebSocketScope
Scope = HTTPScope | WebSocketScope | LifespanScope
WWWScope = Union[HTTPScope, WebSocketScope]
Scope = Union[HTTPScope, WebSocketScope, LifespanScope]
class HTTPRequestEvent(TypedDict):
@ -159,7 +171,7 @@ class _WebSocketReceiveEventText(TypedDict):
text: str
WebSocketReceiveEvent = _WebSocketReceiveEventBytes | _WebSocketReceiveEventText
WebSocketReceiveEvent = Union[_WebSocketReceiveEventBytes, _WebSocketReceiveEventText]
class _WebSocketSendEventBytes(TypedDict):
@ -174,7 +186,7 @@ class _WebSocketSendEventText(TypedDict):
text: str
WebSocketSendEvent = _WebSocketSendEventBytes | _WebSocketSendEventText
WebSocketSendEvent = Union[_WebSocketSendEventBytes, _WebSocketSendEventText]
class WebSocketResponseStartEvent(TypedDict):
@ -227,36 +239,36 @@ class LifespanShutdownFailedEvent(TypedDict):
message: str
WebSocketEvent = WebSocketReceiveEvent | WebSocketDisconnectEvent | WebSocketConnectEvent
WebSocketEvent = Union[WebSocketReceiveEvent, WebSocketDisconnectEvent, WebSocketConnectEvent]
ASGIReceiveEvent = (
HTTPRequestEvent
| HTTPDisconnectEvent
| WebSocketConnectEvent
| WebSocketReceiveEvent
| WebSocketDisconnectEvent
| LifespanStartupEvent
| LifespanShutdownEvent
)
ASGIReceiveEvent = Union[
HTTPRequestEvent,
HTTPDisconnectEvent,
WebSocketConnectEvent,
WebSocketReceiveEvent,
WebSocketDisconnectEvent,
LifespanStartupEvent,
LifespanShutdownEvent,
]
ASGISendEvent = (
HTTPResponseStartEvent
| HTTPResponseBodyEvent
| HTTPResponseTrailersEvent
| HTTPServerPushEvent
| HTTPDisconnectEvent
| WebSocketAcceptEvent
| WebSocketSendEvent
| WebSocketResponseStartEvent
| WebSocketResponseBodyEvent
| WebSocketCloseEvent
| LifespanStartupCompleteEvent
| LifespanStartupFailedEvent
| LifespanShutdownCompleteEvent
| LifespanShutdownFailedEvent
)
ASGISendEvent = Union[
HTTPResponseStartEvent,
HTTPResponseBodyEvent,
HTTPResponseTrailersEvent,
HTTPServerPushEvent,
HTTPDisconnectEvent,
WebSocketAcceptEvent,
WebSocketSendEvent,
WebSocketResponseStartEvent,
WebSocketResponseBodyEvent,
WebSocketCloseEvent,
LifespanStartupCompleteEvent,
LifespanStartupFailedEvent,
LifespanShutdownCompleteEvent,
LifespanShutdownFailedEvent,
]
ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]]
@ -269,6 +281,13 @@ class ASGI2Protocol(Protocol):
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover
ASGI2Application = type[ASGI2Protocol]
ASGI3Application = Callable[[Scope, ASGIReceiveCallable, ASGISendCallable], Awaitable[None]]
ASGIApplication = ASGI2Application | ASGI3Application
ASGI2Application = Type[ASGI2Protocol]
ASGI3Application = Callable[
[
Scope,
ASGIReceiveCallable,
ASGISendCallable,
],
Awaitable[None],
]
ASGIApplication = Union[ASGI2Application, ASGI3Application]

View File

@ -9,14 +9,12 @@ 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 IO, Any, Literal
from typing import IO, Any, Awaitable, Callable, 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
@ -26,9 +24,9 @@ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from uvicorn.middleware.wsgi import WSGIMiddleware
HTTPProtocolType = Literal["auto", "h11", "httptools"]
WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"]
WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
LifespanType = Literal["auto", "on", "off"]
LoopFactoryType = Literal["none", "auto", "asyncio", "uvloop"]
LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
LOG_LEVELS: dict[str, int] = {
@ -39,28 +37,27 @@ LOG_LEVELS: dict[str, int] = {
"debug": logging.DEBUG,
"trace": TRACE_LOG_LEVEL,
}
HTTP_PROTOCOLS: dict[str, str] = {
HTTP_PROTOCOLS: dict[HTTPProtocolType, str] = {
"auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol",
"h11": "uvicorn.protocols.http.h11_impl:H11Protocol",
"httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol",
}
WS_PROTOCOLS: dict[str, str | None] = {
WS_PROTOCOLS: dict[WSProtocolType, 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[str, str] = {
LIFESPAN: dict[LifespanType, str] = {
"auto": "uvicorn.lifespan.on:LifespanOn",
"on": "uvicorn.lifespan.on:LifespanOn",
"off": "uvicorn.lifespan.off:LifespanOff",
}
LOOP_FACTORIES: dict[str, str | None] = {
LOOP_SETUPS: dict[LoopSetupType, str | None] = {
"none": None,
"auto": "uvicorn.loops.auto:auto_loop_factory",
"asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory",
"uvloop": "uvicorn.loops.uvloop:uvloop_loop_factory",
"auto": "uvicorn.loops.auto:auto_loop_setup",
"asyncio": "uvicorn.loops.asyncio:asyncio_setup",
"uvloop": "uvicorn.loops.uvloop:uvloop_setup",
}
INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"]
@ -140,7 +137,7 @@ def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str
# Special case for the .* pattern, otherwise this would only match
# hidden directories which is probably undesired
if pattern == ".*":
continue # pragma: py-not-linux
continue
patterns.append(pattern)
if is_dir(Path(pattern)):
directories.append(Path(pattern))
@ -183,9 +180,9 @@ class Config:
port: int = 8000,
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",
loop: LoopSetupType = "auto",
http: type[asyncio.Protocol] | HTTPProtocolType = "auto",
ws: type[asyncio.Protocol] | WSProtocolType = "auto",
ws_max_size: int = 16 * 1024 * 1024,
ws_max_queue: int = 32,
ws_ping_interval: float | None = 20.0,
@ -193,7 +190,7 @@ class Config:
ws_per_message_deflate: bool = True,
lifespan: LifespanType = "auto",
env_file: str | os.PathLike[str] | None = None,
log_config: dict[str, Any] | str | os.PathLike[str] | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
log_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
log_level: str | int | None = None,
access_log: bool = True,
use_colors: bool | None = None,
@ -211,25 +208,22 @@ class Config:
root_path: str = "",
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: int | None = None,
timeout_worker_healthcheck: int = 5,
callback_notify: Callable[..., Awaitable[None]] | None = None,
ssl_keyfile: str | os.PathLike[str] | None = None,
ssl_keyfile: 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: str | os.PathLike[str] | None = None,
ssl_ca_certs: str | None = None,
ssl_ciphers: str = "TLSv1",
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: int | None = None,
reset_contextvars: bool = False,
socket_load_balance: bool = False,
):
self.app = app
self.host = host
@ -259,12 +253,10 @@ 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
@ -273,12 +265,11 @@ class Config:
self.ssl_cert_reqs = ssl_cert_reqs
self.ssl_ca_certs = ssl_ca_certs
self.ssl_ciphers = ssl_ciphers
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.socket_load_balance = socket_load_balance
self.loaded = False
self.configure_logging()
@ -290,7 +281,7 @@ class Config:
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:
@ -323,7 +314,7 @@ class Config:
+ "directories, watching current working directory.",
reload_dirs,
)
self.reload_dirs = [Path.cwd()]
self.reload_dirs = [Path(os.getcwd())]
logger.info(
"Will watch for changes in these directories: %s",
@ -359,7 +350,7 @@ class Config:
@property
def is_ssl(self) -> bool:
return bool(self.ssl_keyfile or self.ssl_certfile or self.ssl_context_factory)
return bool(self.ssl_keyfile or self.ssl_certfile)
@property
def use_subprocess(self) -> bool:
@ -369,9 +360,6 @@ 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
@ -382,12 +370,9 @@ class Config:
loaded_config = json.load(file)
logging.config.dictConfig(loaded_config)
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
# Install the PyYAML package or the uvicorn[standard] optional
# dependencies to enable this functionality.
import yaml
with open(self.log_config) as file:
loaded_config = yaml.safe_load(file)
@ -399,7 +384,7 @@ class Config:
if self.log_level is not None:
if isinstance(self.log_level, str):
log_level = LOG_LEVELS[self.log_level.lower()]
log_level = LOG_LEVELS[self.log_level]
else:
log_level = self.log_level
logging.getLogger("uvicorn.error").setLevel(log_level)
@ -409,43 +394,12 @@ 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.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:
if self.is_ssl:
assert self.ssl_certfile
self.ssl = create_ssl_context(
self.ssl: ssl.SSLContext | None = create_ssl_context(
keyfile=self.ssl_keyfile,
certfile=self.ssl_certfile,
password=self.ssl_keyfile_password,
@ -465,20 +419,24 @@ class Config:
)
if isinstance(self.http, str):
http_protocol_class = import_from_string(HTTP_PROTOCOLS.get(self.http, self.http))
http_protocol_class = import_from_string(HTTP_PROTOCOLS[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.get(self.ws, self.ws))
ws_protocol_class = import_from_string(WS_PROTOCOLS[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])
self.loaded_app = self.load_app()
try:
self.loaded_app = import_from_string(self.app)
except ImportFromStringError as exc:
logger.error("Error loading ASGI app. %s" % exc)
sys.exit(1)
try:
self.loaded_app = self.loaded_app()
@ -489,17 +447,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 = iscoroutinefunction(self.loaded_app)
use_asgi_3 = asyncio.iscoroutinefunction(self.loaded_app)
else:
call = getattr(self.loaded_app, "__call__", None)
use_asgi_3 = iscoroutinefunction(call)
use_asgi_3 = asyncio.iscoroutinefunction(call)
self.interface = "asgi3" if use_asgi_3 else "asgi2"
if self.interface == "wsgi":
@ -516,28 +474,13 @@ class Config:
self.loaded = True
def setup_event_loop(self) -> None:
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)
loop_setup: Callable | None = import_from_string(LOOP_SETUPS[self.loop])
if loop_setup is not None:
loop_setup(use_subprocess=self.use_subprocess)
def bind_socket(self) -> socket.socket:
logger_args: list[str | int]
if self.uds is not None: # pragma: py-win32
if self.uds: # pragma: py-win32
path = self.uds
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
@ -552,7 +495,7 @@ class Config:
sock_name_format = "%s"
color_message = "Uvicorn running on " + click.style(sock_name_format, bold=True) + " (Press CTRL+C to quit)"
logger_args = [self.uds]
elif self.fd is not None: # pragma: py-win32
elif self.fd: # 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"

View File

@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio
import logging
from asyncio import Queue
from typing import Any
from typing import Any, Union
from uvicorn import Config
from uvicorn._types import (
@ -16,13 +16,13 @@ from uvicorn._types import (
LifespanStartupFailedEvent,
)
LifespanReceiveMessage = LifespanStartupEvent | LifespanShutdownEvent
LifespanSendMessage = (
LifespanStartupFailedEvent
| LifespanShutdownFailedEvent
| LifespanStartupCompleteEvent
| LifespanShutdownCompleteEvent
)
LifespanReceiveMessage = Union[LifespanStartupEvent, LifespanShutdownEvent]
LifespanSendMessage = Union[
LifespanStartupFailedEvent,
LifespanShutdownFailedEvent,
LifespanStartupCompleteEvent,
LifespanShutdownCompleteEvent,
]
STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol."
@ -38,7 +38,7 @@ class LifespanOn:
self.startup_event = asyncio.Event()
self.shutdown_event = asyncio.Event()
self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue()
self.error_occurred = False
self.error_occured = False
self.startup_failed = False
self.shutdown_failed = False
self.should_exit = False
@ -50,26 +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/Kludex/uvicorn/pull/972
# See https://github.com/encode/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_occurred and self.config.lifespan == "on"):
if self.startup_failed or (self.error_occured 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_occurred:
if self.error_occured:
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_occurred and self.config.lifespan == "on"):
if self.shutdown_failed or (self.error_occured 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_occurred = True
self.error_occured = True
if self.startup_failed or self.shutdown_failed:
return
if self.config.lifespan == "auto":

View File

@ -16,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 `extra={"color_message": ...}` it will be used
* If a log call includes an `extras={"color_message": ...}` it will be used
for formatting the output, instead of the plain text message.
"""
@ -55,13 +55,13 @@ class ColourizedFormatter(logging.Formatter):
def formatMessage(self, record: logging.LogRecord) -> str:
recordcopy = copy(record)
levelname = recordcopy.levelname
separator = " " * (8 - len(recordcopy.levelname))
seperator = " " * (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 + ":" + separator
recordcopy.__dict__["levelprefix"] = levelname + ":" + seperator
return super().formatMessage(recordcopy)

View File

@ -1,11 +1,10 @@
from __future__ import annotations
import asyncio
import logging
import sys
from collections.abc import Callable
logger = logging.getLogger("uvicorn.error")
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
def asyncio_setup(use_subprocess: bool = False) -> None:
if sys.platform == "win32" and use_subprocess:
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # pragma: full coverage

View File

@ -1,17 +1,11 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
def auto_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
def auto_loop_setup(use_subprocess: bool = False) -> None:
try:
import uvloop # noqa
except ImportError: # pragma: no cover
from uvicorn.loops.asyncio import asyncio_loop_factory as loop_factory
from uvicorn.loops.asyncio import asyncio_setup as loop_setup
return loop_factory(use_subprocess=use_subprocess)
loop_setup(use_subprocess=use_subprocess)
else: # pragma: no cover
from uvicorn.loops.uvloop import uvloop_loop_factory
from uvicorn.loops.uvloop import uvloop_setup
return uvloop_loop_factory(use_subprocess=use_subprocess)
uvloop_setup(use_subprocess=use_subprocess)

View File

@ -1,10 +1,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
import uvloop
def uvloop_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
return uvloop.new_event_loop
def uvloop_setup(use_subprocess: bool = False) -> None:
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

View File

@ -6,40 +6,39 @@ import os
import platform
import ssl
import sys
import warnings
from collections.abc import Callable
from configparser import RawConfigParser
from typing import IO, Any, get_args
from typing import IO, Any, Callable
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,
LoopFactoryType,
LoopSetupType,
WSProtocolType,
)
from uvicorn.server import Server
from uvicorn.server import Server, ServerState # noqa: F401 # Used to be defined here.
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")
@ -82,7 +81,7 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
"--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,7 +106,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",
@ -118,24 +117,21 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
)
@click.option(
"--loop",
type=str,
metavar=_metavar_from_type(LoopFactoryType),
type=LOOP_CHOICES,
default="auto",
help="Event loop factory implementation.",
help="Event loop implementation.",
show_default=True,
)
@click.option(
"--http",
type=str,
metavar=_metavar_from_type(HTTPProtocolType),
type=HTTP_CHOICES,
default="auto",
help="HTTP protocol implementation.",
show_default=True,
)
@click.option(
"--ws",
type=str,
metavar=_metavar_from_type(WSProtocolType),
type=WS_CHOICES,
default="auto",
help="WebSocket protocol implementation.",
show_default=True,
@ -226,7 +222,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 to populate url scheme and remote address info.",
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For, X-Forwarded-Port to " "populate remote address info.",
)
@click.option(
"--server-header/--no-server-header",
@ -259,7 +255,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",
@ -273,19 +269,11 @@ 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 (in seconds).",
help="Close Keep-Alive connections if no new data is received within this timeout.",
show_default=True,
)
@click.option(
@ -294,13 +282,6 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
default=None,
help="Maximum number of seconds to wait for graceful shutdown.",
)
@click.option(
"--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",
@ -372,13 +353,6 @@ 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,
@ -386,15 +360,22 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> No
help="Treat APP as an application factory, i.e. a () -> <ASGI app> callable.",
show_default=True,
)
@click.option(
"--socket-load-balance",
is_flag=True,
default=False,
help="Use kernel support for socket load balancing",
show_default=True,
)
def main(
app: str,
host: str,
port: int,
uds: str,
fd: int,
loop: LoopFactoryType | str,
http: HTTPProtocolType | str,
ws: WSProtocolType | str,
loop: LoopSetupType,
http: HTTPProtocolType,
ws: WSProtocolType,
ws_max_size: int,
ws_max_queue: int,
ws_ping_interval: float,
@ -420,10 +401,8 @@ def main(
limit_concurrency: int,
backlog: int,
limit_max_requests: int,
limit_max_requests_jitter: int,
timeout_keep_alive: int,
timeout_graceful_shutdown: int | None,
timeout_worker_healthcheck: int,
ssl_keyfile: str,
ssl_certfile: str,
ssl_keyfile_password: str,
@ -435,8 +414,8 @@ def main(
use_colors: bool,
app_dir: str,
h11_max_incomplete_event_size: int | None,
reset_contextvars: bool,
factory: bool,
socket_load_balance: bool = False,
) -> None:
run(
app,
@ -472,10 +451,8 @@ 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,
@ -488,7 +465,7 @@ def main(
factory=factory,
app_dir=app_dir,
h11_max_incomplete_event_size=h11_max_incomplete_event_size,
reset_contextvars=reset_contextvars,
socket_load_balance=socket_load_balance,
)
@ -499,9 +476,9 @@ def run(
port: int = 8000,
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",
loop: LoopSetupType = "auto",
http: type[asyncio.Protocol] | HTTPProtocolType = "auto",
ws: type[asyncio.Protocol] | WSProtocolType = "auto",
ws_max_size: int = 16777216,
ws_max_queue: int = 32,
ws_ping_interval: float | None = 20.0,
@ -516,7 +493,7 @@ def run(
reload_delay: float = 0.25,
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_config: dict[str, Any] | str | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
log_level: str | int | None = None,
access_log: bool = True,
proxy_headers: bool = True,
@ -527,24 +504,21 @@ def run(
limit_concurrency: int | None = None,
backlog: int = 2048,
limit_max_requests: int | None = None,
limit_max_requests_jitter: int = 0,
timeout_keep_alive: int = 5,
timeout_graceful_shutdown: int | None = None,
timeout_worker_healthcheck: int = 5,
ssl_keyfile: str | os.PathLike[str] | None = None,
ssl_keyfile: 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: str | os.PathLike[str] | None = None,
ssl_ca_certs: str | None = None,
ssl_ciphers: str = "TLSv1",
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: int | None = None,
reset_contextvars: bool = False,
socket_load_balance: bool = False,
) -> None:
if app_dir is not None:
sys.path.insert(0, app_dir)
@ -583,10 +557,8 @@ 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,
@ -594,32 +566,30 @@ 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,
socket_load_balance=socket_load_balance,
)
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)
config.load_app()
server = Server(config=config)
try:
if config.should_reload:
sock = config.bind_socket()
ChangeReload(config, target=server.run, sockets=[sock]).run()
with config.bind_socket() as sock:
ChangeReload(config, target=server.run, sockets=[sock]).run()
elif config.workers > 1:
sock = config.bind_socket()
Multiprocess(config, target=server.run, sockets=[sock]).run()
with config.bind_socket() as sock:
Multiprocess(config, target=server.run, sockets=[sock]).run()
else:
server.run()
except KeyboardInterrupt: # pragma: full coverage
pass
except KeyboardInterrupt:
pass # pragma: full coverage
finally:
if config.uds and os.path.exists(config.uds):
os.remove(config.uds) # pragma: py-win32
@ -628,17 +598,5 @@ def run(
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

View File

@ -45,12 +45,16 @@ class ProxyHeadersMiddleware:
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)
host = self.trusted_hosts.get_trusted_client_host(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
# See: https://github.com/encode/uvicorn/issues/1068
# We've lost the connecting client's port information by now,
# so only include the host.
port = 0
scope["client"] = (host, port)
return await self.app(scope, receive, send)
@ -60,46 +64,11 @@ 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.always_trust: bool = trusted_hosts == "*"
self.trusted_literals: set[str] = set()
self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set()
@ -153,22 +122,21 @@ class _TrustedHosts:
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.
def get_trusted_client_host(self, x_forwarded_for: str) -> str:
"""Extract the client host 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])
return 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)
for host in reversed(x_forwarded_for_hosts):
if host not in self:
return host, port
return host
# 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])
# See https://github.com/encode/uvicorn/issues/1068#issuecomment-855371576
return x_forwarded_for_hosts[0]

View File

@ -6,7 +6,7 @@ import io
import sys
import warnings
from collections import deque
from collections.abc import Iterable
from typing import Iterable
from uvicorn._types import (
ASGIReceiveCallable,
@ -82,7 +82,8 @@ def build_environ(scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO)
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
@ -150,7 +151,7 @@ class WSGIResponder:
if message is None:
return
await send(message)
else: # pragma: no cover
else:
await self.send_event.wait()
self.send_event.clear()

View File

@ -1,12 +1,9 @@
from __future__ import annotations
import asyncio
import contextvars
import http
import logging
import sys
from collections.abc import Callable
from typing import Any, Literal
from typing import Any, Callable, Literal, cast
from urllib.parse import unquote
import h11
@ -78,7 +75,7 @@ class H11Protocol(asyncio.Protocol):
# Per-connection state
self.transport: asyncio.Transport = None # type: ignore[assignment]
self.flow: FlowControl = None # type: ignore[assignment]
self.server: tuple[str, int | None] | None = None
self.server: tuple[str, int] | None = None
self.client: tuple[str, int] | None = None
self.scheme: Literal["http", "https"] | None = None
@ -203,7 +200,10 @@ class H11Protocol(asyncio.Protocol):
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.4",
},
"http_version": event.http_version.decode("ascii"),
"server": self.server,
"client": self.client,
@ -250,16 +250,7 @@ class H11Protocol(asyncio.Protocol):
message_event=asyncio.Event(),
on_response=self.on_response_complete,
)
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 = self.loop.create_task(self.cycle.run_asgi(app))
task.add_done_callback(self.tasks.discard)
self.tasks.add(task)
@ -402,7 +393,7 @@ class RequestResponseCycle:
self.waiting_for_100_continue = conn.they_are_waiting_for_100_continue
# Request state
self.body = bytearray()
self.body = b""
self.more_body = True
# Response state
@ -457,6 +448,8 @@ class RequestResponseCycle:
# ASGI interface
async def send(self, message: ASGISendEvent) -> None:
message_type = message["type"]
if self.flow.write_paused and not self.disconnected:
await self.flow.drain() # pragma: full coverage
@ -465,8 +458,10 @@ class RequestResponseCycle:
if not self.response_started:
# Sending response status line and headers
if message["type"] != "http.response.start":
raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.")
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)
self.response_started = True
self.waiting_for_100_continue = False
@ -495,8 +490,10 @@ class RequestResponseCycle:
elif not self.response_complete:
# Sending response body
if message["type"] != "http.response.body":
raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.")
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)
body = message.get("body", b"")
more_body = message.get("more_body", False)
@ -515,7 +512,8 @@ class RequestResponseCycle:
else:
# Response already sent
raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.")
msg = "Unexpected ASGI message '%s' sent, after response already completed."
raise RuntimeError(msg % message_type)
if self.response_complete:
if self.conn.our_state is h11.MUST_CLOSE or not self.keep_alive:
@ -539,6 +537,10 @@ class RequestResponseCycle:
if self.disconnected or self.response_complete:
return {"type": "http.disconnect"}
message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body}
self.body = bytearray()
message: HTTPRequestEvent = {
"type": "http.request",
"body": self.body,
"more_body": self.more_body,
}
self.body = b""
return message

View File

@ -1,16 +1,13 @@
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 collections.abc import Callable
from typing import Any, Literal
from typing import Any, Callable, Literal, cast
import httptools
@ -19,6 +16,7 @@ from uvicorn._types import (
ASGIReceiveEvent,
ASGISendEvent,
HTTPRequestEvent,
HTTPResponseStartEvent,
HTTPScope,
)
from uvicorn.config import Config
@ -27,7 +25,7 @@ from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT,
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_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:[]={} \t\\"]')
HEADER_VALUE_RE = re.compile(b"[\x00-\x08\x0a-\x1f\x7f]")
@ -60,14 +58,6 @@ 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
@ -85,7 +75,7 @@ class HttpToolsProtocol(asyncio.Protocol):
# Per-connection state
self.transport: asyncio.Transport = None # type: ignore[assignment]
self.flow: FlowControl = None # type: ignore[assignment]
self.server: tuple[str, int | None] | None = None
self.server: tuple[str, int] | None = None
self.client: tuple[str, int] | None = None
self.scheme: Literal["http", "https"] | None = None
self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque()
@ -224,7 +214,7 @@ class HttpToolsProtocol(asyncio.Protocol):
self.headers = []
self.scope = { # type: ignore[typeddict-item]
"type": "http",
"asgi": {"version": self.config.asgi_version, "spec_version": "2.3"},
"asgi": {"version": self.config.asgi_version, "spec_version": "2.4"},
"http_version": "1.1",
"server": self.server,
"client": self.client,
@ -289,26 +279,14 @@ class HttpToolsProtocol(asyncio.Protocol):
)
if existing_cycle is None or existing_cycle.response_complete:
# Standard case - start processing the request.
self._start_asgi_task(self.cycle, app)
task = self.loop.create_task(self.cycle.run_asgi(app))
task.add_done_callback(self.tasks.discard)
self.tasks.add(task)
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:
return
@ -339,7 +317,9 @@ class HttpToolsProtocol(asyncio.Protocol):
# Keep-Alive timeout instead.
if self.pipeline:
cycle, app = self.pipeline.pop()
self._start_asgi_task(cycle, app)
task = self.loop.create_task(cycle.run_asgi(app))
task.add_done_callback(self.tasks.discard)
self.tasks.add(task)
else:
self.timeout_keep_alive_task = self.loop.call_later(
self.timeout_keep_alive, self.timeout_keep_alive_handler
@ -406,7 +386,7 @@ class RequestResponseCycle:
self.waiting_for_100_continue = expect_100_continue
# Request state
self.body = bytearray()
self.body = b""
self.more_body = True
# Response state
@ -460,6 +440,8 @@ class RequestResponseCycle:
# ASGI interface
async def send(self, message: ASGISendEvent) -> None:
message_type = message["type"]
if self.flow.write_paused and not self.disconnected:
await self.flow.drain() # pragma: full coverage
@ -468,8 +450,10 @@ class RequestResponseCycle:
if not self.response_started:
# Sending response status line and headers
if message["type"] != "http.response.start":
raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.")
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)
self.response_started = True
self.waiting_for_100_continue = False
@ -520,10 +504,11 @@ class RequestResponseCycle:
elif not self.response_complete:
# Sending response body
if message["type"] != "http.response.body":
raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.")
if message_type != "http.response.body":
msg = "Expected ASGI message 'http.response.body', but got '%s'."
raise RuntimeError(msg % message_type)
body = message.get("body", b"")
body = cast(bytes, message.get("body", b""))
more_body = message.get("more_body", False)
# Write response body
@ -557,7 +542,8 @@ class RequestResponseCycle:
else:
# Response already sent
raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.")
msg = "Unexpected ASGI message '%s' sent, after response already completed."
raise RuntimeError(msg % message_type)
async def receive(self) -> ASGIReceiveEvent:
if self.waiting_for_100_continue and not self.transport.is_closing():
@ -571,6 +557,6 @@ class RequestResponseCycle:
if self.disconnected or self.response_complete:
return {"type": "http.disconnect"}
message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body}
self.body = bytearray()
message: HTTPRequestEvent = {"type": "http.request", "body": self.body, "more_body": self.more_body}
self.body = b""
return message

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import asyncio
import socket
import urllib.parse
from uvicorn._types import WWWScope
@ -11,7 +10,7 @@ class ClientDisconnected(OSError): ...
def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None:
socket_info: socket.socket | None = transport.get_extra_info("socket")
socket_info = transport.get_extra_info("socket")
if socket_info is not None:
try:
info = socket_info.getpeername()
@ -22,25 +21,20 @@ def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None:
return None
info = transport.get_extra_info("peername")
if info is not None and isinstance(info, list | tuple) and len(info) == 2:
if info is not None and isinstance(info, (list, tuple)) and len(info) == 2:
return (str(info[0]), int(info[1]))
return None
def get_local_addr(transport: asyncio.Transport) -> tuple[str, int | None] | None:
socket_info: socket.socket | None = transport.get_extra_info("socket")
def get_local_addr(transport: asyncio.Transport) -> tuple[str, int] | None:
socket_info = transport.get_extra_info("socket")
if socket_info is not None:
info = socket_info.getsockname()
if isinstance(info, tuple):
return (str(info[0]), int(info[1]))
if isinstance(info, str):
return (info, None)
return None
return (str(info[0]), int(info[1])) if isinstance(info, tuple) else None
info = transport.get_extra_info("sockname")
if info is not None and isinstance(info, list | tuple) and len(info) == 2:
if info is not None and isinstance(info, (list, tuple)) and len(info) == 2:
return (str(info[0]), int(info[1]))
if isinstance(info, str):
return (info, None)
return None

View File

@ -1,9 +1,9 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
import typing
AutoWebSocketsProtocol: Callable[..., asyncio.Protocol] | None
AutoWebSocketsProtocol: typing.Callable[..., asyncio.Protocol] | None
try:
import websockets # noqa
except ImportError: # pragma: no cover

View File

@ -3,8 +3,7 @@ from __future__ import annotations
import asyncio
import http
import logging
from collections.abc import Sequence
from typing import Any, Literal, cast
from typing import Any, Literal, Optional, Sequence, cast
from urllib.parse import unquote
import websockets
@ -20,16 +19,20 @@ from websockets.typing import Subprotocol
from uvicorn._types import (
ASGI3Application,
ASGISendEvent,
WebSocketAcceptEvent,
WebSocketCloseEvent,
WebSocketConnectEvent,
WebSocketDisconnectEvent,
WebSocketReceiveEvent,
WebSocketResponseBodyEvent,
WebSocketResponseStartEvent,
WebSocketScope,
WebSocketSendEvent,
)
from uvicorn.config import Config
from uvicorn.logging import TRACE_LOG_LEVEL
from uvicorn.protocols.utils import (
ClientDisconnected,
get_client_addr,
get_local_addr,
get_path_with_query_string,
get_remote_addr,
@ -77,7 +80,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
# Connection state
self.transport: asyncio.Transport = None # type: ignore[assignment]
self.server: tuple[str, int | None] | None = None
self.server: tuple[str, int] | None = None
self.client: tuple[str, int] | None = None
self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
@ -210,7 +213,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
def send_500_response(self) -> None:
msg = b"Internal Server Error"
content = [
b"HTTP/1.1 500 Internal Server Error\r\ncontent-type: text/plain; charset=utf-8\r\n",
b"HTTP/1.1 500 Internal Server Error\r\n" b"content-type: text/plain; charset=utf-8\r\n",
b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n",
b"connection: close\r\n",
b"\r\n",
@ -218,7 +221,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
]
self.transport.write(b"".join(content))
# Allow handler task to terminate cleanly, as websockets doesn't cancel it by
# itself (see https://github.com/Kludex/uvicorn/issues/920)
# itself (see https://github.com/encode/uvicorn/issues/920)
self.handshake_started_event.set()
async def ws_handler(self, protocol: WebSocketServerProtocol, path: str) -> Any: # type: ignore[override]
@ -239,6 +242,7 @@ class WebSocketProtocol(WebSocketServerProtocol):
result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]
except ClientDisconnected: # pragma: full coverage
self.closed_event.set()
self.transport.close()
except BaseException:
self.closed_event.set()
self.logger.exception("Exception in ASGI application\n")
@ -246,26 +250,31 @@ class WebSocketProtocol(WebSocketServerProtocol):
self.send_500_response()
else:
await self.handshake_completed_event.wait()
self.transport.close()
else:
self.closed_event.set()
if not self.handshake_started_event.is_set():
self.logger.error("ASGI callable returned without sending handshake.")
self.send_500_response()
self.transport.close()
elif result is not None:
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
await self.handshake_completed_event.wait()
self.transport.close()
await self.handshake_completed_event.wait()
self.transport.close()
async def asgi_send(self, message: ASGISendEvent) -> None:
message_type = message["type"]
if not self.handshake_started_event.is_set():
if message["type"] == "websocket.accept":
if message_type == "websocket.accept":
message = cast("WebSocketAcceptEvent", message)
self.logger.info(
'%s - "WebSocket %s" [accepted]',
get_client_addr(self.scope),
self.scope["client"],
get_path_with_query_string(self.scope),
)
self.initial_response = None
self.accepted_subprotocol = cast(Subprotocol | None, message.get("subprotocol"))
self.accepted_subprotocol = cast(Optional[Subprotocol], message.get("subprotocol"))
if "headers" in message:
self.extra_headers.extend(
# ASGI spec requires bytes
@ -275,20 +284,22 @@ class WebSocketProtocol(WebSocketServerProtocol):
)
self.handshake_started_event.set()
elif message["type"] == "websocket.close":
elif message_type == "websocket.close":
message = cast("WebSocketCloseEvent", message)
self.logger.info(
'%s - "WebSocket %s" 403',
get_client_addr(self.scope),
self.scope["client"],
get_path_with_query_string(self.scope),
)
self.initial_response = (http.HTTPStatus.FORBIDDEN, [], b"")
self.handshake_started_event.set()
self.closed_event.set()
elif message["type"] == "websocket.http.response.start":
elif message_type == "websocket.http.response.start":
message = cast("WebSocketResponseStartEvent", message)
self.logger.info(
'%s - "WebSocket %s" %d',
get_client_addr(self.scope),
self.scope["client"],
get_path_with_query_string(self.scope),
message["status"],
)
@ -301,48 +312,50 @@ class WebSocketProtocol(WebSocketServerProtocol):
self.handshake_started_event.set()
else:
raise RuntimeError(
msg = (
"Expected ASGI message 'websocket.accept', 'websocket.close', "
f"or 'websocket.http.response.start' but got '{message['type']}'."
"or 'websocket.http.response.start' but got '%s'."
)
raise RuntimeError(msg % message_type)
elif not self.closed_event.is_set() and self.initial_response is None:
await self.handshake_completed_event.wait()
try:
if message["type"] == "websocket.send":
if message_type == "websocket.send":
message = cast("WebSocketSendEvent", message)
bytes_data = message.get("bytes")
text_data = message.get("text")
data = text_data if bytes_data is None else bytes_data
await self.send(data) # type: ignore[arg-type]
elif message["type"] == "websocket.close":
elif message_type == "websocket.close":
message = cast("WebSocketCloseEvent", message)
code = message.get("code", 1000)
reason = message.get("reason", "") or ""
await self.close(code, reason)
self.closed_event.set()
else:
raise RuntimeError(
f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'."
)
msg = "Expected ASGI message 'websocket.send' or 'websocket.close'," " but got '%s'."
raise RuntimeError(msg % message_type)
except ConnectionClosed as exc:
raise ClientDisconnected from exc
elif self.initial_response is not None:
if message["type"] == "websocket.http.response.body":
if message_type == "websocket.http.response.body":
message = cast("WebSocketResponseBodyEvent", message)
body = self.initial_response[2] + message["body"]
self.initial_response = self.initial_response[:2] + (body,)
if not message.get("more_body", False):
self.closed_event.set()
else:
raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.")
msg = "Expected ASGI message 'websocket.http.response.body' " "but got '%s'."
raise RuntimeError(msg % message_type)
else:
raise RuntimeError(
f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close' "
"or response already completed."
)
msg = "Unexpected ASGI message '%s', after sending 'websocket.close' " "or response already completed."
raise RuntimeError(msg % message_type)
async def asgi_receive(self) -> WebSocketDisconnectEvent | WebSocketConnectEvent | WebSocketReceiveEvent:
if not self.connect_sent:

View File

@ -1,477 +0,0 @@
from __future__ import annotations
import asyncio
import logging
import random
import struct
import sys
from asyncio import TimerHandle
from asyncio.transports import BaseTransport, Transport
from http import HTTPStatus
from typing import Any, Literal, cast
from urllib.parse import unquote
from websockets.exceptions import InvalidState
from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
from websockets.frames import Frame, Opcode
from websockets.http11 import Request
from websockets.server import ServerProtocol
from uvicorn._types import (
ASGIReceiveEvent,
ASGISendEvent,
WebSocketScope,
)
from uvicorn.config import Config
from uvicorn.logging import TRACE_LOG_LEVEL
from uvicorn.protocols.utils import (
ClientDisconnected,
get_client_addr,
get_local_addr,
get_path_with_query_string,
get_remote_addr,
is_ssl,
)
from uvicorn.server import ServerState
if sys.version_info >= (3, 11): # pragma: no cover
from typing import assert_never
else: # pragma: no cover
from typing_extensions import assert_never
class WebSocketsSansIOProtocol(asyncio.Protocol):
def __init__(
self,
config: Config,
server_state: ServerState,
app_state: dict[str, Any],
_loop: asyncio.AbstractEventLoop | None = None,
) -> None:
if not config.loaded:
config.load() # pragma: no cover
self.config = config
self.app = config.loaded_app
self.loop = _loop or asyncio.get_event_loop()
self.logger = logging.getLogger("uvicorn.error")
self.root_path = config.root_path
self.app_state = app_state
# Shared server state
self.connections = server_state.connections
self.tasks = server_state.tasks
self.default_headers = server_state.default_headers
# Connection state
self.transport: asyncio.Transport = None # type: ignore[assignment]
self.server: tuple[str, int | None] | None = None
self.client: tuple[str, int] | None = None
self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
# WebSocket state
self.queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue()
self.handshake_initiated = False
self.handshake_complete = False
self.close_sent = False
self.initial_response: tuple[int, list[tuple[str, str]], bytes] | None = None
extensions = []
if self.config.ws_per_message_deflate:
extensions = [
ServerPerMessageDeflateFactory(
server_max_window_bits=12,
client_max_window_bits=12,
compress_settings={"memLevel": 5},
)
]
self.conn = ServerProtocol(
extensions=extensions,
max_size=self.config.ws_max_size,
logger=logging.getLogger("uvicorn.error"),
)
self.read_paused = False
self.writable = asyncio.Event()
self.writable.set()
# Keepalive state
self.ping_interval = config.ws_ping_interval
self.ping_timeout = config.ws_ping_timeout
self.ping_timer: TimerHandle | None = None
self.pong_timer: TimerHandle | None = None
self.pending_ping_payload: bytes | None = None
self.ping_sent_at: float = 0.0
self.last_ping_rtt: float = 0.0
# Buffers
self.bytes = bytearray()
def connection_made(self, transport: BaseTransport) -> None:
"""Called when a connection is made."""
transport = cast(Transport, transport)
self.connections.add(self)
self.transport = transport
self.server = get_local_addr(transport)
self.client = get_remote_addr(transport)
self.scheme = "wss" if is_ssl(transport) else "ws"
if self.logger.level <= TRACE_LOG_LEVEL:
prefix = "%s:%d - " % self.client if self.client else ""
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
def connection_lost(self, exc: Exception | None) -> None:
self.stop_keepalive()
code = 1005 if self.handshake_complete else 1006
self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
self.connections.remove(self)
if self.logger.level <= TRACE_LOG_LEVEL:
prefix = "%s:%d - " % self.client if self.client else ""
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix)
self.handshake_complete = True
if exc is None:
self.transport.close()
def eof_received(self) -> None:
pass
def shutdown(self) -> None:
self.stop_keepalive()
if self.handshake_complete:
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
self.conn.send_close(1012)
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
else:
self.send_500_response()
self.transport.close()
def data_received(self, data: bytes) -> None:
self.conn.receive_data(data)
if self.conn.parser_exc is not None: # pragma: no cover
self.handle_parser_exception()
return
self.handle_events()
def handle_events(self) -> None:
for event in self.conn.events_received():
if isinstance(event, Request):
self.handle_connect(event)
if isinstance(event, Frame):
if event.opcode == Opcode.CONT:
self.handle_cont(event) # pragma: no cover
elif event.opcode == Opcode.TEXT:
self.handle_text(event)
elif event.opcode == Opcode.BINARY:
self.handle_bytes(event)
elif event.opcode == Opcode.PING:
self.handle_ping()
elif event.opcode == Opcode.PONG:
self.handle_pong(event)
elif event.opcode == Opcode.CLOSE:
self.handle_close(event)
else:
assert_never(event.opcode) # pragma: no cover
# Event handlers
def handle_connect(self, event: Request) -> None:
self.request = event
self.response = self.conn.accept(event)
self.handshake_initiated = True
if self.response.status_code != 101:
self.handshake_complete = True
self.close_sent = True
self.conn.send_response(self.response)
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
self.transport.close()
return
headers = [
(key.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
for key, value in event.headers.raw_items()
]
raw_path, _, query_string = event.path.partition("?")
self.scope: WebSocketScope = {
"type": "websocket",
"asgi": {"version": self.config.asgi_version, "spec_version": "2.4"},
"http_version": "1.1",
"scheme": self.scheme,
"server": self.server,
"client": self.client,
"root_path": self.root_path,
"path": self.root_path + unquote(raw_path),
"raw_path": self.root_path.encode("ascii") + raw_path.encode("ascii"),
"query_string": query_string.encode("ascii"),
"headers": headers,
"subprotocols": event.headers.get_all("Sec-WebSocket-Protocol"),
"state": self.app_state.copy(),
"extensions": {"websocket.http.response": {}},
}
self.queue.put_nowait({"type": "websocket.connect"})
task = self.loop.create_task(self.run_asgi())
task.add_done_callback(self.on_task_complete)
self.tasks.add(task)
def handle_cont(self, event: Frame) -> None:
self.bytes.extend(event.data)
if event.fin:
self.send_receive_event_to_app()
def handle_text(self, event: Frame) -> None:
self.bytes = bytearray(event.data)
self.curr_msg_data_type: Literal["text", "bytes"] = "text"
if event.fin:
self.send_receive_event_to_app()
def handle_bytes(self, event: Frame) -> None:
self.bytes = bytearray(event.data)
self.curr_msg_data_type = "bytes"
if event.fin:
self.send_receive_event_to_app()
def send_receive_event_to_app(self) -> None:
if self.curr_msg_data_type == "text":
try:
self.queue.put_nowait({"type": "websocket.receive", "text": self.bytes.decode()})
except UnicodeDecodeError: # pragma: no cover
self.logger.exception("Invalid UTF-8 sequence received from client.")
self.conn.send_close(1007)
self.handle_parser_exception()
return
else:
self.queue.put_nowait({"type": "websocket.receive", "bytes": bytes(self.bytes)})
if not self.read_paused:
self.read_paused = True
self.transport.pause_reading()
def handle_ping(self) -> None:
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
def handle_pong(self, event: Frame) -> None:
# Ignore unsolicited pongs and stale pongs whose payload doesn't match the ping currently in flight
if self.pending_ping_payload is None or bytes(event.data) != self.pending_ping_payload:
return # pragma: no cover
self.last_ping_rtt = self.loop.time() - self.ping_sent_at
self.pending_ping_payload = None
# The peer answered in time; cancel the pong deadline and chain the next ping. This `schedule_ping()` call is
# what keeps the keepalive loop running when ping_timeout is set. When ping_timeout is None the next ping is
# already scheduled by `send_keepalive_ping`, so we must not schedule a duplicate here.
if self.pong_timer is not None:
self.pong_timer.cancel()
self.pong_timer = None
self.schedule_ping()
def start_keepalive(self) -> None:
if self.ping_interval is not None and self.ping_interval > 0:
self.schedule_ping()
def stop_keepalive(self) -> None:
if self.ping_timer is not None:
self.ping_timer.cancel()
self.ping_timer = None
if self.pong_timer is not None: # pragma: no cover
self.pong_timer.cancel()
self.pong_timer = None
self.pending_ping_payload = None
def schedule_ping(self) -> None:
assert self.ping_interval is not None
delay = max(0.0, self.ping_interval - self.last_ping_rtt)
self.ping_timer = self.loop.call_later(delay, self.send_keepalive_ping)
def send_keepalive_ping(self) -> None:
self.ping_timer = None
if self.close_sent or self.transport.is_closing(): # pragma: no cover
return
# Random 4-byte payload identifies this ping; `handle_pong` uses it to ignore stale or unsolicited pongs.
# See https://github.com/python-websockets/websockets/blob/4d229bf9f583d593aa103287aee0a77c9fbc3a79/src/websockets/asyncio/connection.py#L624
self.pending_ping_payload = struct.pack("!I", random.getrandbits(32))
self.ping_sent_at = self.loop.time()
self.conn.send_ping(self.pending_ping_payload)
self.transport.write(b"".join(self.conn.data_to_send()))
if self.ping_timeout is not None:
self.pong_timer = self.loop.call_later(self.ping_timeout, self.keepalive_timeout)
else: # pragma: no cover
self.schedule_ping()
def keepalive_timeout(self) -> None:
self.pong_timer = None
self.pending_ping_payload = None
if self.close_sent or self.transport.is_closing(): # pragma: no cover
return
if self.logger.level <= TRACE_LOG_LEVEL:
prefix = "%s:%d - " % self.client if self.client else ""
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket keepalive ping timeout", prefix)
self.conn.fail(1011, "keepalive ping timeout")
self.transport.write(b"".join(self.conn.data_to_send()))
self.close_sent = True
self.transport.close()
def handle_close(self, event: Frame) -> None:
if not self.close_sent and not self.transport.is_closing():
assert self.conn.close_rcvd is not None
code = self.conn.close_rcvd.code
reason = self.conn.close_rcvd.reason
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
self.transport.close()
def handle_parser_exception(self) -> None: # pragma: no cover
assert self.conn.close_sent is not None
code = self.conn.close_sent.code
reason = self.conn.close_sent.reason
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
self.close_sent = True
self.transport.close()
def on_task_complete(self, task: asyncio.Task[None]) -> None:
self.tasks.discard(task)
async def run_asgi(self) -> None:
try:
result = await self.app(self.scope, self.receive, self.send)
except ClientDisconnected:
pass # pragma: full coverage
except BaseException:
self.logger.exception("Exception in ASGI application\n")
self.send_500_response()
else:
if not self.handshake_complete:
self.logger.error("ASGI callable returned without completing handshake.")
self.send_500_response()
elif result is not None:
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
self.transport.close()
def send_500_response(self) -> None:
if self.initial_response or self.handshake_complete:
return
response = self.conn.reject(500, "Internal Server Error")
self.conn.send_response(response)
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
async def send(self, message: ASGISendEvent) -> None:
await self.writable.wait()
if not self.handshake_complete and self.initial_response is None:
if message["type"] == "websocket.accept":
self.logger.info(
'%s - "WebSocket %s" [accepted]',
get_client_addr(self.scope),
get_path_with_query_string(self.scope),
)
headers = [
(name.decode("latin-1").lower(), value.decode("latin-1"))
for name, value in (self.default_headers + list(message.get("headers", [])))
]
accepted_subprotocol = message.get("subprotocol")
if accepted_subprotocol:
headers.append(("Sec-WebSocket-Protocol", accepted_subprotocol))
self.response.headers.update(headers)
if not self.transport.is_closing():
self.handshake_complete = True
self.conn.send_response(self.response)
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
self.start_keepalive()
elif message["type"] == "websocket.close":
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
self.logger.info(
'%s - "WebSocket %s" 403',
get_client_addr(self.scope),
get_path_with_query_string(self.scope),
)
response = self.conn.reject(HTTPStatus.FORBIDDEN, "")
self.conn.send_response(response)
output = self.conn.data_to_send()
self.close_sent = True
self.handshake_complete = True
self.transport.write(b"".join(output))
self.transport.close()
elif message["type"] == "websocket.http.response.start" and self.initial_response is None:
if not (100 <= message["status"] < 600):
raise RuntimeError("Invalid HTTP status code '%d' in response." % message["status"])
self.logger.info(
'%s - "WebSocket %s" %d',
get_client_addr(self.scope),
get_path_with_query_string(self.scope),
message["status"],
)
headers = [
(name.decode("latin-1"), value.decode("latin-1"))
for name, value in list(message.get("headers", []))
]
self.initial_response = (message["status"], headers, b"")
else:
raise RuntimeError(
"Expected ASGI message 'websocket.accept', 'websocket.close' "
f"or 'websocket.http.response.start' but got '{message['type']}'."
)
elif not self.close_sent and self.initial_response is None:
try:
if message["type"] == "websocket.send":
bytes_data = message.get("bytes")
text_data = message.get("text")
if bytes_data is not None:
self.conn.send_binary(bytes_data)
elif text_data is not None:
self.conn.send_text(text_data.encode())
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
elif message["type"] == "websocket.close":
if not self.transport.is_closing():
code = message.get("code", 1000)
reason = message.get("reason", "") or ""
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
self.conn.send_close(code, reason)
output = self.conn.data_to_send()
self.transport.write(b"".join(output))
self.close_sent = True
self.transport.close()
else:
raise RuntimeError(
f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'."
)
except InvalidState:
raise ClientDisconnected()
elif self.initial_response is not None:
if message["type"] == "websocket.http.response.body":
body = self.initial_response[2] + message["body"]
self.initial_response = self.initial_response[:2] + (body,)
if not message.get("more_body", False):
response = self.conn.reject(self.initial_response[0], body.decode())
response.headers.update(self.initial_response[1])
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
self.conn.send_response(response)
output = self.conn.data_to_send()
self.close_sent = True
self.transport.write(b"".join(output))
self.transport.close()
else: # pragma: no cover
raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.")
else:
raise RuntimeError(f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close'.")
async def receive(self) -> ASGIReceiveEvent:
message = await self.queue.get()
if self.read_paused and self.queue.empty():
self.read_paused = False
self.transport.resume_reading()
return message

View File

@ -2,11 +2,8 @@ from __future__ import annotations
import asyncio
import logging
import random
import struct
from asyncio import TimerHandle
from io import BytesIO, StringIO
from typing import Any, Literal, cast
import typing
from typing import Literal, cast
from urllib.parse import unquote
import wsproto
@ -15,12 +12,21 @@ from wsproto.connection import ConnectionState
from wsproto.extensions import Extension, PerMessageDeflate
from wsproto.utilities import LocalProtocolError, RemoteProtocolError
from uvicorn._types import ASGI3Application, ASGISendEvent, WebSocketEvent, WebSocketReceiveEvent, WebSocketScope
from uvicorn._types import (
ASGI3Application,
ASGISendEvent,
WebSocketAcceptEvent,
WebSocketCloseEvent,
WebSocketEvent,
WebSocketResponseBodyEvent,
WebSocketResponseStartEvent,
WebSocketScope,
WebSocketSendEvent,
)
from uvicorn.config import Config
from uvicorn.logging import TRACE_LOG_LEVEL
from uvicorn.protocols.utils import (
ClientDisconnected,
get_client_addr,
get_local_addr,
get_path_with_query_string,
get_remote_addr,
@ -29,42 +35,12 @@ from uvicorn.protocols.utils import (
from uvicorn.server import ServerState
class FrameTooLargeError(Exception):
"""Raised when accumulated websocket message bytes exceed `ws_max_size`."""
class WebsocketBuffer:
def __init__(self, max_length: int) -> None:
self.value: BytesIO | StringIO | None = None
self.length = 0
self.max_length = max_length
def extend(self, event: events.TextMessage | events.BytesMessage) -> None:
if self.value is None:
self.value = StringIO() if isinstance(event, events.TextMessage) else BytesIO()
self.value.write(event.data) # type: ignore[arg-type]
# `ws_max_size` is a byte budget, so count UTF-8 bytes for text.
self.length += len(event.data.encode()) if isinstance(event, events.TextMessage) else len(event.data)
if self.length > self.max_length:
raise FrameTooLargeError
def clear(self) -> None:
self.value = None
self.length = 0
def to_message(self) -> WebSocketReceiveEvent:
if isinstance(self.value, StringIO):
return {"type": "websocket.receive", "text": self.value.getvalue()}
assert isinstance(self.value, BytesIO)
return {"type": "websocket.receive", "bytes": self.value.getvalue()}
class WSProtocol(asyncio.Protocol):
def __init__(
self,
config: Config,
server_state: ServerState,
app_state: dict[str, Any],
app_state: dict[str, typing.Any],
_loop: asyncio.AbstractEventLoop | None = None,
) -> None:
if not config.loaded:
@ -84,7 +60,7 @@ class WSProtocol(asyncio.Protocol):
# Connection state
self.transport: asyncio.Transport = None # type: ignore[assignment]
self.server: tuple[str, int | None] | None = None
self.server: tuple[str, int] | None = None
self.client: tuple[str, int] | None = None
self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
@ -102,21 +78,15 @@ class WSProtocol(asyncio.Protocol):
self.writable = asyncio.Event()
self.writable.set()
# Keepalive state
self.ping_interval = config.ws_ping_interval
self.ping_timeout = config.ws_ping_timeout
self.ping_timer: TimerHandle | None = None
self.pong_timer: TimerHandle | None = None
self.pending_ping_payload: bytes | None = None
self.ping_sent_at: float = 0.0
self.last_ping_rtt: float = 0.0
# Buffer
self.buffer = WebsocketBuffer(self.config.ws_max_size)
# Buffers
self.bytes = b""
self.text = ""
# Protocol interface
def connection_made(self, transport: asyncio.Transport) -> None: # type: ignore[override]
def connection_made( # type: ignore[override]
self, transport: asyncio.Transport
) -> None:
self.connections.add(self)
self.transport = transport
self.server = get_local_addr(transport)
@ -128,7 +98,6 @@ class WSProtocol(asyncio.Protocol):
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
def connection_lost(self, exc: Exception | None) -> None:
self.stop_keepalive()
code = 1005 if self.handshake_complete else 1006
self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
self.connections.remove(self)
@ -156,18 +125,16 @@ class WSProtocol(asyncio.Protocol):
def handle_events(self) -> None:
for event in self.conn.events():
if self.close_sent:
return
if isinstance(event, events.Request):
self.handle_connect(event)
elif isinstance(event, (events.TextMessage, events.BytesMessage)):
self.handle_message(event)
elif isinstance(event, events.TextMessage):
self.handle_text(event)
elif isinstance(event, events.BytesMessage):
self.handle_bytes(event)
elif isinstance(event, events.CloseConnection):
self.handle_close(event)
elif isinstance(event, events.Ping):
self.handle_ping(event)
elif isinstance(event, events.Pong):
self.handle_pong(event)
def pause_writing(self) -> None:
"""
@ -182,7 +149,6 @@ class WSProtocol(asyncio.Protocol):
self.writable.set() # pragma: full coverage
def shutdown(self) -> None:
self.stop_keepalive()
if self.handshake_complete:
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
output = self.conn.send(wsproto.events.CloseConnection(code=1012))
@ -224,20 +190,21 @@ class WSProtocol(asyncio.Protocol):
task.add_done_callback(self.on_task_complete)
self.tasks.add(task)
def handle_message(self, event: events.TextMessage | events.BytesMessage) -> None:
try:
self.buffer.extend(event)
except FrameTooLargeError:
self.close_sent = True
reason = f"Message exceeds the maximum size ({self.config.ws_max_size} bytes)"
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1009, "reason": reason})
if not self.transport.is_closing():
self.transport.write(self.conn.send(wsproto.events.CloseConnection(code=1009, reason=reason)))
self.transport.close()
return
def handle_text(self, event: events.TextMessage) -> None:
self.text += event.data
if event.message_finished:
self.queue.put_nowait(self.buffer.to_message())
self.buffer.clear()
self.queue.put_nowait({"type": "websocket.receive", "text": self.text})
self.text = ""
if not self.read_paused:
self.read_paused = True
self.transport.pause_reading()
def handle_bytes(self, event: events.BytesMessage) -> None:
self.bytes += event.data
# todo: we may want to guard the size of self.bytes and self.text
if event.message_finished:
self.queue.put_nowait({"type": "websocket.receive", "bytes": self.bytes})
self.bytes = b""
if not self.read_paused:
self.read_paused = True
self.transport.pause_reading()
@ -251,72 +218,12 @@ class WSProtocol(asyncio.Protocol):
def handle_ping(self, event: events.Ping) -> None:
self.transport.write(self.conn.send(event.response()))
def handle_pong(self, event: events.Pong) -> None:
# Ignore unsolicited pongs and stale pongs whose payload doesn't match the ping currently in flight.
if self.pending_ping_payload is None or bytes(event.payload) != self.pending_ping_payload:
return # pragma: no cover
self.last_ping_rtt = self.loop.time() - self.ping_sent_at
self.pending_ping_payload = None
# The peer answered in time; cancel the pong deadline and chain the next ping. This `schedule_ping()` call is
# what keeps the keepalive loop running when ping_timeout is set. When ping_timeout is None the next ping is
# already scheduled by `send_keepalive_ping`, so we must not schedule a duplicate here.
if self.pong_timer is not None:
self.pong_timer.cancel()
self.pong_timer = None
self.schedule_ping()
def start_keepalive(self) -> None:
if self.ping_interval is not None and self.ping_interval > 0:
self.schedule_ping()
def stop_keepalive(self) -> None:
if self.ping_timer is not None:
self.ping_timer.cancel()
self.ping_timer = None
if self.pong_timer is not None: # pragma: no cover
self.pong_timer.cancel()
self.pong_timer = None
self.pending_ping_payload = None
def schedule_ping(self) -> None:
assert self.ping_interval is not None
delay = max(0.0, self.ping_interval - self.last_ping_rtt)
self.ping_timer = self.loop.call_later(delay, self.send_keepalive_ping)
def send_keepalive_ping(self) -> None:
self.ping_timer = None
if self.close_sent or self.transport.is_closing(): # pragma: no cover
return
# Random 4-byte payload identifies this ping; `handle_pong` uses it to ignore stale or unsolicited pongs.
self.pending_ping_payload = struct.pack("!I", random.getrandbits(32))
self.ping_sent_at = self.loop.time()
self.transport.write(self.conn.send(wsproto.events.Ping(payload=self.pending_ping_payload)))
if self.ping_timeout is not None:
self.pong_timer = self.loop.call_later(self.ping_timeout, self.keepalive_timeout)
else: # pragma: no cover
self.schedule_ping()
def keepalive_timeout(self) -> None:
self.pong_timer = None
self.pending_ping_payload = None
if self.close_sent or self.transport.is_closing(): # pragma: no cover
return
if self.logger.level <= TRACE_LOG_LEVEL:
prefix = "%s:%d - " % self.client if self.client else ""
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket keepalive ping timeout", prefix)
reason = "keepalive ping timeout"
self.transport.write(self.conn.send(wsproto.events.CloseConnection(code=1011, reason=reason)))
self.close_sent = True
self.transport.close()
def send_500_response(self) -> None:
if self.response_started or self.handshake_complete:
return # we cannot send responses anymore
headers: list[tuple[bytes, bytes]] = [
(b"content-type", b"text/plain; charset=utf-8"),
(b"connection", b"close"),
(b"content-length", b"21"),
]
output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True))
output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error"))
@ -326,26 +233,31 @@ class WSProtocol(asyncio.Protocol):
try:
result = await self.app(self.scope, self.receive, self.send) # type: ignore[func-returns-value]
except ClientDisconnected:
pass # pragma: full coverage
self.transport.close() # pragma: full coverage
except BaseException:
self.logger.exception("Exception in ASGI application\n")
self.send_500_response()
self.transport.close()
else:
if not self.handshake_complete:
self.logger.error("ASGI callable returned without completing handshake.")
self.send_500_response()
self.transport.close()
elif result is not None:
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
self.transport.close()
self.transport.close()
async def send(self, message: ASGISendEvent) -> None:
await self.writable.wait()
message_type = message["type"]
if not self.handshake_complete:
if message["type"] == "websocket.accept":
if message_type == "websocket.accept":
message = typing.cast(WebSocketAcceptEvent, message)
self.logger.info(
'%s - "WebSocket %s" [accepted]',
get_client_addr(self.scope),
self.scope["client"],
get_path_with_query_string(self.scope),
)
subprotocol = message.get("subprotocol")
@ -363,13 +275,12 @@ class WSProtocol(asyncio.Protocol):
)
)
self.transport.write(output)
self.start_keepalive()
elif message["type"] == "websocket.close":
elif message_type == "websocket.close":
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
self.logger.info(
'%s - "WebSocket %s" 403',
get_client_addr(self.scope),
self.scope["client"],
get_path_with_query_string(self.scope),
)
self.handshake_complete = True
@ -379,14 +290,15 @@ class WSProtocol(asyncio.Protocol):
self.transport.write(output)
self.transport.close()
elif message["type"] == "websocket.http.response.start":
elif message_type == "websocket.http.response.start":
message = typing.cast(WebSocketResponseStartEvent, message)
# ensure status code is in the valid range
if not (100 <= message["status"] < 600):
msg = "Invalid HTTP status code '%d' in response."
raise RuntimeError(msg % message["status"])
self.logger.info(
'%s - "WebSocket %s" %d',
get_client_addr(self.scope),
self.scope["client"],
get_path_with_query_string(self.scope),
message["status"],
)
@ -401,14 +313,17 @@ class WSProtocol(asyncio.Protocol):
self.response_started = True
else:
raise RuntimeError(
msg = (
"Expected ASGI message 'websocket.accept', 'websocket.close' "
f"or 'websocket.http.response.start' but got '{message['type']}'."
"or 'websocket.http.response.start' "
"but got '%s'."
)
raise RuntimeError(msg % message_type)
elif not self.close_sent and not self.response_started:
try:
if message["type"] == "websocket.send":
if message_type == "websocket.send":
message = typing.cast(WebSocketSendEvent, message)
bytes_data = message.get("bytes")
text_data = message.get("text")
data = text_data if bytes_data is None else bytes_data
@ -416,7 +331,8 @@ class WSProtocol(asyncio.Protocol):
if not self.transport.is_closing():
self.transport.write(output)
elif message["type"] == "websocket.close":
elif message_type == "websocket.close":
message = typing.cast(WebSocketCloseEvent, message)
self.close_sent = True
code = message.get("code", 1000)
reason = message.get("reason", "") or ""
@ -427,13 +343,13 @@ class WSProtocol(asyncio.Protocol):
self.transport.close()
else:
raise RuntimeError(
f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'."
)
msg = "Expected ASGI message 'websocket.send' or 'websocket.close'," " but got '%s'."
raise RuntimeError(msg % message_type)
except LocalProtocolError as exc:
raise ClientDisconnected from exc
elif self.response_started:
if message["type"] == "websocket.http.response.body":
if message_type == "websocket.http.response.body":
message = typing.cast("WebSocketResponseBodyEvent", message)
body_finished = not message.get("more_body", False)
reject_data = events.RejectData(data=message["body"], body_finished=body_finished)
output = self.conn.send(reject_data)
@ -445,10 +361,12 @@ class WSProtocol(asyncio.Protocol):
self.transport.close()
else:
raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.")
msg = "Expected ASGI message 'websocket.http.response.body' " "but got '%s'."
raise RuntimeError(msg % message_type)
else:
raise RuntimeError(f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close'.")
msg = "Unexpected ASGI message '%s', after sending 'websocket.close'."
raise RuntimeError(msg % message_type)
async def receive(self) -> WebSocketEvent:
message = await self.queue.get()

View File

@ -2,34 +2,29 @@ from __future__ import annotations
import asyncio
import contextlib
import functools
import logging
import os
import platform
import random
import signal
import socket
import sys
import threading
import time
from collections.abc import Generator, Sequence
from email.utils import formatdate
from types import FrameType
from typing import TYPE_CHECKING, TypeAlias
from typing import TYPE_CHECKING, Generator, Sequence, Union
import click
from uvicorn._compat import asyncio_run
from uvicorn.config import Config
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.websockets_sansio_impl import WebSocketsSansIOProtocol
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
Protocols: TypeAlias = H11Protocol | HttpToolsProtocol | WSProtocol | WebSocketProtocol | WebSocketsSansIOProtocol
Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol]
HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
@ -65,14 +60,9 @@ class Server:
self._captured_signals: list[int] = []
@functools.cached_property
def limit_max_requests(self) -> int | None:
if self.config.limit_max_requests is None:
return None
return self.config.limit_max_requests + random.randint(0, self.config.limit_max_requests_jitter)
def run(self, sockets: list[socket.socket] | None = None) -> None:
return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory())
self.config.setup_event_loop()
return asyncio.run(self.serve(sockets=sockets))
async def serve(self, sockets: list[socket.socket] | None = None) -> None:
with self.capture_signals():
@ -92,14 +82,14 @@ class Server:
logger.info(message, process_id, extra={"color_message": color_message})
await self.startup(sockets=sockets)
if not self.should_exit:
await self.main_loop()
if self.started:
await self.shutdown(sockets=sockets)
if self.should_exit:
return
await self.main_loop()
await self.shutdown(sockets=sockets)
message = "Finished server process [%d]"
color_message = "Finished server process [" + click.style("%d", fg="cyan") + "]"
logger.info(message, process_id, extra={"color_message": color_message})
message = "Finished server process [%d]"
color_message = "Finished server process [" + click.style("%d", fg="cyan") + "]"
logger.info(message, process_id, extra={"color_message": color_message})
async def startup(self, sockets: list[socket.socket] | None = None) -> None:
await self.lifespan.startup()
@ -128,7 +118,7 @@ class Server:
def _share_socket(
sock: socket.SocketType,
) -> socket.SocketType: # pragma py-not-win32
) -> socket.SocketType: # pragma py-linux pragma: py-darwin
# Windows requires the socket be explicitly shared across
# multiple workers (processes).
from socket import fromshare # type: ignore[attr-defined]
@ -260,12 +250,8 @@ class Server:
# Determine if we should exit.
if self.should_exit:
return True
max_requests = self.limit_max_requests
if max_requests is not None and self.server_state.total_requests >= max_requests:
logger.info("Maximum request limit of %d exceeded. Terminating process.", max_requests)
return True
if self.config.limit_max_requests is not None:
return self.server_state.total_requests >= self.config.limit_max_requests
return False
async def shutdown(self, sockets: list[socket.socket] | None = None) -> None:
@ -294,7 +280,10 @@ class Server:
len(self.server_state.tasks),
)
for t in self.server_state.tasks:
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
if sys.version_info < (3, 9): # pragma: py-gte-39
t.cancel()
else: # pragma: py-lt-39
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
# Send the lifespan shutdown event, and wait for application shutdown.
if not self.force_exit:

View File

@ -9,8 +9,15 @@ if TYPE_CHECKING:
ChangeReload: type[BaseReload]
else:
try:
from uvicorn.supervisors.watchfilesreload import WatchFilesReload as ChangeReload
from uvicorn.supervisors.watchfilesreload import (
WatchFilesReload as ChangeReload,
)
except ImportError: # pragma: no cover
from uvicorn.supervisors.statreload import StatReload as ChangeReload
try:
from uvicorn.supervisors.watchgodreload import (
WatchGodReload as ChangeReload,
)
except ImportError:
from uvicorn.supervisors.statreload import StatReload as ChangeReload
__all__ = ["Multiprocess", "ChangeReload"]

View File

@ -5,10 +5,10 @@ import os
import signal
import sys
import threading
from collections.abc import Callable, Iterator
from pathlib import Path
from socket import socket
from types import FrameType
from typing import Callable, Iterator
import click
@ -89,10 +89,6 @@ class BaseReload:
self.is_restarting = True
assert self.process.pid is not None
os.kill(self.process.pid, signal.CTRL_C_EVENT)
# This is a workaround to ensure the Ctrl+C event is processed
sys.stdout.write(" ") # This has to be a non-empty string
sys.stdout.flush()
else: # pragma: py-win32
self.process.terminate()
self.process.join()

View File

@ -4,10 +4,9 @@ import logging
import os
import signal
import threading
from collections.abc import Callable
from multiprocessing import Pipe
from socket import socket
from typing import Any
from typing import Any, Callable
import click
@ -165,7 +164,7 @@ class Multiprocess:
return # parent process is exiting, no need to keep subprocess alive
for idx, process in enumerate(self.processes):
if process.is_alive(timeout=self.config.timeout_worker_healthcheck):
if process.is_alive():
continue
process.kill() # process is hung, kill it

View File

@ -1,9 +1,9 @@
from __future__ import annotations
import logging
from collections.abc import Callable, Iterator
from pathlib import Path
from socket import socket
from typing import Callable, Iterator
from uvicorn.config import Config
from uvicorn.supervisors.basereload import BaseReload
@ -23,7 +23,7 @@ class StatReload(BaseReload):
self.mtimes: dict[Path, float] = {}
if config.reload_excludes or config.reload_includes:
logger.warning("--reload-include and --reload-exclude have no effect unless watchfiles is installed.")
logger.warning("--reload-include and --reload-exclude have no effect unless " "watchfiles is installed.")
def should_restart(self) -> list[Path] | None:
self.pause()

View File

@ -1,8 +1,8 @@
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from socket import socket
from typing import Callable
from watchfiles import watch
@ -61,9 +61,12 @@ class WatchFilesReload(BaseReload):
) -> None:
super().__init__(config, target, sockets)
self.reloader_name = "WatchFiles"
self.reload_dirs: list[Path] = []
self.reload_dirs = []
for directory in config.reload_dirs:
self.reload_dirs.append(directory)
if Path.cwd() not in directory.parents:
self.reload_dirs.append(directory)
if Path.cwd() not in self.reload_dirs:
self.reload_dirs.append(Path.cwd())
self.watch_filter = FileFilter(config)
self.watcher = watch(
@ -73,7 +76,6 @@ class WatchFilesReload(BaseReload):
# using yield_on_timeout here mostly to make sure tests don't
# hang forever, won't affect the class's behavior
yield_on_timeout=True,
ignore_permission_denied=True,
)
def should_restart(self) -> list[Path] | None:

View File

@ -0,0 +1,152 @@
from __future__ import annotations
import logging
import warnings
from pathlib import Path
from socket import socket
from typing import TYPE_CHECKING, Callable
from watchgod import DefaultWatcher
from uvicorn.config import Config
from uvicorn.supervisors.basereload import BaseReload
if TYPE_CHECKING:
import os
DirEntry = os.DirEntry[str]
logger = logging.getLogger("uvicorn.error")
class CustomWatcher(DefaultWatcher):
def __init__(self, root_path: Path, config: Config):
default_includes = ["*.py"]
self.includes = [default for default in default_includes if default not in config.reload_excludes]
self.includes.extend(config.reload_includes)
self.includes = list(set(self.includes))
default_excludes = [".*", ".py[cod]", ".sw.*", "~*"]
self.excludes = [default for default in default_excludes if default not in config.reload_includes]
self.excludes.extend(config.reload_excludes)
self.excludes = list(set(self.excludes))
self.watched_dirs: dict[str, bool] = {}
self.watched_files: dict[str, bool] = {}
self.dirs_includes = set(config.reload_dirs)
self.dirs_excludes = set(config.reload_dirs_excludes)
self.resolved_root = root_path
super().__init__(str(root_path))
def should_watch_file(self, entry: DirEntry) -> bool:
cached_result = self.watched_files.get(entry.path)
if cached_result is not None:
return cached_result
entry_path = Path(entry)
# cwd is not verified through should_watch_dir, so we need to verify here
if entry_path.parent == Path.cwd() and Path.cwd() not in self.dirs_includes:
self.watched_files[entry.path] = False
return False
for include_pattern in self.includes:
if str(entry_path).endswith(include_pattern):
self.watched_files[entry.path] = True
return True
if entry_path.match(include_pattern):
for exclude_pattern in self.excludes:
if entry_path.match(exclude_pattern):
self.watched_files[entry.path] = False
return False
self.watched_files[entry.path] = True
return True
self.watched_files[entry.path] = False
return False
def should_watch_dir(self, entry: DirEntry) -> bool:
cached_result = self.watched_dirs.get(entry.path)
if cached_result is not None:
return cached_result
entry_path = Path(entry)
if entry_path in self.dirs_excludes:
self.watched_dirs[entry.path] = False
return False
for exclude_pattern in self.excludes:
if entry_path.match(exclude_pattern):
is_watched = False
if entry_path in self.dirs_includes:
is_watched = True
for directory in self.dirs_includes:
if directory in entry_path.parents:
is_watched = True
if is_watched:
logger.debug(
"WatchGodReload detected a new excluded dir '%s' in '%s'; " "Adding to exclude list.",
entry_path.relative_to(self.resolved_root),
str(self.resolved_root),
)
self.watched_dirs[entry.path] = False
self.dirs_excludes.add(entry_path)
return False
if entry_path in self.dirs_includes:
self.watched_dirs[entry.path] = True
return True
for directory in self.dirs_includes:
if directory in entry_path.parents:
self.watched_dirs[entry.path] = True
return True
for include_pattern in self.includes:
if entry_path.match(include_pattern):
logger.info(
"WatchGodReload detected a new reload dir '%s' in '%s'; " "Adding to watch list.",
str(entry_path.relative_to(self.resolved_root)),
str(self.resolved_root),
)
self.dirs_includes.add(entry_path)
self.watched_dirs[entry.path] = True
return True
self.watched_dirs[entry.path] = False
return False
class WatchGodReload(BaseReload):
def __init__(
self,
config: Config,
target: Callable[[list[socket] | None], None],
sockets: list[socket],
) -> None:
warnings.warn(
'"watchgod" is deprecated, you should switch ' "to watchfiles (`pip install watchfiles`).",
DeprecationWarning,
)
super().__init__(config, target, sockets)
self.reloader_name = "WatchGod"
self.watchers = []
reload_dirs = []
for directory in config.reload_dirs:
if Path.cwd() not in directory.parents:
reload_dirs.append(directory)
if Path.cwd() not in reload_dirs:
reload_dirs.append(Path.cwd())
for w in reload_dirs:
self.watchers.append(CustomWatcher(w.resolve(), self.config))
def should_restart(self) -> list[Path] | None:
self.pause()
for watcher in self.watchers:
change = watcher.check()
if change != set():
return list({Path(c[1]) for c in change})
return None

Some files were not shown because too many files have changed in this diff Show More