├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-issue.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── publish.yml │ └── test-suite.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── docs ├── CNAME ├── contributing.md ├── deployment │ ├── docker.md │ └── index.md ├── img │ ├── gh-actions-fail-check.png │ └── gh-actions-fail.png ├── index.md ├── overrides │ ├── main.html │ └── partials │ │ ├── nav.html │ │ └── toc-item.html ├── plugins │ └── main.py ├── release-notes.md ├── server-behavior.md ├── settings.md ├── sponsorship.md └── uvicorn.png ├── mkdocs.yml ├── pyproject.toml ├── requirements.txt ├── scripts ├── build ├── check ├── coverage ├── docs ├── install ├── lint ├── publish ├── sync-version └── test ├── tests ├── __init__.py ├── conftest.py ├── importer │ ├── __init__.py │ ├── circular_import_a.py │ ├── circular_import_b.py │ ├── raise_import_error.py │ └── test_importer.py ├── middleware │ ├── __init__.py │ ├── test_logging.py │ ├── test_message_logger.py │ ├── test_proxy_headers.py │ └── test_wsgi.py ├── protocols │ ├── __init__.py │ ├── test_http.py │ ├── test_utils.py │ └── test_websocket.py ├── response.py ├── supervisors │ ├── __init__.py │ ├── test_multiprocess.py │ ├── test_reload.py │ └── test_signal.py ├── test_auto_detection.py ├── test_cli.py ├── test_config.py ├── test_default_headers.py ├── test_lifespan.py ├── test_main.py ├── test_server.py ├── test_ssl.py ├── test_subprocess.py └── utils.py └── uvicorn ├── __init__.py ├── __main__.py ├── _subprocess.py ├── _types.py ├── config.py ├── importer.py ├── lifespan ├── __init__.py ├── off.py └── on.py ├── logging.py ├── loops ├── __init__.py ├── asyncio.py ├── auto.py └── uvloop.py ├── main.py ├── middleware ├── __init__.py ├── asgi2.py ├── message_logger.py ├── proxy_headers.py └── wsgi.py ├── protocols ├── __init__.py ├── http │ ├── __init__.py │ ├── auto.py │ ├── flow_control.py │ ├── h11_impl.py │ └── httptools_impl.py ├── utils.py └── websockets │ ├── __init__.py │ ├── auto.py │ ├── websockets_impl.py │ └── wsproto_impl.py ├── py.typed ├── server.py ├── supervisors ├── __init__.py ├── basereload.py ├── multiprocess.py ├── statreload.py └── watchfilesreload.py └── workers.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Kludex 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-issue.yml: -------------------------------------------------------------------------------- 1 | name: Issue 2 | description: Report a bug or unexpected behavior. 🙏 3 | 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: Thank you for contributing to Uvicorn! ✊ 8 | 9 | - type: checkboxes 10 | id: checks 11 | attributes: 12 | label: Initial Checks 13 | description: Just making sure you open a discussion first. 🙏 14 | options: 15 | - label: I confirm this was discussed, and the maintainers suggest I open an issue. 16 | required: true 17 | - label: I'm aware that if I created this issue without a discussion, it may be closed without a response. 18 | required: true 19 | 20 | - type: textarea 21 | id: discussion 22 | attributes: 23 | label: Discussion Link 24 | description: | 25 | Please link to the discussion that led to this issue. 26 | 27 | If you haven't discussed this issue yet, please do so before opening an issue. 28 | render: Text 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: description 34 | attributes: 35 | label: Description 36 | description: | 37 | Please explain what you're seeing and what you would expect to see. 38 | 39 | Please provide as much detail as possible to make understanding and solving your problem as quick as possible. 🙏 40 | validations: 41 | required: true 42 | 43 | - type: textarea 44 | id: example 45 | attributes: 46 | label: Example Code 47 | description: > 48 | If applicable, please add a self-contained, 49 | [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) 50 | demonstrating the bug. 51 | render: Python 52 | 53 | - type: textarea 54 | id: version 55 | attributes: 56 | label: Python, Uvicorn & OS Version 57 | description: | 58 | Which version of Python & Uvicorn are you using, and which Operating System? 59 | 60 | Please run the following command and copy the output below: 61 | 62 | ```bash 63 | python -m uvicorn --version 64 | ``` 65 | 66 | render: Text 67 | validations: 68 | required: true 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 2 | blank_issues_enabled: false 3 | contact_links: 4 | - name: Discussions 5 | url: https://github.com/encode/uvicorn/discussions 6 | about: The "Discussions" forum is where you want to start. 💖 7 | - name: Chat 8 | url: https://discord.com/invite/SWU73HffbV 9 | about: Our community Discord server. 💬 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 3 | 4 | # Summary 5 | 6 | 7 | 8 | # Checklist 9 | 10 | - [ ] I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!) 11 | - [ ] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change. 12 | - [ ] I've updated the documentation accordingly. 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | python-packages: 9 | patterns: 10 | - "*" 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: monthly 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.12" 17 | 18 | - name: Install dependencies 19 | run: scripts/install 20 | 21 | - name: Build package & docs 22 | run: scripts/build 23 | 24 | - name: Upload package distributions 25 | uses: actions/upload-artifact@v4 26 | with: 27 | name: package-distributions 28 | path: dist/ 29 | 30 | - name: Upload documentation 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: documentation 34 | path: site/ 35 | 36 | pypi-publish: 37 | runs-on: ubuntu-latest 38 | needs: build 39 | 40 | permissions: 41 | id-token: write 42 | 43 | environment: 44 | name: pypi 45 | url: https://pypi.org/project/uvicorn 46 | 47 | steps: 48 | - name: Download artifacts 49 | uses: actions/download-artifact@v4 50 | with: 51 | name: package-distributions 52 | path: dist/ 53 | 54 | - name: Publish distribution 📦 to PyPI 55 | uses: pypa/gh-action-pypi-publish@release/v1 56 | 57 | docs-publish: 58 | runs-on: ubuntu-latest 59 | needs: build 60 | 61 | permissions: 62 | contents: write 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | - name: Download artifacts 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: documentation 70 | path: site/ 71 | 72 | - name: Configure Git Credentials 73 | run: | 74 | git config user.name github-actions[bot] 75 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 76 | 77 | - uses: actions/setup-python@v5 78 | with: 79 | python-version: "3.12" 80 | 81 | - name: Install dependencies 82 | run: scripts/install 83 | 84 | - name: Publish documentation 📚 to GitHub Pages 85 | run: mkdocs gh-deploy --force 86 | -------------------------------------------------------------------------------- /.github/workflows/test-suite.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test Suite 3 | 4 | on: 5 | push: 6 | branches: ["master"] 7 | pull_request: 8 | branches: ["master"] 9 | 10 | jobs: 11 | tests: 12 | name: "Python ${{ matrix.python-version }} ${{ matrix.os }}" 13 | runs-on: "${{ matrix.os }}" 14 | timeout-minutes: 10 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 19 | os: [windows-latest, ubuntu-latest, macos-latest] 20 | steps: 21 | - uses: "actions/checkout@v4" 22 | - uses: "actions/setup-python@v5" 23 | with: 24 | python-version: "${{ matrix.python-version }}" 25 | - name: "Install dependencies" 26 | run: "scripts/install" 27 | shell: bash 28 | - name: "Run linting checks" 29 | run: "scripts/check" 30 | shell: bash 31 | if: "${{ matrix.os == 'ubuntu-latest'}}" 32 | - name: "Build package & docs" 33 | run: "scripts/build" 34 | shell: bash 35 | - name: "Run tests" 36 | run: "scripts/test" 37 | shell: bash 38 | - name: "Enforce coverage" 39 | run: "scripts/coverage" 40 | shell: bash 41 | 42 | # https://github.com/marketplace/actions/alls-green#why 43 | check: 44 | if: always() 45 | needs: [tests] 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Decide whether the needed jobs succeeded or failed 49 | uses: re-actors/alls-green@release/v1 50 | with: 51 | jobs: ${{ toJSON(needs) }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .coverage 3 | .coverage.* 4 | .mypy_cache/ 5 | __pycache__/ 6 | uvicorn.egg-info/ 7 | venv/ 8 | htmlcov/ 9 | site/ 10 | dist/ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | docs/release-notes.md -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2017-present, [Encode OSS Ltd](https://www.encode.io/). 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | uvicorn 3 |

4 | 5 |

6 | An ASGI web server, for Python. 7 |

8 | 9 | --- 10 | 11 | [![Build Status](https://github.com/encode/uvicorn/workflows/Test%20Suite/badge.svg)](https://github.com/encode/uvicorn/actions) 12 | [![Package version](https://badge.fury.io/py/uvicorn.svg)](https://pypi.python.org/pypi/uvicorn) 13 | [![Supported Python Version](https://img.shields.io/pypi/pyversions/uvicorn.svg?color=%2334D058)](https://pypi.org/project/uvicorn) 14 | 15 | **Documentation**: [https://www.uvicorn.org](https://www.uvicorn.org) 16 | 17 | --- 18 | 19 | Uvicorn is an ASGI web server implementation for Python. 20 | 21 | Until recently Python has lacked a minimal low-level server/application interface for 22 | async frameworks. The [ASGI specification][asgi] fills this gap, and means we're now able to 23 | start building a common set of tooling usable across all async frameworks. 24 | 25 | Uvicorn supports HTTP/1.1 and WebSockets. 26 | 27 | ## Quickstart 28 | 29 | Install using `pip`: 30 | 31 | ```shell 32 | $ pip install uvicorn 33 | ``` 34 | 35 | This will install uvicorn with minimal (pure Python) dependencies. 36 | 37 | ```shell 38 | $ pip install 'uvicorn[standard]' 39 | ``` 40 | 41 | This will install uvicorn with "Cython-based" dependencies (where possible) and other "optional extras". 42 | 43 | In this context, "Cython-based" means the following: 44 | 45 | - the event loop `uvloop` will be installed and used if possible. 46 | - the http protocol will be handled by `httptools` if possible. 47 | 48 | Moreover, "optional extras" means that: 49 | 50 | - the websocket protocol will be handled by `websockets` (should you want to use `wsproto` you'd need to install it manually) if possible. 51 | - the `--reload` flag in development mode will use `watchfiles`. 52 | - windows users will have `colorama` installed for the colored logs. 53 | - `python-dotenv` will be installed should you want to use the `--env-file` option. 54 | - `PyYAML` will be installed to allow you to provide a `.yaml` file to `--log-config`, if desired. 55 | 56 | Create an application, in `example.py`: 57 | 58 | ```python 59 | async def app(scope, receive, send): 60 | assert scope['type'] == 'http' 61 | 62 | await send({ 63 | 'type': 'http.response.start', 64 | 'status': 200, 65 | 'headers': [ 66 | (b'content-type', b'text/plain'), 67 | ], 68 | }) 69 | await send({ 70 | 'type': 'http.response.body', 71 | 'body': b'Hello, world!', 72 | }) 73 | ``` 74 | 75 | Run the server: 76 | 77 | ```shell 78 | $ uvicorn example:app 79 | ``` 80 | 81 | --- 82 | 83 | ## Why ASGI? 84 | 85 | Most well established Python Web frameworks started out as WSGI-based frameworks. 86 | 87 | WSGI applications are a single, synchronous callable that takes a request and returns a response. 88 | This doesn’t allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections, 89 | which WSGI doesn't support well. 90 | 91 | Having an async concurrency model also allows for options such as lightweight background tasks, 92 | and can be less of a limiting factor for endpoints that have long periods being blocked on network 93 | I/O such as dealing with slow HTTP requests. 94 | 95 | --- 96 | 97 | ## Alternative ASGI servers 98 | 99 | A strength of the ASGI protocol is that it decouples the server implementation 100 | from the application framework. This allows for an ecosystem of interoperating 101 | webservers and application frameworks. 102 | 103 | ### Daphne 104 | 105 | The first ASGI server implementation, originally developed to power Django Channels, is [the Daphne webserver][daphne]. 106 | 107 | It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets. 108 | 109 | Any of the example applications given here can equally well be run using `daphne` instead. 110 | 111 | ``` 112 | $ pip install daphne 113 | $ daphne app:App 114 | ``` 115 | 116 | ### Hypercorn 117 | 118 | [Hypercorn][hypercorn] was initially part of the Quart web framework, before 119 | being separated out into a standalone ASGI server. 120 | 121 | Hypercorn supports HTTP/1.1, HTTP/2, and WebSockets. 122 | 123 | It also supports [the excellent `trio` async framework][trio], as an alternative to `asyncio`. 124 | 125 | ``` 126 | $ pip install hypercorn 127 | $ hypercorn app:App 128 | ``` 129 | 130 | ### Mangum 131 | 132 | [Mangum][mangum] is an adapter for using ASGI applications with AWS Lambda & API Gateway. 133 | 134 | ### Granian 135 | 136 | [Granian][granian] is an ASGI compatible Rust HTTP server which supports HTTP/2, TLS and WebSockets. 137 | 138 | --- 139 | 140 |

Uvicorn is BSD licensed code.
Designed & crafted with care.

— 🦄 —

141 | 142 | [asgi]: https://asgi.readthedocs.io/en/latest/ 143 | [daphne]: https://github.com/django/daphne 144 | [hypercorn]: https://github.com/pgjones/hypercorn 145 | [trio]: https://trio.readthedocs.io 146 | [mangum]: https://github.com/jordaneremieff/mangum 147 | [granian]: https://github.com/emmett-framework/granian 148 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | www.uvicorn.org 2 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for being interested in contributing to Uvicorn. 4 | There are many ways you can contribute to the project: 5 | 6 | - Using Uvicorn on your stack and [reporting bugs/issues you find](https://github.com/encode/uvicorn/issues/new) 7 | - [Implementing new features and fixing bugs](https://github.com/encode/uvicorn/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) 8 | - [Review Pull Requests of others](https://github.com/encode/uvicorn/pulls) 9 | - Write documentation 10 | - Participate in discussions 11 | 12 | ## Reporting Bugs, Issues or Feature Requests 13 | 14 | Found something that Uvicorn should support? 15 | Stumbled upon some unexpected behaviour? 16 | Need a missing functionality? 17 | 18 | Contributions should generally start out from a previous discussion. 19 | You can reach out someone at the [community chat](https://discord.com/invite/SWU73HffbV) 20 | or at the [github discussions tab](https://github.com/encode/uvicorn/discussions). 21 | 22 | When creating a new topic in the discussions tab, possible bugs may be raised 23 | as a "Potential Issue" discussion, feature requests may be raised as an 24 | "Ideas" discussion. We can then determine if the discussion needs 25 | to be escalated into an "Issue" or not, or if we'd consider a pull request. 26 | 27 | Try to be more descriptive as you can and in case of a bug report, 28 | provide as much information as possible like: 29 | 30 | - OS platform 31 | - Python version 32 | - Installed dependencies and versions (`python -m pip freeze`) 33 | - Code snippet 34 | - Error traceback 35 | 36 | You should always try to reduce any examples to the *simplest possible case* 37 | that demonstrates the issue. 38 | 39 | Some possibly useful tips for narrowing down potential issues... 40 | 41 | - Does the issue exist with a specific supervisor like `Multiprocess` or more than one? 42 | - Does the issue exist on asgi, or wsgi, or both? 43 | - Are you running Uvicorn in conjunction with Gunicorn, others, or standalone? 44 | 45 | ## Development 46 | 47 | To start developing Uvicorn create a **fork** of the 48 | [Uvicorn repository](https://github.com/encode/uvicorn) on GitHub. 49 | 50 | Then clone your fork with the following command replacing `YOUR-USERNAME` with 51 | your GitHub username: 52 | 53 | ```shell 54 | $ git clone https://github.com/YOUR-USERNAME/uvicorn 55 | ``` 56 | 57 | You can now install the project and its dependencies using: 58 | 59 | ```shell 60 | $ cd uvicorn 61 | $ scripts/install 62 | ``` 63 | 64 | ## Testing and Linting 65 | 66 | We use custom shell scripts to automate testing, linting, 67 | and documentation building workflow. 68 | 69 | To run the tests, use: 70 | 71 | ```shell 72 | $ scripts/test 73 | ``` 74 | 75 | Any additional arguments will be passed to `pytest`. See the [pytest documentation](https://docs.pytest.org/en/latest/how-to/usage.html) for more information. 76 | 77 | For example, to run a single test script: 78 | 79 | ```shell 80 | $ scripts/test tests/test_cli.py 81 | ``` 82 | 83 | To run the code auto-formatting: 84 | 85 | ```shell 86 | $ scripts/lint 87 | ``` 88 | 89 | Lastly, to run code checks separately (they are also run as part of `scripts/test`), run: 90 | 91 | ```shell 92 | $ scripts/check 93 | ``` 94 | 95 | ## Documenting 96 | 97 | Documentation pages are located under the `docs/` folder. 98 | 99 | To run the documentation site locally (useful for previewing changes), use: 100 | 101 | ```shell 102 | $ scripts/docs serve 103 | ``` 104 | 105 | ## Resolving Build / CI Failures 106 | 107 | Once you've submitted your pull request, the test suite will 108 | automatically run, and the results will show up in GitHub. 109 | If the test suite fails, you'll want to click through to the 110 | "Details" link, and try to identify why the test suite failed. 111 | 112 |

113 | Failing PR commit status 114 |

115 | 116 | Here are some common ways the test suite can fail: 117 | 118 | ### Check Job Failed 119 | 120 |

121 | Failing GitHub action lint job 122 |

123 | 124 | This job failing means there is either a code formatting issue or type-annotation issue. 125 | You can look at the job output to figure out why it's failed or within a shell run: 126 | 127 | ```shell 128 | $ scripts/check 129 | ``` 130 | 131 | It may be worth it to run `$ scripts/lint` to attempt auto-formatting the code 132 | and if that job succeeds commit the changes. 133 | 134 | ### Docs Job Failed 135 | 136 | This job failing means the documentation failed to build. This can happen for 137 | a variety of reasons like invalid markdown or missing configuration within `mkdocs.yml`. 138 | 139 | ### Python 3.X Job Failed 140 | 141 | This job failing means the unit tests failed or not all code paths are covered by unit tests. 142 | 143 | If tests are failing you will see this message under the coverage report: 144 | 145 | `=== 1 failed, 354 passed, 1 skipped, 1 xfailed in 37.08s ===` 146 | 147 | If tests succeed but coverage doesn't reach 100%, you will see this 148 | message under the coverage report: 149 | 150 | `Coverage failure: total of 98 is less than fail-under=100` 151 | 152 | ## Releasing 153 | 154 | *This section is targeted at Uvicorn maintainers.* 155 | 156 | Before releasing a new version, create a pull request that includes: 157 | 158 | - **An update to the changelog**: 159 | - We follow the format from [keepachangelog](https://keepachangelog.com/en/1.0.0/). 160 | - [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: 161 | - Things that **must** go in the changelog: added, changed, deprecated or removed features, and bug fixes. 162 | - Things that **should not** go in the changelog: changes to documentation, tests or tooling. 163 | - Try sorting entries in descending order of impact / importance. 164 | - Keep it concise and to-the-point. 🎯 165 | - **A version bump**: see `__init__.py`. 166 | 167 | For an example, see [#1006](https://github.com/encode/uvicorn/pull/1107). 168 | 169 | Once the release PR is merged, create a 170 | [new release](https://github.com/encode/uvicorn/releases/new) including: 171 | 172 | - Tag version like `0.13.3`. 173 | - Release title `Version 0.13.3` 174 | - Description copied from the changelog. 175 | 176 | Once created this release will be automatically uploaded to PyPI. 177 | 178 | If something goes wrong with the PyPI job the release can be published using the 179 | `scripts/publish` script. 180 | -------------------------------------------------------------------------------- /docs/deployment/docker.md: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | 3 | **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. 4 | 5 | While the example below won't fit every use case, it offers an excellent starting point that you can adapt to your specific needs. 6 | 7 | 8 | ## Quickstart 9 | 10 | For this example, we'll need to install [`docker`](https://docs.docker.com/get-docker/), 11 | [docker-compose](https://docs.docker.com/compose/install/) and 12 | [`uv`](https://docs.astral.sh/uv/getting-started/installation/). 13 | 14 | Then, let's create a new project with `uv`: 15 | 16 | ```bash 17 | uv init app 18 | ``` 19 | 20 | This will create a new project with a basic structure: 21 | 22 | ```bash 23 | app/ 24 | ├── main.py 25 | ├── pyproject.toml 26 | └── README.md 27 | ``` 28 | 29 | On `main.py`, let's create a simple ASGI application: 30 | 31 | ```python title="main.py" 32 | async def app(scope, receive, send): 33 | body = "Hello, world!" 34 | await send( 35 | { 36 | "type": "http.response.start", 37 | "status": 200, 38 | "headers": [ 39 | [b"content-type", b"text/plain"], 40 | [b"content-length", len(body)], 41 | ], 42 | } 43 | ) 44 | await send( 45 | { 46 | "type": "http.response.body", 47 | "body": body.encode("utf-8"), 48 | } 49 | ) 50 | ``` 51 | 52 | We need to include `uvicorn` in the dependencies: 53 | 54 | ```bash 55 | uv add uvicorn 56 | ``` 57 | 58 | This will also create a `uv.lock` file. :sunglasses: 59 | 60 | ??? tip "What is `uv.lock`?" 61 | 62 | `uv.lock` is a `uv` specific lockfile. A lockfile is a file that contains the exact versions of the dependencies 63 | that were installed when the `uv.lock` file was created. 64 | 65 | This allows for deterministic builds and consistent deployments. 66 | 67 | Just to make sure everything is working, let's run the application: 68 | 69 | ```bash 70 | uv run uvicorn main:app 71 | ``` 72 | 73 | You should see the following output: 74 | 75 | ```bash 76 | INFO: Started server process [62727] 77 | INFO: Waiting for application startup. 78 | INFO: ASGI 'lifespan' protocol appears unsupported. 79 | INFO: Application startup complete. 80 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 81 | ``` 82 | 83 | ## Dockerfile 84 | 85 | 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. 86 | 87 | ```dockerfile title="Dockerfile" 88 | FROM python:3.12-slim 89 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ 90 | 91 | # Change the working directory to the `app` directory 92 | WORKDIR /app 93 | 94 | # Install dependencies 95 | RUN --mount=type=cache,target=/root/.cache/uv \ 96 | --mount=type=bind,source=uv.lock,target=uv.lock \ 97 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 98 | uv sync --frozen --no-install-project 99 | 100 | # Copy the project into the image 101 | ADD . /app 102 | 103 | # Sync the project 104 | RUN --mount=type=cache,target=/root/.cache/uv \ 105 | uv sync --frozen 106 | 107 | # Run with uvicorn 108 | CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] 109 | ``` 110 | 111 | A common question is **"how many workers should I run?"**. The image above uses a single Uvicorn worker. 112 | The recommended approach is to let your orchestration system manage the number of deployed containers rather than 113 | relying on the process manager inside the container. 114 | 115 | You can read more about this in the 116 | [Decouple applications](https://docs.docker.com/build/building/best-practices/#decouple-applications) section 117 | of the Docker documentation. 118 | 119 | !!! warning "For production, create a non-root user!" 120 | When running in production, you should create a non-root user and run the container as that user. 121 | 122 | To make sure it works, let's build the image and run it: 123 | 124 | ```bash 125 | docker build -t my-app . 126 | docker run -p 8000:8000 my-app 127 | ``` 128 | 129 | For more information on using uv with Docker, refer to the 130 | [official uv Docker integration guide](https://docs.astral.sh/uv/guides/integration/docker/). 131 | 132 | ## Docker Compose 133 | 134 | When running in development, it's often useful to have a way to hot-reload the application when code changes. 135 | 136 | Let's create a `docker-compose.yml` file to run the application: 137 | 138 | ```yaml title="docker-compose.yml" 139 | services: 140 | backend: 141 | build: . 142 | ports: 143 | - "8000:8000" 144 | environment: 145 | - UVICORN_RELOAD=true 146 | volumes: 147 | - .:/app 148 | tty: true 149 | ``` 150 | 151 | You can run the application with `docker compose up` and it will automatically rebuild the image when code changes. 152 | 153 | Now you have a fully working development environment! :tada: 154 | -------------------------------------------------------------------------------- /docs/img/gh-actions-fail-check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/docs/img/gh-actions-fail-check.png -------------------------------------------------------------------------------- /docs/img/gh-actions-fail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/docs/img/gh-actions-fail.png -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block announce %} 4 | 13 | If you're using uvicorn in production, please consider sponsoring the project to help with maintenance and development. ✨ 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /docs/overrides/partials/nav.html: -------------------------------------------------------------------------------- 1 | {% import "partials/nav-item.html" as item with context %} 2 | 3 | 4 | {% set class = "md-nav md-nav--primary" %} 5 | {% if "navigation.tabs" in features %} 6 | {% set class = class ~ " md-nav--lifted" %} 7 | {% endif %} 8 | {% if "toc.integrate" in features %} 9 | {% set class = class ~ " md-nav--integrated" %} 10 | {% endif %} 11 | 12 | 13 | 48 | -------------------------------------------------------------------------------- /docs/overrides/partials/toc-item.html: -------------------------------------------------------------------------------- 1 | 2 |
  • 3 | 4 | 5 | {{ toc_item.title }} 6 | 7 | 8 | 9 | 10 | {% if toc_item.children %} 11 | 17 | {% endif %} 18 | 19 | -------------------------------------------------------------------------------- /docs/plugins/main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations as _annotations 2 | 3 | import re 4 | import subprocess 5 | from functools import lru_cache 6 | 7 | from mkdocs.config import Config 8 | from mkdocs.structure.files import Files 9 | from mkdocs.structure.pages import Page 10 | 11 | 12 | def on_page_content(html: str, page: Page, config: Config, files: Files) -> str: 13 | """Called on each page after the markdown is converted to HTML.""" 14 | html = add_hyperlink_to_pull_request(html, page, config, files) 15 | return html 16 | 17 | 18 | def add_hyperlink_to_pull_request(html: str, page: Page, config: Config, files: Files) -> str: 19 | """Add hyperlink on PRs mentioned on the release notes page. 20 | 21 | If we find "(#\\d+)" it will be added an hyperlink to https://github.com/encode/uvicorn/pull/$1. 22 | """ 23 | if not page.file.name == "release-notes": 24 | return html 25 | 26 | return re.sub(r"\(#(\d+)\)", r"(#\1)", html) 27 | 28 | 29 | def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str: 30 | """Called on each file after it is read and before it is converted to HTML.""" 31 | markdown = uvicorn_print_help(markdown, page) 32 | return markdown 33 | 34 | 35 | def uvicorn_print_help(markdown: str, page: Page) -> str: 36 | return re.sub(r"{{ *uvicorn_help *}}", get_uvicorn_help(), markdown) 37 | 38 | 39 | @lru_cache 40 | def get_uvicorn_help(): 41 | output = subprocess.run(["uvicorn", "--help"], capture_output=True, check=True) 42 | return output.stdout.decode() 43 | -------------------------------------------------------------------------------- /docs/server-behavior.md: -------------------------------------------------------------------------------- 1 | # Server Behavior 2 | 3 | Uvicorn is designed with particular attention to connection and resource management, in order to provide a robust server implementation. It aims to ensure graceful behavior to either server or client errors, and resilience to poor client behavior or denial of service attacks. 4 | 5 | ## HTTP Headers 6 | 7 | The `Server` and `Date` headers are added to all outgoing requests. 8 | 9 | If a `Connection: Close` header is included then Uvicorn will close the connection after the response. Otherwise connections will stay open, pending the keep-alive timeout. 10 | 11 | If a `Content-Length` header is included then Uvicorn will ensure that the content length of the response body matches the value in the header, and raise an error otherwise. 12 | 13 | If no `Content-Length` header is included then Uvicorn will use chunked encoding for the response body, and will set a `Transfer-Encoding` header if required. 14 | 15 | If a `Transfer-Encoding` header is included then any `Content-Length` header will be ignored. 16 | 17 | HTTP headers are mandated to be case-insensitive. Uvicorn will always send response headers strictly in lowercase. 18 | 19 | --- 20 | 21 | ## Flow Control 22 | 23 | Proper flow control ensures that large amounts of data do not become buffered on the transport when either side of a connection is sending data faster than its counterpart is able to handle. 24 | 25 | ### Write flow control 26 | 27 | If the write buffer passes a high water mark, then Uvicorn ensures the ASGI `send` messages will only return once the write buffer has been drained below the low water mark. 28 | 29 | ### Read flow control 30 | 31 | Uvicorn will pause reading from a transport once the buffered request body hits a high water mark, and will only resume once `receive` has been called, or once the response has been sent. 32 | 33 | --- 34 | 35 | ## Request and Response bodies 36 | 37 | ### Response completion 38 | 39 | Once a response has been sent, Uvicorn will no longer buffer any remaining request body. Any later calls to `receive` will return an `http.disconnect` message. 40 | 41 | Together with the read flow control, this behavior ensures that responses that return without reading the request body will not stream any substantial amounts of data into memory. 42 | 43 | ### Expect: 100-Continue 44 | 45 | The `Expect: 100-Continue` header may be sent by clients to require a confirmation from the server before uploading the request body. This can be used to ensure that large request bodies are only sent once the client has confirmation that the server is willing to accept the request. 46 | 47 | Uvicorn ensures that any required `100 Continue` confirmations are only sent if the ASGI application calls `receive` to read the request body. 48 | 49 | Note that proxy configurations may not necessarily forward on `Expect: 100-Continue` headers. In particular, Nginx defaults to buffering request bodies, and automatically sends `100 Continues` rather than passing the header on to the upstream server. 50 | 51 | ### HEAD requests 52 | 53 | Uvicorn will strip any response body from HTTP requests with the `HEAD` method. 54 | 55 | Applications should generally treat `HEAD` requests in the same manner as `GET` requests, in order to ensure that identical headers are sent in both cases, and that any ASGI middleware that modifies the headers will operate identically in either case. 56 | 57 | One exception to this might be if your application serves large file downloads, in which case you might wish to only generate the response headers. 58 | 59 | --- 60 | 61 | ## Timeouts 62 | 63 | Uvicorn provides the following timeouts: 64 | 65 | * Keep-Alive. Defaults to 5 seconds. Between requests, connections must receive new data within this period or be disconnected. 66 | 67 | --- 68 | 69 | ## Resource Limits 70 | 71 | Uvicorn provides the following resource limiting: 72 | 73 | * Concurrency. Defaults to `None`. If set, this provides a maximum number of concurrent tasks *or* open connections that should be allowed. Any new requests or connections that occur once this limit has been reached will result in a "503 Service Unavailable" response. Setting this value to a limit that you know your servers are able to support will help ensure reliable resource usage, even against significantly over-resourced servers. 74 | * Max requests. Defaults to `None`. If set, this provides a maximum number of HTTP requests that will be serviced before terminating a process. Together with a process manager this can be used to prevent memory leaks from impacting long running processes. 75 | 76 | --- 77 | 78 | ## Server Errors 79 | 80 | Server errors will be logged at the `error` log level. All logging defaults to being written to `stdout`. 81 | 82 | ### Exceptions 83 | 84 | 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. 85 | 86 | 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) 87 | 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`. 88 | 89 | ### Invalid responses 90 | 91 | 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. 92 | 93 | --- 94 | 95 | ## Graceful Process Shutdown 96 | 97 | Graceful process shutdowns are particularly important during a restart period. During this period you want to: 98 | 99 | * Start a number of new server processes to handle incoming requests, listening on the existing socket. 100 | * Stop the previous server processes from listening on the existing socket. 101 | * Close any connections that are not currently waiting on an HTTP response, and wait for any other connections to finalize their HTTP responses. 102 | * Wait for any background tasks to run to completion, such as occurs when the ASGI application has sent the HTTP response, but the asyncio task has not yet run to completion. 103 | 104 | Uvicorn handles process shutdown gracefully, ensuring that connections are properly finalized, and all tasks have run to completion. During a shutdown period Uvicorn will ensure that responses and tasks must still complete within the configured timeout periods. 105 | 106 | --- 107 | 108 | ## HTTP Pipelining 109 | 110 | HTTP/1.1 provides support for sending multiple requests on a single connection, before having received each corresponding response. Servers are required to support HTTP pipelining, but it is now generally accepted to lead to implementation issues. It is not enabled on browsers, and may not necessarily be enabled on any proxies that the HTTP request passes through. 111 | 112 | Uvicorn supports pipelining pragmatically. It will queue up any pipelined HTTP requests, and pause reading from the underlying transport. It will not start processing pipelined requests until each response has been dealt with in turn. 113 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | Use the following options to configure Uvicorn, when running from the command line. 4 | 5 | ## Configuration Methods 6 | 7 | There are three ways to configure Uvicorn: 8 | 9 | 1. **Command Line**: Use command line options when running Uvicorn directly. 10 | ```bash 11 | uvicorn main:app --host 0.0.0.0 --port 8000 12 | ``` 13 | 14 | 2. **Programmatic**: Use keyword arguments when running programmatically with `uvicorn.run()`. 15 | ```python 16 | uvicorn.run("main:app", host="0.0.0.0", port=8000) 17 | ``` 18 | 19 | !!! note 20 | When using `reload=True` or `workers=NUM`, you should put `uvicorn.run` into 21 | an `if __name__ == '__main__'` clause in the main module. 22 | 23 | 3. **Environment Variables**: Use environment variables with the prefix `UVICORN_`. 24 | ```bash 25 | export UVICORN_HOST="0.0.0.0" 26 | export UVICORN_PORT="8000" 27 | uvicorn main:app 28 | ``` 29 | 30 | CLI options and the arguments for `uvicorn.run()` take precedence over environment variables. 31 | 32 | Also note that `UVICORN_*` prefixed settings cannot be used from within an environment 33 | configuration file. Using an environment configuration file with the `--env-file` flag is 34 | intended for configuring the ASGI application that uvicorn runs, rather than configuring 35 | uvicorn itself. 36 | 37 | ## Application 38 | 39 | * `APP` - The ASGI application to run, in the format `":"`. 40 | * `--factory` - Treat `APP` as an application factory, i.e. a `() -> ` callable. 41 | * `--app-dir ` - Look for APP in the specified directory by adding it to the PYTHONPATH. **Default:** *Current working directory*. 42 | 43 | ## Socket Binding 44 | 45 | * `--host ` - 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'*. 46 | * `--port ` - Bind to a socket with this port. If set to 0, an available port will be picked. **Default:** *8000*. 47 | * `--uds ` - Bind to a UNIX domain socket, for example `--uds /tmp/uvicorn.sock`. Useful if you want to run Uvicorn behind a reverse proxy. 48 | * `--fd ` - Bind to socket from this file descriptor. Useful if you want to run Uvicorn within a process manager. 49 | 50 | ## Development 51 | 52 | * `--reload` - Enable auto-reload. Uvicorn supports two versions of auto-reloading behavior enabled by this option. **Default:** *False*. 53 | * `--reload-dir ` - 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. 54 | * `--reload-delay ` - Delay between previous and next check if application needs to be reloaded. **Default:** *0.25*. 55 | 56 | ### Reloading without watchfiles 57 | 58 | If Uvicorn _cannot_ load [watchfiles](https://pypi.org/project/watchfiles/) at runtime, it will periodically look for changes in modification times to all `*.py` files (and only `*.py` files) inside of its monitored directories. See the `--reload-dir` option. Specifying other file extensions is not supported unless watchfiles is installed. See the `--reload-include` and `--reload-exclude` options for details. 59 | 60 | ### Reloading with watchfiles 61 | 62 | 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. 63 | 64 | Using Uvicorn with watchfiles will enable the following options (which are otherwise ignored): 65 | 66 | * `--reload-include ` - 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`. 67 | * `--reload-exclude ` - 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`. 68 | 69 | !!! tip 70 | When using Uvicorn through [WSL](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux), you might 71 | have to set the `WATCHFILES_FORCE_POLLING` environment variable, for file changes to trigger a reload. 72 | See [watchfiles documentation](https://watchfiles.helpmanual.io/api/watch/) for further details. 73 | 74 | ## Production 75 | 76 | * `--workers ` - Number of worker processes. Defaults to the `$WEB_CONCURRENCY` environment variable if available, or 1. Not valid with `--reload`. 77 | * `--env-file ` - Environment configuration file for the ASGI application. **Default:** *None*. 78 | 79 | !!! note 80 | The `--reload` and `--workers` arguments are mutually exclusive. You cannot use both at the same time. 81 | 82 | ## Logging 83 | 84 | * `--log-config ` - Logging configuration file. **Options:** *`dictConfig()` formats: .json, .yaml*. Any other format will be processed with `fileConfig()`. Set the `formatters.default.use_colors` and `formatters.access.use_colors` values to override the auto-detected behavior. 85 | * 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. 86 | * `--log-level ` - Set the log level. **Options:** *'critical', 'error', 'warning', 'info', 'debug', 'trace'.* **Default:** *'info'*. 87 | * `--no-access-log` - Disable access log only, without changing log level. 88 | * `--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. 89 | 90 | ## Implementation 91 | 92 | * `--loop ` - 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'*. 93 | * `--http ` - Set the HTTP protocol implementation. The httptools implementation provides greater performance, but it not compatible with PyPy. **Options:** *'auto', 'h11', 'httptools'.* **Default:** *'auto'*. 94 | * `--ws ` - 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'*. 95 | * `--ws-max-size ` - Set the WebSockets max message size, in bytes. Only available with the `websockets` protocol. **Default:** *16777216* (16 MB). 96 | * `--ws-max-queue ` - Set the maximum length of the WebSocket incoming message queue. Only available with the `websockets` protocol. **Default:** *32*. 97 | * `--ws-ping-interval ` - Set the WebSockets ping interval, in seconds. Only available with the `websockets` protocol. **Default:** *20.0*. 98 | * `--ws-ping-timeout ` - Set the WebSockets ping timeout, in seconds. Only available with the `websockets` protocol. **Default:** *20.0*. 99 | * `--ws-per-message-deflate ` - Enable/disable WebSocket per-message-deflate compression. Only available with the `websockets` protocol. **Default:** *True*. 100 | * `--lifespan ` - Set the Lifespan protocol implementation. **Options:** *'auto', 'on', 'off'.* **Default:** *'auto'*. 101 | * `--h11-max-incomplete-event-size ` - Set the maximum number of bytes to buffer of an incomplete event. Only available for `h11` HTTP protocol implementation. **Default:** *16384* (16 KB). 102 | 103 | ## Application Interface 104 | 105 | * `--interface ` - Select ASGI3, ASGI2, or WSGI as the application interface. 106 | Note that WSGI mode always disables WebSocket support, as it is not supported by the WSGI interface. 107 | **Options:** *'auto', 'asgi3', 'asgi2', 'wsgi'.* **Default:** *'auto'*. 108 | 109 | !!! warning 110 | Uvicorn's native WSGI implementation is deprecated, you should switch 111 | to [a2wsgi](https://github.com/abersheeran/a2wsgi) (`pip install a2wsgi`). 112 | 113 | ## HTTP 114 | 115 | * `--root-path ` - Set the ASGI `root_path` for applications submounted below a given URL path. **Default:** *""*. 116 | * `--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. 117 | * `--forwarded-allow-ips ` - 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. 118 | * `--server-header / --no-server-header` - Enable/Disable default `Server` header. **Default:** *True*. 119 | * `--date-header / --no-date-header` - Enable/Disable default `Date` header. **Default:** *True*. 120 | * `--header ` - Specify custom default HTTP response headers as a Name:Value pair. May be used multiple times. 121 | 122 | !!! note 123 | The `--no-date-header` flag doesn't have effect on the `websockets` implementation. 124 | 125 | ## HTTPS 126 | 127 | The [SSL context](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) can be configured with the following options: 128 | 129 | * `--ssl-keyfile ` - The SSL key file. 130 | * `--ssl-keyfile-password ` - The password to decrypt the ssl key. 131 | * `--ssl-certfile ` - The SSL certificate file. 132 | * `--ssl-version ` - The SSL version to use. **Default:** *ssl.PROTOCOL_TLS_SERVER*. 133 | * `--ssl-cert-reqs ` - Whether client certificate is required. **Default:** *ssl.CERT_NONE*. 134 | * `--ssl-ca-certs ` - The CA certificates file. 135 | * `--ssl-ciphers ` - The ciphers to use. **Default:** *"TLSv1"*. 136 | 137 | To understand more about the SSL context options, please refer to the [Python documentation](https://docs.python.org/3/library/ssl.html). 138 | 139 | ## Resource Limits 140 | 141 | * `--limit-concurrency ` - 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. 142 | * `--limit-max-requests ` - 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. 143 | * `--backlog ` - Maximum number of connections to hold in backlog. Relevant for heavy incoming traffic. **Default:** *2048*. 144 | 145 | ## Timeouts 146 | 147 | * `--timeout-keep-alive ` - Close Keep-Alive connections if no new data is received within this timeout. **Default:** *5*. 148 | * `--timeout-graceful-shutdown ` - Maximum number of seconds to wait for graceful shutdown. After this timeout, the server will start terminating requests. 149 | -------------------------------------------------------------------------------- /docs/uvicorn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/docs/uvicorn.png -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Uvicorn 2 | site_description: The lightning-fast ASGI server. 3 | site_url: https://www.uvicorn.org 4 | strict: true 5 | 6 | theme: 7 | name: material 8 | custom_dir: docs/overrides 9 | logo: uvicorn.png 10 | favicon: uvicorn.png 11 | palette: 12 | - scheme: "default" 13 | media: "(prefers-color-scheme: light)" 14 | toggle: 15 | icon: "material/lightbulb" 16 | name: "Switch to dark mode" 17 | - scheme: "slate" 18 | media: "(prefers-color-scheme: dark)" 19 | primary: "blue" 20 | toggle: 21 | icon: "material/lightbulb-outline" 22 | name: "Switch to light mode" 23 | features: 24 | - search.suggest 25 | - search.highlight 26 | - content.tabs.link 27 | - content.code.annotate 28 | - content.code.copy # https://squidfunk.github.io/mkdocs-material/upgrade/?h=content+copy#contentcodecopy 29 | - navigation.path 30 | - navigation.indexes 31 | - navigation.sections # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation 32 | - navigation.top # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#back-to-top-button 33 | - navigation.tracking 34 | - navigation.footer # https://squidfunk.github.io/mkdocs-material/upgrade/?h=content+copy#navigationfooter 35 | - toc.follow # https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/#anchor-following 36 | - announce.dismiss # https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-header/#mark-as-read 37 | 38 | repo_name: encode/uvicorn 39 | repo_url: https://github.com/encode/uvicorn 40 | edit_uri: edit/master/docs/ 41 | 42 | # https://www.mkdocs.org/user-guide/configuration/#validation 43 | validation: 44 | omitted_files: warn 45 | absolute_links: warn 46 | unrecognized_links: warn 47 | 48 | nav: 49 | - Introduction: index.md 50 | - Settings: settings.md 51 | - Deployment: 52 | - Deployment: deployment/index.md 53 | - Docker: deployment/docker.md 54 | - Server Behavior: server-behavior.md 55 | - Release Notes: release-notes.md 56 | - Contributing: contributing.md 57 | - Sponsorship: sponsorship.md 58 | 59 | extra: 60 | analytics: 61 | provider: google 62 | property: G-KTS6TXPD85 63 | 64 | markdown_extensions: 65 | - attr_list 66 | - admonition 67 | - codehilite: 68 | css_class: highlight 69 | - toc: 70 | permalink: true 71 | - pymdownx.details 72 | - pymdownx.inlinehilite 73 | - pymdownx.snippets 74 | - pymdownx.superfences 75 | - pymdownx.emoji: 76 | emoji_index: !!python/name:material.extensions.emoji.twemoji 77 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 78 | - pymdownx.tasklist: 79 | custom_checkbox: true 80 | 81 | hooks: 82 | - docs/plugins/main.py 83 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "uvicorn" 7 | dynamic = ["version"] 8 | description = "The lightning-fast ASGI server." 9 | readme = "README.md" 10 | license = "BSD-3-Clause" 11 | requires-python = ">=3.9" 12 | authors = [ 13 | { name = "Tom Christie", email = "tom@tomchristie.com" }, 14 | { name = "Marcelo Trylesinski", email = "marcelotryle@gmail.com" }, 15 | ] 16 | classifiers = [ 17 | "Development Status :: 4 - Beta", 18 | "Environment :: Web Environment", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: BSD License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | "Programming Language :: Python :: Implementation :: CPython", 29 | "Programming Language :: Python :: Implementation :: PyPy", 30 | "Topic :: Internet :: WWW/HTTP", 31 | ] 32 | dependencies = [ 33 | "click>=7.0", 34 | "h11>=0.8", 35 | "typing_extensions>=4.0; python_version < '3.11'", 36 | ] 37 | 38 | [project.optional-dependencies] 39 | standard = [ 40 | "colorama>=0.4; sys_platform == 'win32'", 41 | "httptools>=0.6.3", 42 | "python-dotenv>=0.13", 43 | "PyYAML>=5.1", 44 | "uvloop>=0.15.1; sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')", 45 | "watchfiles>=0.13", 46 | "websockets>=10.4", 47 | ] 48 | 49 | [project.scripts] 50 | uvicorn = "uvicorn.main:main" 51 | 52 | [project.urls] 53 | Changelog = "https://www.uvicorn.org/release-notes" 54 | Funding = "https://github.com/sponsors/encode" 55 | Homepage = "https://www.uvicorn.org/" 56 | Source = "https://github.com/encode/uvicorn" 57 | 58 | [tool.hatch.version] 59 | path = "uvicorn/__init__.py" 60 | 61 | [tool.hatch.build.targets.sdist] 62 | include = ["/uvicorn", "/tests", "/requirements.txt"] 63 | 64 | [tool.ruff] 65 | line-length = 120 66 | 67 | [tool.ruff.lint] 68 | select = ["E", "F", "I", "FA", "UP"] 69 | ignore = ["B904", "B028", "UP031"] 70 | 71 | [tool.ruff.lint.isort] 72 | combine-as-imports = true 73 | 74 | [tool.mypy] 75 | warn_unused_ignores = true 76 | warn_redundant_casts = true 77 | show_error_codes = true 78 | disallow_untyped_defs = true 79 | ignore_missing_imports = true 80 | follow_imports = "silent" 81 | 82 | [[tool.mypy.overrides]] 83 | module = "tests.*" 84 | disallow_untyped_defs = false 85 | check_untyped_defs = true 86 | 87 | [tool.pytest.ini_options] 88 | addopts = "-rxXs --strict-config --strict-markers -n 8" 89 | xfail_strict = true 90 | filterwarnings = [ 91 | "error", 92 | "ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning", 93 | "ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning", 94 | "ignore: remove second argument of ws_handler:DeprecationWarning:websockets", 95 | ] 96 | 97 | [tool.coverage.run] 98 | parallel = true 99 | source_pkgs = ["uvicorn", "tests"] 100 | plugins = ["coverage_conditional_plugin"] 101 | omit = ["uvicorn/workers.py", "uvicorn/__main__.py"] 102 | 103 | [tool.coverage.report] 104 | precision = 2 105 | fail_under = 100 106 | show_missing = true 107 | skip_covered = true 108 | exclude_lines = [ 109 | "pragma: no cover", 110 | "pragma: nocover", 111 | "pragma: full coverage", 112 | "if TYPE_CHECKING:", 113 | "if typing.TYPE_CHECKING:", 114 | "raise NotImplementedError", 115 | ] 116 | 117 | [tool.coverage.coverage_conditional_plugin.omit] 118 | "sys_platform == 'win32'" = [ 119 | "uvicorn/loops/uvloop.py", 120 | "uvicorn/supervisors/multiprocess.py", 121 | "tests/supervisors/test_multiprocess.py", 122 | ] 123 | "sys_platform != 'win32'" = ["uvicorn/loops/asyncio.py"] 124 | 125 | [tool.coverage.coverage_conditional_plugin.rules] 126 | py-win32 = "sys_platform == 'win32'" 127 | py-not-win32 = "sys_platform != 'win32'" 128 | py-linux = "sys_platform == 'linux'" 129 | py-not-linux = "sys_platform != 'linux'" 130 | py-darwin = "sys_platform == 'darwin'" 131 | py-gte-39 = "sys_version_info >= (3, 9)" 132 | py-lt-39 = "sys_version_info < (3, 9)" 133 | py-gte-310 = "sys_version_info >= (3, 10)" 134 | py-lt-310 = "sys_version_info < (3, 10)" 135 | py-gte-311 = "sys_version_info >= (3, 11)" 136 | py-lt-311 = "sys_version_info < (3, 11)" 137 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e .[standard] 2 | 3 | # Core dependencies 4 | h11==0.16.0 5 | 6 | # Explicit optionals 7 | a2wsgi==1.10.8 8 | wsproto==1.2.0 9 | websockets==13.1 10 | 11 | # Packaging 12 | build==1.2.2.post1 13 | twine==6.1.0 14 | 15 | # Testing 16 | ruff==0.11.9 17 | pytest==8.3.5 18 | pytest-mock==3.14.0 19 | pytest-xdist[psutil]==3.6.1 20 | mypy==1.15.0 21 | types-click==7.1.8 22 | types-pyyaml==6.0.12.20250402 23 | trustme==1.2.1 24 | cryptography==44.0.3 25 | coverage==7.8.0 26 | coverage-conditional-plugin==0.9.0 27 | coverage-enable-subprocess==1.0 28 | httpx==0.28.1 29 | 30 | # Documentation 31 | mkdocs==1.6.1 32 | mkdocs-material==9.6.13 33 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ -d 'venv' ] ; then 4 | PREFIX="venv/bin/" 5 | else 6 | PREFIX="" 7 | fi 8 | 9 | set -x 10 | 11 | ${PREFIX}python -m build 12 | ${PREFIX}twine check dist/* 13 | ${PREFIX}mkdocs build 14 | -------------------------------------------------------------------------------- /scripts/check: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | if [ -d 'venv' ] ; then 5 | export PREFIX="venv/bin/" 6 | export PATH=${PREFIX}:${PATH} 7 | fi 8 | export SOURCE_FILES="uvicorn tests" 9 | 10 | set -x 11 | 12 | ./scripts/sync-version 13 | ${PREFIX}ruff format --check --diff $SOURCE_FILES 14 | ${PREFIX}mypy $SOURCE_FILES 15 | ${PREFIX}ruff check $SOURCE_FILES 16 | -------------------------------------------------------------------------------- /scripts/coverage: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | if [ -d 'venv' ]; then 5 | export PREFIX="venv/bin/" 6 | fi 7 | export SOURCE_FILES="uvicorn tests" 8 | 9 | set -x 10 | 11 | ${PREFIX}coverage combine 12 | ${PREFIX}coverage report 13 | -------------------------------------------------------------------------------- /scripts/docs: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | PREFIX="" 4 | if [ -d "venv" ] ; then 5 | PREFIX="venv/bin/" 6 | fi 7 | 8 | set -x 9 | 10 | ${PREFIX}mkdocs "$@" 11 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Use the Python executable provided from the `-p` option, or a default. 4 | [ "$1" = "-p" ] && PYTHON=$2 || PYTHON="python3" 5 | 6 | REQUIREMENTS="requirements.txt" 7 | VENV="venv" 8 | 9 | set -x 10 | 11 | if [ -z "$GITHUB_ACTIONS" ]; then 12 | "$PYTHON" -m venv "$VENV" 13 | PIP="$VENV/bin/pip" 14 | else 15 | PIP="$PYTHON -m pip" 16 | fi 17 | 18 | ${PIP} install -U pip 19 | ${PIP} install -r "$REQUIREMENTS" 20 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | if [ -d 'venv' ] ; then 5 | export PREFIX="venv/bin/" 6 | export PATH=${PREFIX}:${PATH} 7 | fi 8 | export SOURCE_FILES="uvicorn tests" 9 | 10 | set -x 11 | 12 | ${PREFIX}ruff format $SOURCE_FILES 13 | ${PREFIX}ruff check --fix $SOURCE_FILES 14 | -------------------------------------------------------------------------------- /scripts/publish: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | VERSION_FILE="uvicorn/__init__.py" 4 | 5 | if [ -d 'venv' ] ; then 6 | PREFIX="venv/bin/" 7 | else 8 | PREFIX="" 9 | fi 10 | 11 | if [ ! -z "$GITHUB_ACTIONS" ]; then 12 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 13 | git config --local user.name "GitHub Action" 14 | 15 | VERSION=`grep __version__ ${VERSION_FILE} | grep -o '[0-9][^"]*'` 16 | 17 | if [ "refs/tags/${VERSION}" != "${GITHUB_REF}" ] ; then 18 | echo "GitHub Ref '${GITHUB_REF}' did not match package version '${VERSION}'" 19 | exit 1 20 | fi 21 | fi 22 | 23 | set -x 24 | 25 | ${PREFIX}twine upload dist/* 26 | ${PREFIX}mkdocs gh-deploy --force 27 | -------------------------------------------------------------------------------- /scripts/sync-version: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | SEMVER_REGEX="([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?" 4 | CHANGELOG_VERSION=$(grep -o -E $SEMVER_REGEX docs/release-notes.md | head -1) 5 | VERSION=$(grep -o -E $SEMVER_REGEX uvicorn/__init__.py | head -1) 6 | if [ "$CHANGELOG_VERSION" != "$VERSION" ]; then 7 | echo "Version in changelog does not match version in uvicorn/__init__.py!" 8 | exit 1 9 | fi 10 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PREFIX="" 4 | if [ -d 'venv' ]; then 5 | export PREFIX="venv/bin/" 6 | fi 7 | 8 | set -ex 9 | 10 | if [ -z $GITHUB_ACTIONS ]; then 11 | scripts/check 12 | fi 13 | 14 | export COVERAGE_PROCESS_START=$(pwd)/pyproject.toml 15 | 16 | ${PREFIX}coverage run --debug config -m pytest "$@" 17 | 18 | if [ -z $GITHUB_ACTIONS ]; then 19 | scripts/coverage 20 | fi 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import importlib.util 5 | import os 6 | import socket 7 | import ssl 8 | from copy import deepcopy 9 | from hashlib import md5 10 | from pathlib import Path 11 | from tempfile import TemporaryDirectory 12 | from typing import Any 13 | from uuid import uuid4 14 | 15 | import pytest 16 | 17 | try: 18 | import trustme 19 | from cryptography.hazmat.backends import default_backend 20 | from cryptography.hazmat.primitives import serialization 21 | 22 | HAVE_TRUSTME = True 23 | except ImportError: # pragma: no cover 24 | HAVE_TRUSTME = False 25 | 26 | from uvicorn.config import LOGGING_CONFIG 27 | from uvicorn.importer import import_from_string 28 | 29 | # Note: We explicitly turn the propagate on just for tests, because pytest 30 | # caplog not able to capture no-propagate loggers. 31 | # 32 | # And the caplog_for_logger helper also not work on test config cases, because 33 | # when create Config object, Config.configure_logging will remove caplog.handler. 34 | # 35 | # The simple solution is set propagate=True before execute tests. 36 | # 37 | # See also: https://github.com/pytest-dev/pytest/issues/3697 38 | LOGGING_CONFIG["loggers"]["uvicorn"]["propagate"] = True 39 | 40 | 41 | @pytest.fixture 42 | def tls_certificate_authority() -> trustme.CA: 43 | if not HAVE_TRUSTME: 44 | pytest.skip("trustme not installed") # pragma: no cover 45 | return trustme.CA() 46 | 47 | 48 | @pytest.fixture 49 | def tls_certificate(tls_certificate_authority: trustme.CA) -> trustme.LeafCert: 50 | return tls_certificate_authority.issue_cert( 51 | "localhost", 52 | "127.0.0.1", 53 | "::1", 54 | ) 55 | 56 | 57 | @pytest.fixture 58 | def tls_ca_certificate_pem_path(tls_certificate_authority: trustme.CA): 59 | with tls_certificate_authority.cert_pem.tempfile() as ca_cert_pem: 60 | yield ca_cert_pem 61 | 62 | 63 | @pytest.fixture 64 | def tls_ca_certificate_private_key_path(tls_certificate_authority: trustme.CA): 65 | with tls_certificate_authority.private_key_pem.tempfile() as private_key: 66 | yield private_key 67 | 68 | 69 | @pytest.fixture 70 | def tls_certificate_private_key_encrypted_path(tls_certificate): 71 | private_key = serialization.load_pem_private_key( 72 | tls_certificate.private_key_pem.bytes(), 73 | password=None, 74 | backend=default_backend(), 75 | ) 76 | encrypted_key = private_key.private_bytes( 77 | serialization.Encoding.PEM, 78 | serialization.PrivateFormat.TraditionalOpenSSL, 79 | serialization.BestAvailableEncryption(b"uvicorn password for the win"), 80 | ) 81 | with trustme.Blob(encrypted_key).tempfile() as private_encrypted_key: 82 | yield private_encrypted_key 83 | 84 | 85 | @pytest.fixture 86 | def tls_certificate_private_key_path(tls_certificate: trustme.CA): 87 | with tls_certificate.private_key_pem.tempfile() as private_key: 88 | yield private_key 89 | 90 | 91 | @pytest.fixture 92 | def tls_certificate_key_and_chain_path(tls_certificate: trustme.LeafCert): 93 | with tls_certificate.private_key_and_cert_chain_pem.tempfile() as cert_pem: 94 | yield cert_pem 95 | 96 | 97 | @pytest.fixture 98 | def tls_certificate_server_cert_path(tls_certificate: trustme.LeafCert): 99 | with tls_certificate.cert_chain_pems[0].tempfile() as cert_pem: 100 | yield cert_pem 101 | 102 | 103 | @pytest.fixture 104 | def tls_ca_ssl_context(tls_certificate_authority: trustme.CA) -> ssl.SSLContext: 105 | ssl_ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) 106 | tls_certificate_authority.configure_trust(ssl_ctx) 107 | return ssl_ctx 108 | 109 | 110 | @pytest.fixture(scope="package") 111 | def reload_directory_structure(tmp_path_factory: pytest.TempPathFactory): 112 | """ 113 | This fixture creates a directory structure to enable reload parameter tests 114 | 115 | The fixture has the following structure: 116 | root 117 | ├── [app, app_first, app_second, app_third] 118 | │   ├── css 119 | │   │   └── main.css 120 | │   ├── js 121 | │   │   └── main.js 122 | │   ├── src 123 | │   │   └── main.py 124 | │   └── sub 125 | │   └── sub.py 126 | ├── ext 127 | │   └── ext.jpg 128 | ├── .dotted 129 | ├── .dotted_dir 130 | │   └── file.txt 131 | └── main.py 132 | """ 133 | root = tmp_path_factory.mktemp("reload_directory") 134 | apps = ["app", "app_first", "app_second", "app_third"] 135 | 136 | root_file = root / "main.py" 137 | root_file.touch() 138 | 139 | dotted_file = root / ".dotted" 140 | dotted_file.touch() 141 | 142 | dotted_dir = root / ".dotted_dir" 143 | dotted_dir.mkdir() 144 | dotted_dir_file = dotted_dir / "file.txt" 145 | dotted_dir_file.touch() 146 | 147 | for app in apps: 148 | app_path = root / app 149 | app_path.mkdir() 150 | dir_files = [ 151 | ("src", ["main.py"]), 152 | ("js", ["main.js"]), 153 | ("css", ["main.css"]), 154 | ("sub", ["sub.py"]), 155 | ] 156 | for directory, files in dir_files: 157 | directory_path = app_path / directory 158 | directory_path.mkdir() 159 | for file in files: 160 | file_path = directory_path / file 161 | file_path.touch() 162 | ext_dir = root / "ext" 163 | ext_dir.mkdir() 164 | ext_file = ext_dir / "ext.jpg" 165 | ext_file.touch() 166 | 167 | yield root 168 | 169 | 170 | @pytest.fixture 171 | def anyio_backend() -> str: 172 | return "asyncio" 173 | 174 | 175 | @pytest.fixture(scope="function") 176 | def logging_config() -> dict[str, Any]: 177 | return deepcopy(LOGGING_CONFIG) 178 | 179 | 180 | @pytest.fixture 181 | def short_socket_name(tmp_path, tmp_path_factory): # pragma: py-win32 182 | max_sock_len = 100 183 | socket_filename = "my.sock" 184 | identifier = f"{uuid4()}-" 185 | identifier_len = len(identifier.encode()) 186 | tmp_dir = Path("/tmp").resolve() 187 | os_tmp_dir = Path(os.getenv("TMPDIR", "/tmp")).resolve() 188 | basetemp = Path( 189 | str(tmp_path_factory.getbasetemp()), 190 | ).resolve() 191 | hash_basetemp = md5( 192 | str(basetemp).encode(), 193 | ).hexdigest() 194 | 195 | def make_tmp_dir(base_dir): 196 | return TemporaryDirectory( 197 | dir=str(base_dir), 198 | prefix="p-", 199 | suffix=f"-{hash_basetemp}", 200 | ) 201 | 202 | paths = basetemp, os_tmp_dir, tmp_dir 203 | for _num, tmp_dir_path in enumerate(paths, 1): 204 | with make_tmp_dir(tmp_dir_path) as tmpd: 205 | tmpd = Path(tmpd).resolve() 206 | sock_path = str(tmpd / socket_filename) 207 | sock_path_len = len(sock_path.encode()) 208 | if sock_path_len <= max_sock_len: 209 | if max_sock_len - sock_path_len >= identifier_len: # pragma: no cover 210 | sock_path = str(tmpd / "".join((identifier, socket_filename))) 211 | yield sock_path 212 | return 213 | 214 | 215 | def _unused_port(socket_type: int) -> int: 216 | """Find an unused localhost port from 1024-65535 and return it.""" 217 | with contextlib.closing(socket.socket(type=socket_type)) as sock: 218 | sock.bind(("127.0.0.1", 0)) 219 | return sock.getsockname()[1] 220 | 221 | 222 | # This was copied from pytest-asyncio. 223 | # Ref.: https://github.com/pytest-dev/pytest-asyncio/blob/25d9592286682bc6dbfbf291028ff7a9594cf283/pytest_asyncio/plugin.py#L525-L527 # noqa: E501 224 | @pytest.fixture 225 | def unused_tcp_port() -> int: 226 | return _unused_port(socket.SOCK_STREAM) 227 | 228 | 229 | @pytest.fixture( 230 | params=[ 231 | pytest.param( 232 | "uvicorn.protocols.websockets.wsproto_impl:WSProtocol", 233 | marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."), 234 | id="wsproto", 235 | ), 236 | pytest.param( 237 | "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", 238 | id="websockets", 239 | ), 240 | ] 241 | ) 242 | def ws_protocol_cls(request: pytest.FixtureRequest): 243 | return import_from_string(request.param) 244 | 245 | 246 | @pytest.fixture( 247 | params=[ 248 | pytest.param( 249 | "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol", 250 | marks=pytest.mark.skipif( 251 | not importlib.util.find_spec("httptools"), 252 | reason="httptools not installed.", 253 | ), 254 | id="httptools", 255 | ), 256 | pytest.param("uvicorn.protocols.http.h11_impl:H11Protocol", id="h11"), 257 | ] 258 | ) 259 | def http_protocol_cls(request: pytest.FixtureRequest): 260 | return import_from_string(request.param) 261 | -------------------------------------------------------------------------------- /tests/importer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/tests/importer/__init__.py -------------------------------------------------------------------------------- /tests/importer/circular_import_a.py: -------------------------------------------------------------------------------- 1 | # Used by test_importer.py 2 | from .circular_import_b import foo # noqa 3 | 4 | bar = 123 # pragma: no cover 5 | -------------------------------------------------------------------------------- /tests/importer/circular_import_b.py: -------------------------------------------------------------------------------- 1 | # Used by test_importer.py 2 | from .circular_import_a import bar # noqa 3 | 4 | foo = 123 # pragma: no cover 5 | -------------------------------------------------------------------------------- /tests/importer/raise_import_error.py: -------------------------------------------------------------------------------- 1 | # Used by test_importer.py 2 | 3 | myattr = 123 4 | 5 | import does_not_exist # noqa 6 | -------------------------------------------------------------------------------- /tests/importer/test_importer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from uvicorn.importer import ImportFromStringError, import_from_string 4 | 5 | 6 | def test_invalid_format() -> None: 7 | with pytest.raises(ImportFromStringError) as exc_info: 8 | import_from_string("example:") 9 | expected = 'Import string "example:" must be in format ":".' 10 | assert expected in str(exc_info.value) 11 | 12 | 13 | def test_invalid_module() -> None: 14 | with pytest.raises(ImportFromStringError) as exc_info: 15 | import_from_string("module_does_not_exist:myattr") 16 | expected = 'Could not import module "module_does_not_exist".' 17 | assert expected in str(exc_info.value) 18 | 19 | 20 | def test_invalid_attr() -> None: 21 | with pytest.raises(ImportFromStringError) as exc_info: 22 | import_from_string("tempfile:attr_does_not_exist") 23 | expected = 'Attribute "attr_does_not_exist" not found in module "tempfile".' 24 | assert expected in str(exc_info.value) 25 | 26 | 27 | def test_internal_import_error() -> None: 28 | with pytest.raises(ImportError): 29 | import_from_string("tests.importer.raise_import_error:myattr") 30 | 31 | 32 | def test_valid_import() -> None: 33 | instance = import_from_string("tempfile:TemporaryFile") 34 | from tempfile import TemporaryFile 35 | 36 | assert instance == TemporaryFile 37 | 38 | 39 | def test_no_import_needed() -> None: 40 | from tempfile import TemporaryFile 41 | 42 | instance = import_from_string(TemporaryFile) 43 | assert instance == TemporaryFile 44 | 45 | 46 | def test_circular_import_error() -> None: 47 | with pytest.raises(ImportError) as exc_info: 48 | import_from_string("tests.importer.circular_import_a:bar") 49 | expected = ( 50 | "cannot import name 'bar' from partially initialized module " 51 | "'tests.importer.circular_import_a' (most likely due to a circular import)" 52 | ) 53 | assert expected in str(exc_info.value) 54 | -------------------------------------------------------------------------------- /tests/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/tests/middleware/__init__.py -------------------------------------------------------------------------------- /tests/middleware/test_logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import logging 5 | import socket 6 | import sys 7 | from collections.abc import Iterator 8 | from typing import TYPE_CHECKING 9 | 10 | import httpx 11 | import pytest 12 | import websockets 13 | import websockets.client 14 | 15 | from tests.utils import run_server 16 | from uvicorn import Config 17 | from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope 18 | 19 | if TYPE_CHECKING: 20 | import sys 21 | 22 | from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol 23 | from uvicorn.protocols.websockets.wsproto_impl import WSProtocol as _WSProtocol 24 | 25 | if sys.version_info >= (3, 10): # pragma: no cover 26 | from typing import TypeAlias 27 | else: # pragma: no cover 28 | from typing_extensions import TypeAlias 29 | 30 | WSProtocol: TypeAlias = "type[WebSocketProtocol | _WSProtocol]" 31 | 32 | pytestmark = pytest.mark.anyio 33 | 34 | 35 | @contextlib.contextmanager 36 | def caplog_for_logger(caplog: pytest.LogCaptureFixture, logger_name: str) -> Iterator[pytest.LogCaptureFixture]: 37 | logger = logging.getLogger(logger_name) 38 | logger.propagate, old_propagate = False, logger.propagate 39 | logger.addHandler(caplog.handler) 40 | try: 41 | yield caplog 42 | finally: 43 | logger.removeHandler(caplog.handler) 44 | logger.propagate = old_propagate 45 | 46 | 47 | async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): 48 | assert scope["type"] == "http" 49 | await send({"type": "http.response.start", "status": 204, "headers": []}) 50 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 51 | 52 | 53 | async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int): 54 | config = Config( 55 | app=app, 56 | log_level="trace", 57 | log_config=logging_config, 58 | lifespan="auto", 59 | port=unused_tcp_port, 60 | ) 61 | with caplog_for_logger(caplog, "uvicorn.asgi"): 62 | async with run_server(config): 63 | async with httpx.AsyncClient() as client: 64 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 65 | assert response.status_code == 204 66 | messages = [record.message for record in caplog.records if record.name == "uvicorn.asgi"] 67 | assert "ASGI [1] Started scope=" in messages.pop(0) 68 | assert "ASGI [1] Raised exception" in messages.pop(0) 69 | assert "ASGI [2] Started scope=" in messages.pop(0) 70 | assert "ASGI [2] Send " in messages.pop(0) 71 | assert "ASGI [2] Send " in messages.pop(0) 72 | assert "ASGI [2] Completed" in messages.pop(0) 73 | 74 | 75 | async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging_config, unused_tcp_port: int): 76 | config = Config( 77 | app=app, 78 | log_level="trace", 79 | http=http_protocol_cls, 80 | log_config=logging_config, 81 | port=unused_tcp_port, 82 | ) 83 | with caplog_for_logger(caplog, "uvicorn.error"): 84 | async with run_server(config): 85 | async with httpx.AsyncClient() as client: 86 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 87 | assert response.status_code == 204 88 | messages = [record.message for record in caplog.records if record.name == "uvicorn.error"] 89 | assert any(" - HTTP connection made" in message for message in messages) 90 | assert any(" - HTTP connection lost" in message for message in messages) 91 | 92 | 93 | async def test_trace_logging_on_ws_protocol( 94 | ws_protocol_cls: WSProtocol, 95 | caplog, 96 | logging_config, 97 | unused_tcp_port: int, 98 | ): 99 | async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): 100 | assert scope["type"] == "websocket" 101 | while True: 102 | message = await receive() 103 | if message["type"] == "websocket.connect": 104 | await send({"type": "websocket.accept"}) 105 | elif message["type"] == "websocket.disconnect": 106 | break 107 | 108 | async def open_connection(url): 109 | async with websockets.client.connect(url) as websocket: 110 | return websocket.open 111 | 112 | config = Config( 113 | app=websocket_app, 114 | log_level="trace", 115 | log_config=logging_config, 116 | ws=ws_protocol_cls, 117 | port=unused_tcp_port, 118 | ) 119 | with caplog_for_logger(caplog, "uvicorn.error"): 120 | async with run_server(config): 121 | is_open = await open_connection(f"ws://127.0.0.1:{unused_tcp_port}") 122 | assert is_open 123 | messages = [record.message for record in caplog.records if record.name == "uvicorn.error"] 124 | assert any(" - Upgrading to WebSocket" in message for message in messages) 125 | assert any(" - WebSocket connection made" in message for message in messages) 126 | assert any(" - WebSocket connection lost" in message for message in messages) 127 | 128 | 129 | @pytest.mark.parametrize("use_colors", [(True), (False), (None)]) 130 | async def test_access_logging(use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int): 131 | config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port) 132 | with caplog_for_logger(caplog, "uvicorn.access"): 133 | async with run_server(config): 134 | async with httpx.AsyncClient() as client: 135 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 136 | 137 | assert response.status_code == 204 138 | messages = [record.message for record in caplog.records if record.name == "uvicorn.access"] 139 | assert '"GET / HTTP/1.1" 204' in messages.pop() 140 | 141 | 142 | @pytest.mark.parametrize("use_colors", [(True), (False)]) 143 | async def test_default_logging( 144 | use_colors: bool, caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int 145 | ): 146 | config = Config(app=app, use_colors=use_colors, log_config=logging_config, port=unused_tcp_port) 147 | with caplog_for_logger(caplog, "uvicorn.access"): 148 | async with run_server(config): 149 | async with httpx.AsyncClient() as client: 150 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 151 | assert response.status_code == 204 152 | messages = [record.message for record in caplog.records if "uvicorn" in record.name] 153 | assert "Started server process" in messages.pop(0) 154 | assert "Waiting for application startup" in messages.pop(0) 155 | assert "ASGI 'lifespan' protocol appears unsupported" in messages.pop(0) 156 | assert "Application startup complete" in messages.pop(0) 157 | assert "Uvicorn running on http://127.0.0.1" in messages.pop(0) 158 | assert '"GET / HTTP/1.1" 204' in messages.pop(0) 159 | assert "Shutting down" in messages.pop(0) 160 | 161 | 162 | @pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system") 163 | async def test_running_log_using_uds( 164 | caplog: pytest.LogCaptureFixture, short_socket_name: str, unused_tcp_port: int 165 | ): # pragma: py-win32 166 | config = Config(app=app, uds=short_socket_name, port=unused_tcp_port) 167 | with caplog_for_logger(caplog, "uvicorn.access"): 168 | async with run_server(config): 169 | ... 170 | 171 | messages = [record.message for record in caplog.records if "uvicorn" in record.name] 172 | assert f"Uvicorn running on unix socket {short_socket_name} (Press CTRL+C to quit)" in messages 173 | 174 | 175 | @pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system") 176 | async def test_running_log_using_fd(caplog: pytest.LogCaptureFixture, unused_tcp_port: int): # pragma: py-win32 177 | with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: 178 | fd = sock.fileno() 179 | config = Config(app=app, fd=fd, port=unused_tcp_port) 180 | with caplog_for_logger(caplog, "uvicorn.access"): 181 | async with run_server(config): 182 | ... 183 | sockname = sock.getsockname() 184 | messages = [record.message for record in caplog.records if "uvicorn" in record.name] 185 | assert f"Uvicorn running on socket {sockname} (Press CTRL+C to quit)" in messages 186 | 187 | 188 | async def test_unknown_status_code(caplog: pytest.LogCaptureFixture, unused_tcp_port: int): 189 | async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable): 190 | assert scope["type"] == "http" 191 | await send({"type": "http.response.start", "status": 599, "headers": []}) 192 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 193 | 194 | config = Config(app=app, port=unused_tcp_port) 195 | with caplog_for_logger(caplog, "uvicorn.access"): 196 | async with run_server(config): 197 | async with httpx.AsyncClient() as client: 198 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 199 | 200 | assert response.status_code == 599 201 | messages = [record.message for record in caplog.records if record.name == "uvicorn.access"] 202 | assert '"GET / HTTP/1.1" 599' in messages.pop() 203 | 204 | 205 | async def test_server_start_with_port_zero(caplog: pytest.LogCaptureFixture): 206 | config = Config(app=app, port=0) 207 | async with run_server(config) as _server: 208 | server = _server.servers[0] 209 | sock = server.sockets[0] 210 | host, port = sock.getsockname() 211 | messages = [record.message for record in caplog.records if "uvicorn" in record.name] 212 | assert f"Uvicorn running on http://{host}:{port} (Press CTRL+C to quit)" in messages 213 | -------------------------------------------------------------------------------- /tests/middleware/test_message_logger.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | 4 | from tests.middleware.test_logging import caplog_for_logger 5 | from uvicorn.logging import TRACE_LOG_LEVEL 6 | from uvicorn.middleware.message_logger import MessageLoggerMiddleware 7 | 8 | 9 | @pytest.mark.anyio 10 | async def test_message_logger(caplog): 11 | async def app(scope, receive, send): 12 | await receive() 13 | await send({"type": "http.response.start", "status": 200, "headers": []}) 14 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 15 | 16 | with caplog_for_logger(caplog, "uvicorn.asgi"): 17 | caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi") 18 | caplog.set_level(TRACE_LOG_LEVEL) 19 | 20 | transport = httpx.ASGITransport(MessageLoggerMiddleware(app)) # type: ignore 21 | async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: 22 | response = await client.get("/") 23 | assert response.status_code == 200 24 | messages = [record.msg % record.args for record in caplog.records] 25 | assert sum(["ASGI [1] Started" in message for message in messages]) == 1 26 | assert sum(["ASGI [1] Send" in message for message in messages]) == 2 27 | assert sum(["ASGI [1] Receive" in message for message in messages]) == 1 28 | assert sum(["ASGI [1] Completed" in message for message in messages]) == 1 29 | assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 0 30 | 31 | 32 | @pytest.mark.anyio 33 | async def test_message_logger_exc(caplog): 34 | async def app(scope, receive, send): 35 | raise RuntimeError() 36 | 37 | with caplog_for_logger(caplog, "uvicorn.asgi"): 38 | caplog.set_level(TRACE_LOG_LEVEL, logger="uvicorn.asgi") 39 | caplog.set_level(TRACE_LOG_LEVEL) 40 | transport = httpx.ASGITransport(MessageLoggerMiddleware(app)) # type: ignore 41 | async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: 42 | with pytest.raises(RuntimeError): 43 | await client.get("/") 44 | messages = [record.msg % record.args for record in caplog.records] 45 | assert sum(["ASGI [1] Started" in message for message in messages]) == 1 46 | assert sum(["ASGI [1] Send" in message for message in messages]) == 0 47 | assert sum(["ASGI [1] Receive" in message for message in messages]) == 0 48 | assert sum(["ASGI [1] Completed" in message for message in messages]) == 0 49 | assert sum(["ASGI [1] Raised exception" in message for message in messages]) == 1 50 | -------------------------------------------------------------------------------- /tests/middleware/test_wsgi.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import sys 5 | from collections.abc import AsyncGenerator 6 | from typing import Callable 7 | 8 | import a2wsgi 9 | import httpx 10 | import pytest 11 | 12 | from uvicorn._types import Environ, HTTPRequestEvent, HTTPScope, StartResponse 13 | from uvicorn.middleware import wsgi 14 | 15 | 16 | def hello_world(environ: Environ, start_response: StartResponse) -> list[bytes]: 17 | status = "200 OK" 18 | output = b"Hello World!\n" 19 | headers = [ 20 | ("Content-Type", "text/plain; charset=utf-8"), 21 | ("Content-Length", str(len(output))), 22 | ] 23 | start_response(status, headers, None) 24 | return [output] 25 | 26 | 27 | def echo_body(environ: Environ, start_response: StartResponse) -> list[bytes]: 28 | status = "200 OK" 29 | output = environ["wsgi.input"].read() 30 | headers = [ 31 | ("Content-Type", "text/plain; charset=utf-8"), 32 | ("Content-Length", str(len(output))), 33 | ] 34 | start_response(status, headers, None) 35 | return [output] 36 | 37 | 38 | def raise_exception(environ: Environ, start_response: StartResponse) -> list[bytes]: 39 | raise RuntimeError("Something went wrong") 40 | 41 | 42 | def return_exc_info(environ: Environ, start_response: StartResponse) -> list[bytes]: 43 | try: 44 | raise RuntimeError("Something went wrong") 45 | except RuntimeError: 46 | status = "500 Internal Server Error" 47 | output = b"Internal Server Error" 48 | headers = [ 49 | ("Content-Type", "text/plain; charset=utf-8"), 50 | ("Content-Length", str(len(output))), 51 | ] 52 | start_response(status, headers, sys.exc_info()) # type: ignore[arg-type] 53 | return [output] 54 | 55 | 56 | @pytest.fixture(params=[wsgi._WSGIMiddleware, a2wsgi.WSGIMiddleware]) 57 | def wsgi_middleware(request: pytest.FixtureRequest) -> Callable: 58 | return request.param 59 | 60 | 61 | @pytest.mark.anyio 62 | async def test_wsgi_get(wsgi_middleware: Callable) -> None: 63 | transport = httpx.ASGITransport(wsgi_middleware(hello_world)) 64 | async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: 65 | response = await client.get("/") 66 | assert response.status_code == 200 67 | assert response.text == "Hello World!\n" 68 | 69 | 70 | @pytest.mark.anyio 71 | async def test_wsgi_post(wsgi_middleware: Callable) -> None: 72 | transport = httpx.ASGITransport(wsgi_middleware(echo_body)) 73 | async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: 74 | response = await client.post("/", json={"example": 123}) 75 | assert response.status_code == 200 76 | assert response.text == '{"example":123}' 77 | 78 | 79 | @pytest.mark.anyio 80 | async def test_wsgi_put_more_body(wsgi_middleware: Callable) -> None: 81 | async def generate_body() -> AsyncGenerator[bytes, None]: 82 | for _ in range(1024): 83 | yield b"123456789abcdef\n" * 64 84 | 85 | transport = httpx.ASGITransport(wsgi_middleware(echo_body)) 86 | async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: 87 | response = await client.put("/", content=generate_body()) 88 | assert response.status_code == 200 89 | assert response.text == "123456789abcdef\n" * 64 * 1024 90 | 91 | 92 | @pytest.mark.anyio 93 | async def test_wsgi_exception(wsgi_middleware: Callable) -> None: 94 | # Note that we're testing the WSGI app directly here. 95 | # The HTTP protocol implementations would catch this error and return 500. 96 | transport = httpx.ASGITransport(wsgi_middleware(raise_exception)) 97 | async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: 98 | with pytest.raises(RuntimeError): 99 | await client.get("/") 100 | 101 | 102 | @pytest.mark.anyio 103 | async def test_wsgi_exc_info(wsgi_middleware: Callable) -> None: 104 | app = wsgi_middleware(return_exc_info) 105 | transport = httpx.ASGITransport( 106 | app=app, 107 | raise_app_exceptions=False, 108 | ) 109 | async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: 110 | response = await client.get("/") 111 | assert response.status_code == 500 112 | assert response.text == "Internal Server Error" 113 | 114 | 115 | def test_build_environ_encoding() -> None: 116 | scope: HTTPScope = { 117 | "asgi": {"version": "3.0", "spec_version": "2.0"}, 118 | "scheme": "http", 119 | "raw_path": b"/\xe6\x96\x87%2Fall", 120 | "type": "http", 121 | "http_version": "1.1", 122 | "method": "GET", 123 | "path": "/文/all", 124 | "root_path": "/文", 125 | "client": None, 126 | "server": None, 127 | "query_string": b"a=123&b=456", 128 | "headers": [(b"key", b"value1"), (b"key", b"value2")], 129 | "extensions": {}, 130 | } 131 | message: HTTPRequestEvent = { 132 | "type": "http.request", 133 | "body": b"", 134 | "more_body": False, 135 | } 136 | environ = wsgi.build_environ(scope, message, io.BytesIO(b"")) 137 | assert environ["SCRIPT_NAME"] == "/文".encode().decode("latin-1") 138 | assert environ["PATH_INFO"] == b"/all".decode("latin-1") 139 | assert environ["HTTP_KEY"] == "value1,value2" 140 | -------------------------------------------------------------------------------- /tests/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/tests/protocols/__init__.py -------------------------------------------------------------------------------- /tests/protocols/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import socket 4 | from asyncio import Transport 5 | from typing import Any 6 | 7 | import pytest 8 | 9 | from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_remote_addr 10 | 11 | 12 | class MockSocket: 13 | def __init__(self, family, peername=None, sockname=None): 14 | self.peername = peername 15 | self.sockname = sockname 16 | self.family = family 17 | 18 | def getpeername(self): 19 | return self.peername 20 | 21 | def getsockname(self): 22 | return self.sockname 23 | 24 | 25 | class MockTransport(Transport): 26 | def __init__(self, info: dict[str, Any]) -> None: 27 | self.info = info 28 | 29 | def get_extra_info(self, name: str, default: Any = None) -> Any: 30 | return self.info.get(name) 31 | 32 | 33 | def test_get_local_addr_with_socket(): 34 | transport = MockTransport({"socket": MockSocket(family=socket.AF_IPX)}) 35 | assert get_local_addr(transport) is None 36 | 37 | transport = MockTransport({"socket": MockSocket(family=socket.AF_INET6, sockname=("::1", 123))}) 38 | assert get_local_addr(transport) == ("::1", 123) 39 | 40 | transport = MockTransport({"socket": MockSocket(family=socket.AF_INET, sockname=("123.45.6.7", 123))}) 41 | assert get_local_addr(transport) == ("123.45.6.7", 123) 42 | 43 | if hasattr(socket, "AF_UNIX"): # pragma: no cover 44 | transport = MockTransport({"socket": MockSocket(family=socket.AF_UNIX, sockname=("127.0.0.1", 8000))}) 45 | assert get_local_addr(transport) == ("127.0.0.1", 8000) 46 | 47 | 48 | def test_get_remote_addr_with_socket(): 49 | transport = MockTransport({"socket": MockSocket(family=socket.AF_IPX)}) 50 | assert get_remote_addr(transport) is None 51 | 52 | transport = MockTransport({"socket": MockSocket(family=socket.AF_INET6, peername=("::1", 123))}) 53 | assert get_remote_addr(transport) == ("::1", 123) 54 | 55 | transport = MockTransport({"socket": MockSocket(family=socket.AF_INET, peername=("123.45.6.7", 123))}) 56 | assert get_remote_addr(transport) == ("123.45.6.7", 123) 57 | 58 | if hasattr(socket, "AF_UNIX"): # pragma: no cover 59 | transport = MockTransport({"socket": MockSocket(family=socket.AF_UNIX, peername=("127.0.0.1", 8000))}) 60 | assert get_remote_addr(transport) == ("127.0.0.1", 8000) 61 | 62 | 63 | def test_get_local_addr(): 64 | transport = MockTransport({"sockname": "path/to/unix-domain-socket"}) 65 | assert get_local_addr(transport) is None 66 | 67 | transport = MockTransport({"sockname": ("123.45.6.7", 123)}) 68 | assert get_local_addr(transport) == ("123.45.6.7", 123) 69 | 70 | 71 | def test_get_remote_addr(): 72 | transport = MockTransport({"peername": None}) 73 | assert get_remote_addr(transport) is None 74 | 75 | transport = MockTransport({"peername": ("123.45.6.7", 123)}) 76 | assert get_remote_addr(transport) == ("123.45.6.7", 123) 77 | 78 | 79 | @pytest.mark.parametrize( 80 | "scope, expected_client", 81 | [({"client": ("127.0.0.1", 36000)}, "127.0.0.1:36000"), ({"client": None}, "")], 82 | ids=["ip:port client", "None client"], 83 | ) 84 | def test_get_client_addr(scope, expected_client): 85 | assert get_client_addr(scope) == expected_client 86 | -------------------------------------------------------------------------------- /tests/response.py: -------------------------------------------------------------------------------- 1 | class Response: 2 | charset = "utf-8" 3 | 4 | def __init__(self, content, status_code=200, headers=None, media_type=None): 5 | self.body = self.render(content) 6 | self.status_code = status_code 7 | self.headers = headers or {} 8 | self.media_type = media_type 9 | self.set_content_type() 10 | self.set_content_length() 11 | 12 | async def __call__(self, scope, receive, send) -> None: 13 | prefix = "websocket." if scope["type"] == "websocket" else "" 14 | await send( 15 | { 16 | "type": prefix + "http.response.start", 17 | "status": self.status_code, 18 | "headers": [[key.encode(), value.encode()] for key, value in self.headers.items()], 19 | } 20 | ) 21 | await send({"type": prefix + "http.response.body", "body": self.body}) 22 | 23 | def render(self, content) -> bytes: 24 | if isinstance(content, bytes): 25 | return content 26 | return content.encode(self.charset) 27 | 28 | def set_content_length(self): 29 | if "content-length" not in self.headers: 30 | self.headers["content-length"] = str(len(self.body)) 31 | 32 | def set_content_type(self): 33 | if self.media_type is not None and "content-type" not in self.headers: 34 | content_type = self.media_type 35 | if content_type.startswith("text/") and self.charset is not None: 36 | content_type += "; charset=%s" % self.charset 37 | self.headers["content-type"] = content_type 38 | -------------------------------------------------------------------------------- /tests/supervisors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/tests/supervisors/__init__.py -------------------------------------------------------------------------------- /tests/supervisors/test_multiprocess.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import functools 4 | import os 5 | import signal 6 | import socket 7 | import threading 8 | import time 9 | from typing import Any, Callable 10 | 11 | import pytest 12 | 13 | from uvicorn import Config 14 | from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope 15 | from uvicorn.supervisors import Multiprocess 16 | from uvicorn.supervisors.multiprocess import Process 17 | 18 | 19 | def new_console_in_windows(test_function: Callable[[], Any]) -> Callable[[], Any]: # pragma: no cover 20 | if os.name != "nt": 21 | return test_function 22 | 23 | @functools.wraps(test_function) 24 | def new_function(): 25 | import subprocess 26 | import sys 27 | 28 | module = test_function.__module__ 29 | name = test_function.__name__ 30 | 31 | subprocess.check_call( 32 | [ 33 | sys.executable, 34 | "-c", 35 | f"from {module} import {name}; {name}.__wrapped__()", 36 | ], 37 | creationflags=subprocess.CREATE_NO_WINDOW, # type: ignore[attr-defined] 38 | ) 39 | 40 | return new_function 41 | 42 | 43 | async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 44 | pass # pragma: no cover 45 | 46 | 47 | def run(sockets: list[socket.socket] | None) -> None: 48 | while True: # pragma: no cover 49 | time.sleep(1) 50 | 51 | 52 | def test_process_ping_pong() -> None: 53 | process = Process(Config(app=app), target=lambda x: None, sockets=[]) 54 | threading.Thread(target=process.always_pong, daemon=True).start() 55 | assert process.ping() 56 | 57 | 58 | def test_process_ping_pong_timeout() -> None: 59 | process = Process(Config(app=app), target=lambda x: None, sockets=[]) 60 | assert not process.ping(0.1) 61 | 62 | 63 | @new_console_in_windows 64 | def test_multiprocess_run() -> None: 65 | """ 66 | A basic sanity check. 67 | 68 | Simply run the supervisor against a no-op server, and signal for it to 69 | quit immediately. 70 | """ 71 | config = Config(app=app, workers=2) 72 | supervisor = Multiprocess(config, target=run, sockets=[]) 73 | threading.Thread(target=supervisor.run, daemon=True).start() 74 | supervisor.signal_queue.append(signal.SIGINT) 75 | supervisor.join_all() 76 | 77 | 78 | @new_console_in_windows 79 | def test_multiprocess_health_check() -> None: 80 | """ 81 | Ensure that the health check works as expected. 82 | """ 83 | config = Config(app=app, workers=2) 84 | supervisor = Multiprocess(config, target=run, sockets=[]) 85 | threading.Thread(target=supervisor.run, daemon=True).start() 86 | time.sleep(1) 87 | process = supervisor.processes[0] 88 | process.kill() 89 | assert not process.is_alive() 90 | time.sleep(1) 91 | for p in supervisor.processes: 92 | assert p.is_alive() 93 | supervisor.signal_queue.append(signal.SIGINT) 94 | supervisor.join_all() 95 | 96 | 97 | @new_console_in_windows 98 | def test_multiprocess_sigterm() -> None: 99 | """ 100 | Ensure that the SIGTERM signal is handled as expected. 101 | """ 102 | config = Config(app=app, workers=2) 103 | supervisor = Multiprocess(config, target=run, sockets=[]) 104 | threading.Thread(target=supervisor.run, daemon=True).start() 105 | time.sleep(1) 106 | supervisor.signal_queue.append(signal.SIGTERM) 107 | supervisor.join_all() 108 | 109 | 110 | @pytest.mark.skipif(not hasattr(signal, "SIGBREAK"), reason="platform unsupports SIGBREAK") 111 | @new_console_in_windows 112 | def test_multiprocess_sigbreak() -> None: # pragma: py-not-win32 113 | """ 114 | Ensure that the SIGBREAK signal is handled as expected. 115 | """ 116 | config = Config(app=app, workers=2) 117 | supervisor = Multiprocess(config, target=run, sockets=[]) 118 | threading.Thread(target=supervisor.run, daemon=True).start() 119 | time.sleep(1) 120 | supervisor.signal_queue.append(getattr(signal, "SIGBREAK")) 121 | supervisor.join_all() 122 | 123 | 124 | @pytest.mark.skipif(not hasattr(signal, "SIGHUP"), reason="platform unsupports SIGHUP") 125 | def test_multiprocess_sighup() -> None: 126 | """ 127 | Ensure that the SIGHUP signal is handled as expected. 128 | """ 129 | config = Config(app=app, workers=2) 130 | supervisor = Multiprocess(config, target=run, sockets=[]) 131 | threading.Thread(target=supervisor.run, daemon=True).start() 132 | time.sleep(1) 133 | pids = [p.pid for p in supervisor.processes] 134 | supervisor.signal_queue.append(signal.SIGHUP) 135 | time.sleep(1) 136 | assert pids != [p.pid for p in supervisor.processes] 137 | supervisor.signal_queue.append(signal.SIGINT) 138 | supervisor.join_all() 139 | 140 | 141 | @pytest.mark.skipif(not hasattr(signal, "SIGTTIN"), reason="platform unsupports SIGTTIN") 142 | def test_multiprocess_sigttin() -> None: 143 | """ 144 | Ensure that the SIGTTIN signal is handled as expected. 145 | """ 146 | config = Config(app=app, workers=2) 147 | supervisor = Multiprocess(config, target=run, sockets=[]) 148 | threading.Thread(target=supervisor.run, daemon=True).start() 149 | supervisor.signal_queue.append(signal.SIGTTIN) 150 | time.sleep(1) 151 | assert len(supervisor.processes) == 3 152 | supervisor.signal_queue.append(signal.SIGINT) 153 | supervisor.join_all() 154 | 155 | 156 | @pytest.mark.skipif(not hasattr(signal, "SIGTTOU"), reason="platform unsupports SIGTTOU") 157 | def test_multiprocess_sigttou() -> None: 158 | """ 159 | Ensure that the SIGTTOU signal is handled as expected. 160 | """ 161 | config = Config(app=app, workers=2) 162 | supervisor = Multiprocess(config, target=run, sockets=[]) 163 | threading.Thread(target=supervisor.run, daemon=True).start() 164 | supervisor.signal_queue.append(signal.SIGTTOU) 165 | time.sleep(1) 166 | assert len(supervisor.processes) == 1 167 | supervisor.signal_queue.append(signal.SIGTTOU) 168 | time.sleep(1) 169 | assert len(supervisor.processes) == 1 170 | supervisor.signal_queue.append(signal.SIGINT) 171 | supervisor.join_all() 172 | -------------------------------------------------------------------------------- /tests/supervisors/test_signal.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import signal 3 | from asyncio import Event 4 | 5 | import httpx 6 | import pytest 7 | 8 | from tests.utils import assert_signal, run_server 9 | from uvicorn import Server 10 | from uvicorn.config import Config 11 | 12 | 13 | @pytest.mark.anyio 14 | async def test_sigint_finish_req(unused_tcp_port: int): 15 | """ 16 | 1. Request is sent 17 | 2. Sigint is sent to uvicorn 18 | 3. Shutdown sequence start 19 | 4. Request is finished before timeout_graceful_shutdown=1 20 | 21 | Result: Request should go through, even though the server was cancelled. 22 | """ 23 | 24 | server_event = Event() 25 | 26 | async def wait_app(scope, receive, send): 27 | await send({"type": "http.response.start", "status": 200, "headers": []}) 28 | await send({"type": "http.response.body", "body": b"start", "more_body": True}) 29 | await server_event.wait() 30 | await send({"type": "http.response.body", "body": b"end", "more_body": False}) 31 | 32 | config = Config(app=wait_app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1) 33 | server: Server 34 | with assert_signal(signal.SIGINT): 35 | async with run_server(config) as server, httpx.AsyncClient() as client: 36 | req = asyncio.create_task(client.get(f"http://127.0.0.1:{unused_tcp_port}")) 37 | await asyncio.sleep(0.1) # ensure next tick 38 | server.handle_exit(sig=signal.SIGINT, frame=None) # exit 39 | server_event.set() # continue request 40 | # ensure httpx has processed the response and result is complete 41 | await req 42 | assert req.result().status_code == 200 43 | await asyncio.sleep(0.1) # ensure shutdown is complete 44 | 45 | 46 | @pytest.mark.anyio 47 | async def test_sigint_abort_req(unused_tcp_port: int, caplog): 48 | """ 49 | 1. Request is sent 50 | 2. Sigint is sent to uvicorn 51 | 3. Shutdown sequence start 52 | 4. Request is _NOT_ finished before timeout_graceful_shutdown=1 53 | 54 | Result: Request is cancelled mid-execution, and httpx will raise a 55 | `RemoteProtocolError`. 56 | """ 57 | 58 | async def forever_app(scope, receive, send): 59 | server_event = Event() 60 | await send({"type": "http.response.start", "status": 200, "headers": []}) 61 | await send({"type": "http.response.body", "body": b"start", "more_body": True}) 62 | # we never continue this one, so this request will time out 63 | await server_event.wait() 64 | await send({"type": "http.response.body", "body": b"end", "more_body": False}) # pragma: full coverage 65 | 66 | config = Config(app=forever_app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1) 67 | server: Server 68 | with assert_signal(signal.SIGINT): 69 | async with run_server(config) as server, httpx.AsyncClient() as client: 70 | req = asyncio.create_task(client.get(f"http://127.0.0.1:{unused_tcp_port}")) 71 | await asyncio.sleep(0.1) # next tick 72 | # trigger exit, this request should time out in ~1 sec 73 | server.handle_exit(sig=signal.SIGINT, frame=None) 74 | with pytest.raises(httpx.RemoteProtocolError): 75 | await req 76 | 77 | # req.result() 78 | assert "Cancel 1 running task(s), timeout graceful shutdown exceeded" in caplog.messages 79 | 80 | 81 | @pytest.mark.anyio 82 | async def test_sigint_deny_request_after_triggered(unused_tcp_port: int, caplog): 83 | """ 84 | 1. Server is started 85 | 2. Shutdown sequence start 86 | 3. Request is sent, but not accepted 87 | 88 | Result: Request should fail, and not be able to be sent, since server is no longer 89 | accepting connections. 90 | """ 91 | 92 | async def app(scope, receive, send): 93 | await send({"type": "http.response.start", "status": 200, "headers": []}) 94 | await asyncio.sleep(1) # pragma: full coverage 95 | 96 | config = Config(app=app, reload=False, port=unused_tcp_port, timeout_graceful_shutdown=1) 97 | server: Server 98 | with assert_signal(signal.SIGINT): 99 | async with run_server(config) as server, httpx.AsyncClient() as client: 100 | # exit and ensure we do not accept more requests 101 | server.handle_exit(sig=signal.SIGINT, frame=None) 102 | await asyncio.sleep(0.1) # next tick 103 | with pytest.raises(httpx.ConnectError): 104 | await client.get(f"http://127.0.0.1:{unused_tcp_port}") 105 | -------------------------------------------------------------------------------- /tests/test_auto_detection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import importlib 3 | 4 | import pytest 5 | 6 | from uvicorn.config import Config 7 | from uvicorn.loops.auto import auto_loop_setup 8 | from uvicorn.protocols.http.auto import AutoHTTPProtocol 9 | from uvicorn.protocols.websockets.auto import AutoWebSocketsProtocol 10 | from uvicorn.server import ServerState 11 | 12 | try: 13 | importlib.import_module("uvloop") 14 | expected_loop = "uvloop" # pragma: py-win32 15 | except ImportError: # pragma: py-not-win32 16 | expected_loop = "asyncio" 17 | 18 | try: 19 | importlib.import_module("httptools") 20 | expected_http = "HttpToolsProtocol" 21 | except ImportError: # pragma: no cover 22 | expected_http = "H11Protocol" 23 | 24 | try: 25 | importlib.import_module("websockets") 26 | expected_websockets = "WebSocketProtocol" 27 | except ImportError: # pragma: no cover 28 | expected_websockets = "WSProtocol" 29 | 30 | 31 | async def app(scope, receive, send): 32 | pass # pragma: no cover 33 | 34 | 35 | def test_loop_auto(): 36 | auto_loop_setup() 37 | policy = asyncio.get_event_loop_policy() 38 | assert isinstance(policy, asyncio.events.BaseDefaultEventLoopPolicy) 39 | assert type(policy).__module__.startswith(expected_loop) 40 | 41 | 42 | @pytest.mark.anyio 43 | async def test_http_auto(): 44 | config = Config(app=app) 45 | server_state = ServerState() 46 | protocol = AutoHTTPProtocol( # type: ignore[call-arg] 47 | config=config, server_state=server_state, app_state={} 48 | ) 49 | assert type(protocol).__name__ == expected_http 50 | 51 | 52 | @pytest.mark.anyio 53 | async def test_websocket_auto(): 54 | config = Config(app=app) 55 | server_state = ServerState() 56 | 57 | assert AutoWebSocketsProtocol is not None 58 | protocol = AutoWebSocketsProtocol(config=config, server_state=server_state, app_state={}) 59 | assert type(protocol).__name__ == expected_websockets 60 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import importlib 3 | import os 4 | import platform 5 | import sys 6 | from collections.abc import Iterator 7 | from pathlib import Path 8 | from textwrap import dedent 9 | from unittest import mock 10 | 11 | import pytest 12 | from click.testing import CliRunner 13 | 14 | import uvicorn 15 | from uvicorn.config import Config 16 | from uvicorn.main import main as cli 17 | from uvicorn.server import Server 18 | from uvicorn.supervisors import ChangeReload, Multiprocess 19 | 20 | HEADERS = "Content-Security-Policy:default-src 'self'; script-src https://example.com" 21 | main = importlib.import_module("uvicorn.main") 22 | 23 | 24 | @contextlib.contextmanager 25 | def load_env_var(key: str, value: str) -> Iterator[None]: 26 | old_environ = dict(os.environ) 27 | os.environ[key] = value 28 | yield 29 | os.environ.clear() 30 | os.environ.update(old_environ) 31 | 32 | 33 | class App: 34 | pass 35 | 36 | 37 | def test_cli_print_version() -> None: 38 | runner = CliRunner() 39 | 40 | result = runner.invoke(cli, ["--version"]) 41 | 42 | assert result.exit_code == 0 43 | assert ( 44 | "Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032 45 | version=uvicorn.__version__, 46 | py_implementation=platform.python_implementation(), 47 | py_version=platform.python_version(), 48 | system=platform.system(), 49 | ) 50 | ) in result.output 51 | 52 | 53 | def test_cli_headers() -> None: 54 | runner = CliRunner() 55 | 56 | with mock.patch.object(main, "run") as mock_run: 57 | result = runner.invoke(cli, ["tests.test_cli:App", "--header", HEADERS]) 58 | 59 | assert result.output == "" 60 | assert result.exit_code == 0 61 | mock_run.assert_called_once() 62 | assert mock_run.call_args[1]["headers"] == [ 63 | [ 64 | "Content-Security-Policy", 65 | "default-src 'self'; script-src https://example.com", 66 | ] 67 | ] 68 | 69 | 70 | def test_cli_call_server_run() -> None: 71 | runner = CliRunner() 72 | 73 | with mock.patch.object(Server, "run") as mock_run: 74 | result = runner.invoke(cli, ["tests.test_cli:App"]) 75 | 76 | assert result.exit_code == 3 77 | mock_run.assert_called_once() 78 | 79 | 80 | def test_cli_call_change_reload_run() -> None: 81 | runner = CliRunner() 82 | 83 | with mock.patch.object(Config, "bind_socket") as mock_bind_socket: 84 | with mock.patch.object(ChangeReload, "run") as mock_run: 85 | result = runner.invoke(cli, ["tests.test_cli:App", "--reload"]) 86 | 87 | assert result.exit_code == 0 88 | mock_bind_socket.assert_called_once() 89 | mock_run.assert_called_once() 90 | 91 | 92 | def test_cli_call_multiprocess_run() -> None: 93 | runner = CliRunner() 94 | 95 | with mock.patch.object(Config, "bind_socket") as mock_bind_socket: 96 | with mock.patch.object(Multiprocess, "run") as mock_run: 97 | result = runner.invoke(cli, ["tests.test_cli:App", "--workers=2"]) 98 | 99 | assert result.exit_code == 0 100 | mock_bind_socket.assert_called_once() 101 | mock_run.assert_called_once() 102 | 103 | 104 | @pytest.fixture(params=(True, False)) 105 | def uds_file(tmp_path: Path, request: pytest.FixtureRequest) -> Path: # pragma: py-win32 106 | file = tmp_path / "uvicorn.sock" 107 | should_create_file = request.param 108 | if should_create_file: 109 | file.touch(exist_ok=True) 110 | return file 111 | 112 | 113 | @pytest.mark.skipif(sys.platform == "win32", reason="require unix-like system") 114 | def test_cli_uds(uds_file: Path) -> None: # pragma: py-win32 115 | runner = CliRunner() 116 | 117 | with mock.patch.object(Config, "bind_socket") as mock_bind_socket: 118 | with mock.patch.object(Multiprocess, "run") as mock_run: 119 | result = runner.invoke(cli, ["tests.test_cli:App", "--workers=2", "--uds", str(uds_file)]) 120 | 121 | assert result.exit_code == 0 122 | assert result.output == "" 123 | mock_bind_socket.assert_called_once() 124 | mock_run.assert_called_once() 125 | assert not uds_file.exists() 126 | 127 | 128 | def test_cli_incomplete_app_parameter() -> None: 129 | runner = CliRunner() 130 | 131 | result = runner.invoke(cli, ["tests.test_cli"]) 132 | 133 | assert ( 134 | 'Error loading ASGI app. Import string "tests.test_cli" must be in format ":".' 135 | ) in result.output 136 | assert result.exit_code == 1 137 | 138 | 139 | def test_cli_event_size() -> None: 140 | runner = CliRunner() 141 | 142 | with mock.patch.object(main, "run") as mock_run: 143 | result = runner.invoke( 144 | cli, 145 | ["tests.test_cli:App", "--h11-max-incomplete-event-size", str(32 * 1024)], 146 | ) 147 | 148 | assert result.output == "" 149 | assert result.exit_code == 0 150 | mock_run.assert_called_once() 151 | assert mock_run.call_args[1]["h11_max_incomplete_event_size"] == 32768 152 | 153 | 154 | @pytest.mark.parametrize("http_protocol", ["h11", "httptools"]) 155 | def test_env_variables(http_protocol: str): 156 | with load_env_var("UVICORN_HTTP", http_protocol): 157 | runner = CliRunner(env=os.environ) 158 | with mock.patch.object(main, "run") as mock_run: 159 | runner.invoke(cli, ["tests.test_cli:App"]) 160 | _, kwargs = mock_run.call_args 161 | assert kwargs["http"] == http_protocol 162 | 163 | 164 | def test_ignore_environment_variable_when_set_on_cli(): 165 | with load_env_var("UVICORN_HTTP", "h11"): 166 | runner = CliRunner(env=os.environ) 167 | with mock.patch.object(main, "run") as mock_run: 168 | runner.invoke(cli, ["tests.test_cli:App", "--http=httptools"]) 169 | _, kwargs = mock_run.call_args 170 | assert kwargs["http"] == "httptools" 171 | 172 | 173 | def test_app_dir(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: 174 | app_dir = tmp_path / "dir" / "app_dir" 175 | app_file = app_dir / "main.py" 176 | app_dir.mkdir(parents=True) 177 | app_file.touch() 178 | app_file.write_text( 179 | dedent( 180 | """ 181 | async def app(scope, receive, send): 182 | ... 183 | """ 184 | ) 185 | ) 186 | runner = CliRunner() 187 | with mock.patch.object(Server, "run") as mock_run: 188 | result = runner.invoke(cli, ["main:app", "--app-dir", f"{str(app_dir)}"]) 189 | 190 | assert result.exit_code == 3 191 | mock_run.assert_called_once() 192 | assert sys.path[0] == str(app_dir) 193 | 194 | 195 | def test_set_app_via_environment_variable(): 196 | app_path = "tests.test_cli:App" 197 | with load_env_var("UVICORN_APP", app_path): 198 | runner = CliRunner(env=os.environ) 199 | with mock.patch.object(main, "run") as mock_run: 200 | result = runner.invoke(cli) 201 | args, _ = mock_run.call_args 202 | assert result.exit_code == 0 203 | assert args == (app_path,) 204 | -------------------------------------------------------------------------------- /tests/test_default_headers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import httpx 4 | import pytest 5 | 6 | from tests.utils import run_server 7 | from uvicorn import Config 8 | from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope 9 | 10 | pytestmark = pytest.mark.anyio 11 | 12 | 13 | async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 14 | assert scope["type"] == "http" 15 | await send({"type": "http.response.start", "status": 200, "headers": []}) 16 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 17 | 18 | 19 | async def test_default_default_headers(unused_tcp_port: int): 20 | config = Config(app=app, loop="asyncio", limit_max_requests=1, port=unused_tcp_port) 21 | async with run_server(config): 22 | async with httpx.AsyncClient() as client: 23 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 24 | assert response.headers["server"] == "uvicorn" and response.headers["date"] 25 | 26 | 27 | async def test_override_server_header(unused_tcp_port: int): 28 | headers: list[tuple[str, str]] = [("Server", "over-ridden")] 29 | config = Config(app=app, loop="asyncio", limit_max_requests=1, headers=headers, port=unused_tcp_port) 30 | async with run_server(config): 31 | async with httpx.AsyncClient() as client: 32 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 33 | assert response.headers["server"] == "over-ridden" and response.headers["date"] 34 | 35 | 36 | async def test_disable_default_server_header(unused_tcp_port: int): 37 | config = Config(app=app, loop="asyncio", limit_max_requests=1, server_header=False, port=unused_tcp_port) 38 | async with run_server(config): 39 | async with httpx.AsyncClient() as client: 40 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 41 | assert "server" not in response.headers 42 | 43 | 44 | async def test_override_server_header_multiple_times(unused_tcp_port: int): 45 | headers: list[tuple[str, str]] = [("Server", "over-ridden"), ("Server", "another-value")] 46 | config = Config(app=app, loop="asyncio", limit_max_requests=1, headers=headers, port=unused_tcp_port) 47 | async with run_server(config): 48 | async with httpx.AsyncClient() as client: 49 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 50 | assert response.headers["server"] == "over-ridden, another-value" and response.headers["date"] 51 | 52 | 53 | async def test_add_additional_header(unused_tcp_port: int): 54 | headers: list[tuple[str, str]] = [("X-Additional", "new-value")] 55 | config = Config(app=app, loop="asyncio", limit_max_requests=1, headers=headers, port=unused_tcp_port) 56 | async with run_server(config): 57 | async with httpx.AsyncClient() as client: 58 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 59 | assert response.headers["x-additional"] == "new-value" 60 | assert response.headers["server"] == "uvicorn" 61 | assert response.headers["date"] 62 | 63 | 64 | async def test_disable_default_date_header(unused_tcp_port: int): 65 | config = Config(app=app, loop="asyncio", limit_max_requests=1, date_header=False, port=unused_tcp_port) 66 | async with run_server(config): 67 | async with httpx.AsyncClient() as client: 68 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 69 | assert "date" not in response.headers 70 | -------------------------------------------------------------------------------- /tests/test_lifespan.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from uvicorn.config import Config 6 | from uvicorn.lifespan.off import LifespanOff 7 | from uvicorn.lifespan.on import LifespanOn 8 | 9 | 10 | def test_lifespan_on(): 11 | startup_complete = False 12 | shutdown_complete = False 13 | 14 | async def app(scope, receive, send): 15 | nonlocal startup_complete, shutdown_complete 16 | message = await receive() 17 | assert message["type"] == "lifespan.startup" 18 | startup_complete = True 19 | await send({"type": "lifespan.startup.complete"}) 20 | message = await receive() 21 | assert message["type"] == "lifespan.shutdown" 22 | shutdown_complete = True 23 | await send({"type": "lifespan.shutdown.complete"}) 24 | 25 | async def test(): 26 | config = Config(app=app, lifespan="on") 27 | lifespan = LifespanOn(config) 28 | 29 | assert not startup_complete 30 | assert not shutdown_complete 31 | await lifespan.startup() 32 | assert startup_complete 33 | assert not shutdown_complete 34 | await lifespan.shutdown() 35 | assert startup_complete 36 | assert shutdown_complete 37 | 38 | loop = asyncio.new_event_loop() 39 | loop.run_until_complete(test()) 40 | loop.close() 41 | 42 | 43 | def test_lifespan_off(): 44 | async def app(scope, receive, send): 45 | pass # pragma: no cover 46 | 47 | async def test(): 48 | config = Config(app=app, lifespan="off") 49 | lifespan = LifespanOff(config) 50 | 51 | await lifespan.startup() 52 | await lifespan.shutdown() 53 | 54 | loop = asyncio.new_event_loop() 55 | loop.run_until_complete(test()) 56 | loop.close() 57 | 58 | 59 | def test_lifespan_auto(): 60 | startup_complete = False 61 | shutdown_complete = False 62 | 63 | async def app(scope, receive, send): 64 | nonlocal startup_complete, shutdown_complete 65 | message = await receive() 66 | assert message["type"] == "lifespan.startup" 67 | startup_complete = True 68 | await send({"type": "lifespan.startup.complete"}) 69 | message = await receive() 70 | assert message["type"] == "lifespan.shutdown" 71 | shutdown_complete = True 72 | await send({"type": "lifespan.shutdown.complete"}) 73 | 74 | async def test(): 75 | config = Config(app=app, lifespan="auto") 76 | lifespan = LifespanOn(config) 77 | 78 | assert not startup_complete 79 | assert not shutdown_complete 80 | await lifespan.startup() 81 | assert startup_complete 82 | assert not shutdown_complete 83 | await lifespan.shutdown() 84 | assert startup_complete 85 | assert shutdown_complete 86 | 87 | loop = asyncio.new_event_loop() 88 | loop.run_until_complete(test()) 89 | loop.close() 90 | 91 | 92 | def test_lifespan_auto_with_error(): 93 | async def app(scope, receive, send): 94 | assert scope["type"] == "http" 95 | 96 | async def test(): 97 | config = Config(app=app, lifespan="auto") 98 | lifespan = LifespanOn(config) 99 | 100 | await lifespan.startup() 101 | assert lifespan.error_occured 102 | assert not lifespan.should_exit 103 | await lifespan.shutdown() 104 | 105 | loop = asyncio.new_event_loop() 106 | loop.run_until_complete(test()) 107 | loop.close() 108 | 109 | 110 | def test_lifespan_on_with_error(): 111 | async def app(scope, receive, send): 112 | if scope["type"] != "http": 113 | raise RuntimeError() 114 | 115 | async def test(): 116 | config = Config(app=app, lifespan="on") 117 | lifespan = LifespanOn(config) 118 | 119 | await lifespan.startup() 120 | assert lifespan.error_occured 121 | assert lifespan.should_exit 122 | await lifespan.shutdown() 123 | 124 | loop = asyncio.new_event_loop() 125 | loop.run_until_complete(test()) 126 | loop.close() 127 | 128 | 129 | @pytest.mark.parametrize("mode", ("auto", "on")) 130 | @pytest.mark.parametrize("raise_exception", (True, False)) 131 | def test_lifespan_with_failed_startup(mode, raise_exception, caplog): 132 | async def app(scope, receive, send): 133 | message = await receive() 134 | assert message["type"] == "lifespan.startup" 135 | await send({"type": "lifespan.startup.failed", "message": "the lifespan event failed"}) 136 | if raise_exception: 137 | # App should be able to re-raise an exception if startup failed. 138 | raise RuntimeError() 139 | 140 | async def test(): 141 | config = Config(app=app, lifespan=mode) 142 | lifespan = LifespanOn(config) 143 | 144 | await lifespan.startup() 145 | assert lifespan.startup_failed 146 | assert lifespan.error_occured is raise_exception 147 | assert lifespan.should_exit 148 | await lifespan.shutdown() 149 | 150 | loop = asyncio.new_event_loop() 151 | loop.run_until_complete(test()) 152 | loop.close() 153 | error_messages = [ 154 | record.message for record in caplog.records if record.name == "uvicorn.error" and record.levelname == "ERROR" 155 | ] 156 | assert "the lifespan event failed" in error_messages.pop(0) 157 | assert "Application startup failed. Exiting." in error_messages.pop(0) 158 | 159 | 160 | def test_lifespan_scope_asgi3app(): 161 | async def asgi3app(scope, receive, send): 162 | assert scope == { 163 | "type": "lifespan", 164 | "asgi": {"version": "3.0", "spec_version": "2.0"}, 165 | "state": {}, 166 | } 167 | 168 | async def test(): 169 | config = Config(app=asgi3app, lifespan="on") 170 | lifespan = LifespanOn(config) 171 | 172 | await lifespan.startup() 173 | assert not lifespan.startup_failed 174 | assert not lifespan.error_occured 175 | assert not lifespan.should_exit 176 | await lifespan.shutdown() 177 | 178 | loop = asyncio.new_event_loop() 179 | loop.run_until_complete(test()) 180 | loop.close() 181 | 182 | 183 | def test_lifespan_scope_asgi2app(): 184 | def asgi2app(scope): 185 | assert scope == { 186 | "type": "lifespan", 187 | "asgi": {"version": "2.0", "spec_version": "2.0"}, 188 | "state": {}, 189 | } 190 | 191 | async def asgi(receive, send): 192 | pass 193 | 194 | return asgi 195 | 196 | async def test(): 197 | config = Config(app=asgi2app, lifespan="on") 198 | lifespan = LifespanOn(config) 199 | 200 | await lifespan.startup() 201 | await lifespan.shutdown() 202 | 203 | loop = asyncio.new_event_loop() 204 | loop.run_until_complete(test()) 205 | loop.close() 206 | 207 | 208 | @pytest.mark.parametrize("mode", ("auto", "on")) 209 | @pytest.mark.parametrize("raise_exception", (True, False)) 210 | def test_lifespan_with_failed_shutdown(mode, raise_exception, caplog): 211 | async def app(scope, receive, send): 212 | message = await receive() 213 | assert message["type"] == "lifespan.startup" 214 | await send({"type": "lifespan.startup.complete"}) 215 | message = await receive() 216 | assert message["type"] == "lifespan.shutdown" 217 | await send({"type": "lifespan.shutdown.failed", "message": "the lifespan event failed"}) 218 | 219 | if raise_exception: 220 | # App should be able to re-raise an exception if startup failed. 221 | raise RuntimeError() 222 | 223 | async def test(): 224 | config = Config(app=app, lifespan=mode) 225 | lifespan = LifespanOn(config) 226 | 227 | await lifespan.startup() 228 | assert not lifespan.startup_failed 229 | await lifespan.shutdown() 230 | assert lifespan.shutdown_failed 231 | assert lifespan.error_occured is raise_exception 232 | assert lifespan.should_exit 233 | 234 | loop = asyncio.new_event_loop() 235 | loop.run_until_complete(test()) 236 | error_messages = [ 237 | record.message for record in caplog.records if record.name == "uvicorn.error" and record.levelname == "ERROR" 238 | ] 239 | assert "the lifespan event failed" in error_messages.pop(0) 240 | assert "Application shutdown failed. Exiting." in error_messages.pop(0) 241 | loop.close() 242 | 243 | 244 | def test_lifespan_state(): 245 | async def app(scope, receive, send): 246 | message = await receive() 247 | assert message["type"] == "lifespan.startup" 248 | await send({"type": "lifespan.startup.complete"}) 249 | scope["state"]["foo"] = 123 250 | message = await receive() 251 | assert message["type"] == "lifespan.shutdown" 252 | await send({"type": "lifespan.shutdown.complete"}) 253 | 254 | async def test(): 255 | config = Config(app=app, lifespan="on") 256 | lifespan = LifespanOn(config) 257 | 258 | await lifespan.startup() 259 | assert lifespan.state == {"foo": 123} 260 | await lifespan.shutdown() 261 | 262 | loop = asyncio.new_event_loop() 263 | loop.run_until_complete(test()) 264 | loop.close() 265 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import inspect 3 | import socket 4 | from logging import WARNING 5 | 6 | import httpx 7 | import pytest 8 | 9 | import uvicorn.server 10 | from tests.utils import run_server 11 | from uvicorn import Server 12 | from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope 13 | from uvicorn.config import Config 14 | from uvicorn.main import run 15 | 16 | pytestmark = pytest.mark.anyio 17 | 18 | 19 | async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 20 | assert scope["type"] == "http" 21 | await send({"type": "http.response.start", "status": 204, "headers": []}) 22 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 23 | 24 | 25 | def _has_ipv6(host: str): 26 | sock = None 27 | has_ipv6 = False 28 | if socket.has_ipv6: 29 | try: 30 | sock = socket.socket(socket.AF_INET6) 31 | sock.bind((host, 0)) 32 | has_ipv6 = True 33 | except Exception: # pragma: no cover 34 | pass 35 | if sock: 36 | sock.close() 37 | return has_ipv6 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "host, url", 42 | [ 43 | pytest.param(None, "http://127.0.0.1", id="default"), 44 | pytest.param("localhost", "http://127.0.0.1", id="hostname"), 45 | pytest.param( 46 | "::1", 47 | "http://[::1]", 48 | id="ipv6", 49 | marks=pytest.mark.skipif(not _has_ipv6("::1"), reason="IPV6 not enabled"), 50 | ), 51 | ], 52 | ) 53 | async def test_run(host, url: str, unused_tcp_port: int): 54 | config = Config(app=app, host=host, loop="asyncio", limit_max_requests=1, port=unused_tcp_port) 55 | async with run_server(config): 56 | async with httpx.AsyncClient() as client: 57 | response = await client.get(f"{url}:{unused_tcp_port}") 58 | assert response.status_code == 204 59 | 60 | 61 | async def test_run_multiprocess(unused_tcp_port: int): 62 | config = Config(app=app, loop="asyncio", workers=2, limit_max_requests=1, port=unused_tcp_port) 63 | async with run_server(config): 64 | async with httpx.AsyncClient() as client: 65 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 66 | assert response.status_code == 204 67 | 68 | 69 | async def test_run_reload(unused_tcp_port: int): 70 | config = Config(app=app, loop="asyncio", reload=True, limit_max_requests=1, port=unused_tcp_port) 71 | async with run_server(config): 72 | async with httpx.AsyncClient() as client: 73 | response = await client.get(f"http://127.0.0.1:{unused_tcp_port}") 74 | assert response.status_code == 204 75 | 76 | 77 | def test_run_invalid_app_config_combination(caplog: pytest.LogCaptureFixture) -> None: 78 | with pytest.raises(SystemExit) as exit_exception: 79 | run(app, reload=True) 80 | assert exit_exception.value.code == 1 81 | assert caplog.records[-1].name == "uvicorn.error" 82 | assert caplog.records[-1].levelno == WARNING 83 | assert caplog.records[-1].message == ( 84 | "You must pass the application as an import string to enable 'reload' or 'workers'." 85 | ) 86 | 87 | 88 | def test_run_startup_failure(caplog: pytest.LogCaptureFixture) -> None: 89 | async def app(scope, receive, send): 90 | assert scope["type"] == "lifespan" 91 | message = await receive() 92 | if message["type"] == "lifespan.startup": 93 | raise RuntimeError("Startup failed") 94 | 95 | with pytest.raises(SystemExit) as exit_exception: 96 | run(app, lifespan="on") 97 | assert exit_exception.value.code == 3 98 | 99 | 100 | def test_run_match_config_params() -> None: 101 | config_params = { 102 | key: repr(value) 103 | for key, value in inspect.signature(Config.__init__).parameters.items() 104 | if key not in ("self", "timeout_notify", "callback_notify") 105 | } 106 | run_params = { 107 | key: repr(value) for key, value in inspect.signature(run).parameters.items() if key not in ("app_dir",) 108 | } 109 | assert config_params == run_params 110 | 111 | 112 | async def test_exit_on_create_server_with_invalid_host() -> None: 113 | with pytest.raises(SystemExit) as exc_info: 114 | config = Config(app=app, host="illegal_host") 115 | server = Server(config=config) 116 | await server.serve() 117 | assert exc_info.value.code == 1 118 | 119 | 120 | def test_deprecated_server_state_from_main() -> None: 121 | with pytest.deprecated_call( 122 | match="uvicorn.main.ServerState is deprecated, use uvicorn.server.ServerState instead." 123 | ): 124 | main = importlib.import_module("uvicorn.main") 125 | server_state_cls = getattr(main, "ServerState") 126 | assert server_state_cls is uvicorn.server.ServerState 127 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import contextlib 5 | import logging 6 | import signal 7 | import sys 8 | from collections.abc import Generator 9 | from contextlib import AbstractContextManager 10 | from typing import Callable 11 | 12 | import httpx 13 | import pytest 14 | 15 | from tests.utils import run_server 16 | from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope 17 | from uvicorn.config import Config 18 | from uvicorn.protocols.http.h11_impl import H11Protocol 19 | from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol 20 | from uvicorn.server import Server 21 | 22 | pytestmark = pytest.mark.anyio 23 | 24 | 25 | # asyncio does NOT allow raising in signal handlers, so to detect 26 | # raised signals raised a mutable `witness` receives the signal 27 | @contextlib.contextmanager 28 | def capture_signal_sync(sig: signal.Signals) -> Generator[list[int], None, None]: 29 | """Replace `sig` handling with a normal exception via `signal""" 30 | witness: list[int] = [] 31 | original_handler = signal.signal(sig, lambda signum, frame: witness.append(signum)) 32 | yield witness 33 | signal.signal(sig, original_handler) 34 | 35 | 36 | @contextlib.contextmanager 37 | def capture_signal_async(sig: signal.Signals) -> Generator[list[int], None, None]: # pragma: py-win32 38 | """Replace `sig` handling with a normal exception via `asyncio""" 39 | witness: list[int] = [] 40 | original_handler = signal.getsignal(sig) 41 | asyncio.get_running_loop().add_signal_handler(sig, witness.append, sig) 42 | yield witness 43 | signal.signal(sig, original_handler) 44 | 45 | 46 | async def dummy_app(scope, receive, send): # pragma: py-win32 47 | pass 48 | 49 | 50 | async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 51 | assert scope["type"] == "http" 52 | await send({"type": "http.response.start", "status": 200, "headers": []}) 53 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 54 | 55 | 56 | if sys.platform == "win32": # pragma: py-not-win32 57 | signals = [signal.SIGBREAK] 58 | signal_captures = [capture_signal_sync] 59 | else: # pragma: py-win32 60 | signals = [signal.SIGTERM, signal.SIGINT] 61 | signal_captures = [capture_signal_sync, capture_signal_async] 62 | 63 | 64 | @pytest.mark.parametrize("exception_signal", signals) 65 | @pytest.mark.parametrize("capture_signal", signal_captures) 66 | async def test_server_interrupt( 67 | exception_signal: signal.Signals, 68 | capture_signal: Callable[[signal.Signals], AbstractContextManager[None]], 69 | unused_tcp_port: int, 70 | ): # pragma: py-win32 71 | """Test interrupting a Server that is run explicitly inside asyncio""" 72 | 73 | async def interrupt_running(srv: Server): 74 | while not srv.started: 75 | await asyncio.sleep(0.01) 76 | signal.raise_signal(exception_signal) 77 | 78 | server = Server(Config(app=dummy_app, loop="asyncio", port=unused_tcp_port)) 79 | asyncio.create_task(interrupt_running(server)) 80 | with capture_signal(exception_signal) as witness: 81 | await server.serve() 82 | assert witness 83 | # set by the server's graceful exit handler 84 | assert server.should_exit 85 | 86 | 87 | async def test_request_than_limit_max_requests_warn_log( 88 | unused_tcp_port: int, http_protocol_cls: type[H11Protocol | HttpToolsProtocol], caplog: pytest.LogCaptureFixture 89 | ): 90 | caplog.set_level(logging.WARNING, logger="uvicorn.error") 91 | config = Config(app=app, limit_max_requests=1, port=unused_tcp_port, http=http_protocol_cls) 92 | async with run_server(config): 93 | async with httpx.AsyncClient() as client: 94 | tasks = [client.get(f"http://127.0.0.1:{unused_tcp_port}") for _ in range(2)] 95 | responses = await asyncio.gather(*tasks) 96 | assert len(responses) == 2 97 | assert "Maximum request limit of 1 exceeded. Terminating process." in caplog.text 98 | -------------------------------------------------------------------------------- /tests/test_ssl.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import pytest 3 | 4 | from tests.utils import run_server 5 | from uvicorn.config import Config 6 | 7 | 8 | async def app(scope, receive, send): 9 | assert scope["type"] == "http" 10 | await send({"type": "http.response.start", "status": 204, "headers": []}) 11 | await send({"type": "http.response.body", "body": b"", "more_body": False}) 12 | 13 | 14 | @pytest.mark.anyio 15 | async def test_run( 16 | tls_ca_ssl_context, 17 | tls_certificate_server_cert_path, 18 | tls_certificate_private_key_path, 19 | tls_ca_certificate_pem_path, 20 | unused_tcp_port: int, 21 | ): 22 | config = Config( 23 | app=app, 24 | loop="asyncio", 25 | limit_max_requests=1, 26 | ssl_keyfile=tls_certificate_private_key_path, 27 | ssl_certfile=tls_certificate_server_cert_path, 28 | ssl_ca_certs=tls_ca_certificate_pem_path, 29 | port=unused_tcp_port, 30 | ) 31 | async with run_server(config): 32 | async with httpx.AsyncClient(verify=tls_ca_ssl_context) as client: 33 | response = await client.get(f"https://127.0.0.1:{unused_tcp_port}") 34 | assert response.status_code == 204 35 | 36 | 37 | @pytest.mark.anyio 38 | async def test_run_chain( 39 | tls_ca_ssl_context, 40 | tls_certificate_key_and_chain_path, 41 | tls_ca_certificate_pem_path, 42 | unused_tcp_port: int, 43 | ): 44 | config = Config( 45 | app=app, 46 | loop="asyncio", 47 | limit_max_requests=1, 48 | ssl_certfile=tls_certificate_key_and_chain_path, 49 | ssl_ca_certs=tls_ca_certificate_pem_path, 50 | port=unused_tcp_port, 51 | ) 52 | async with run_server(config): 53 | async with httpx.AsyncClient(verify=tls_ca_ssl_context) as client: 54 | response = await client.get(f"https://127.0.0.1:{unused_tcp_port}") 55 | assert response.status_code == 204 56 | 57 | 58 | @pytest.mark.anyio 59 | async def test_run_chain_only(tls_ca_ssl_context, tls_certificate_key_and_chain_path, unused_tcp_port: int): 60 | config = Config( 61 | app=app, 62 | loop="asyncio", 63 | limit_max_requests=1, 64 | ssl_certfile=tls_certificate_key_and_chain_path, 65 | port=unused_tcp_port, 66 | ) 67 | async with run_server(config): 68 | async with httpx.AsyncClient(verify=tls_ca_ssl_context) as client: 69 | response = await client.get(f"https://127.0.0.1:{unused_tcp_port}") 70 | assert response.status_code == 204 71 | 72 | 73 | @pytest.mark.anyio 74 | async def test_run_password( 75 | tls_ca_ssl_context, 76 | tls_certificate_server_cert_path, 77 | tls_ca_certificate_pem_path, 78 | tls_certificate_private_key_encrypted_path, 79 | unused_tcp_port: int, 80 | ): 81 | config = Config( 82 | app=app, 83 | loop="asyncio", 84 | limit_max_requests=1, 85 | ssl_keyfile=tls_certificate_private_key_encrypted_path, 86 | ssl_certfile=tls_certificate_server_cert_path, 87 | ssl_keyfile_password="uvicorn password for the win", 88 | ssl_ca_certs=tls_ca_certificate_pem_path, 89 | port=unused_tcp_port, 90 | ) 91 | async with run_server(config): 92 | async with httpx.AsyncClient(verify=tls_ca_ssl_context) as client: 93 | response = await client.get(f"https://127.0.0.1:{unused_tcp_port}") 94 | assert response.status_code == 204 95 | -------------------------------------------------------------------------------- /tests/test_subprocess.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import socket 4 | from unittest.mock import patch 5 | 6 | from uvicorn._subprocess import SpawnProcess, get_subprocess, subprocess_started 7 | from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope 8 | from uvicorn.config import Config 9 | 10 | 11 | def server_run(sockets: list[socket.socket]): # pragma: no cover 12 | ... 13 | 14 | 15 | async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: # pragma: no cover 16 | ... 17 | 18 | 19 | def test_get_subprocess() -> None: 20 | fdsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | fd = fdsock.fileno() 22 | config = Config(app=app, fd=fd) 23 | config.load() 24 | 25 | process = get_subprocess(config, server_run, [fdsock]) 26 | assert isinstance(process, SpawnProcess) 27 | 28 | fdsock.close() 29 | 30 | 31 | def test_subprocess_started() -> None: 32 | fdsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 33 | fd = fdsock.fileno() 34 | config = Config(app=app, fd=fd) 35 | config.load() 36 | 37 | with patch("tests.test_subprocess.server_run") as mock_run: 38 | with patch.object(config, "configure_logging") as mock_config_logging: 39 | subprocess_started(config, server_run, [fdsock], None) 40 | mock_run.assert_called_once() 41 | mock_config_logging.assert_called_once() 42 | 43 | fdsock.close() 44 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import os 5 | import signal 6 | from collections.abc import AsyncIterator 7 | from contextlib import asynccontextmanager, contextmanager 8 | from pathlib import Path 9 | from socket import socket 10 | 11 | from uvicorn import Config, Server 12 | 13 | 14 | @asynccontextmanager 15 | async def run_server(config: Config, sockets: list[socket] | None = None) -> AsyncIterator[Server]: 16 | server = Server(config=config) 17 | task = asyncio.create_task(server.serve(sockets=sockets)) 18 | await asyncio.sleep(0.1) 19 | try: 20 | yield server 21 | finally: 22 | await server.shutdown() 23 | task.cancel() 24 | 25 | 26 | @contextmanager 27 | def assert_signal(sig: signal.Signals): 28 | """Check that a signal was received and handled in a block""" 29 | seen: set[int] = set() 30 | prev_handler = signal.signal(sig, lambda num, frame: seen.add(num)) 31 | try: 32 | yield 33 | assert sig in seen, f"process signal {signal.Signals(sig)!r} was not received or handled" 34 | finally: 35 | signal.signal(sig, prev_handler) 36 | 37 | 38 | @contextmanager 39 | def as_cwd(path: Path): 40 | """Changes working directory and returns to previous on exit.""" 41 | prev_cwd = Path.cwd() 42 | os.chdir(path) 43 | try: 44 | yield 45 | finally: 46 | os.chdir(prev_cwd) 47 | -------------------------------------------------------------------------------- /uvicorn/__init__.py: -------------------------------------------------------------------------------- 1 | from uvicorn.config import Config 2 | from uvicorn.main import Server, main, run 3 | 4 | __version__ = "0.34.3" 5 | __all__ = ["main", "run", "Config", "Server"] 6 | -------------------------------------------------------------------------------- /uvicorn/__main__.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | if __name__ == "__main__": 4 | uvicorn.main() 5 | -------------------------------------------------------------------------------- /uvicorn/_subprocess.py: -------------------------------------------------------------------------------- 1 | """ 2 | Some light wrappers around Python's multiprocessing, to deal with cleanly 3 | starting child processes. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | import multiprocessing 9 | import os 10 | import sys 11 | from multiprocessing.context import SpawnProcess 12 | from socket import socket 13 | from typing import Callable 14 | 15 | from uvicorn.config import Config 16 | 17 | multiprocessing.allow_connection_pickling() 18 | spawn = multiprocessing.get_context("spawn") 19 | 20 | 21 | def get_subprocess( 22 | config: Config, 23 | target: Callable[..., None], 24 | sockets: list[socket], 25 | ) -> SpawnProcess: 26 | """ 27 | Called in the parent process, to instantiate a new child process instance. 28 | The child is not yet started at this point. 29 | 30 | * config - The Uvicorn configuration instance. 31 | * target - A callable that accepts a list of sockets. In practice this will 32 | be the `Server.run()` method. 33 | * sockets - A list of sockets to pass to the server. Sockets are bound once 34 | by the parent process, and then passed to the child processes. 35 | """ 36 | # We pass across the stdin fileno, and reopen it in the child process. 37 | # This is required for some debugging environments. 38 | try: 39 | stdin_fileno = sys.stdin.fileno() 40 | # The `sys.stdin` can be `None`, see https://docs.python.org/3/library/sys.html#sys.__stdin__. 41 | except (AttributeError, OSError): 42 | stdin_fileno = None 43 | 44 | kwargs = { 45 | "config": config, 46 | "target": target, 47 | "sockets": sockets, 48 | "stdin_fileno": stdin_fileno, 49 | } 50 | 51 | return spawn.Process(target=subprocess_started, kwargs=kwargs) 52 | 53 | 54 | def subprocess_started( 55 | config: Config, 56 | target: Callable[..., None], 57 | sockets: list[socket], 58 | stdin_fileno: int | None, 59 | ) -> None: 60 | """ 61 | Called when the child process starts. 62 | 63 | * config - The Uvicorn configuration instance. 64 | * target - A callable that accepts a list of sockets. In practice this will 65 | be the `Server.run()` method. 66 | * sockets - A list of sockets to pass to the server. Sockets are bound once 67 | by the parent process, and then passed to the child processes. 68 | * stdin_fileno - The file number of sys.stdin, so that it can be reattached 69 | to the child process. 70 | """ 71 | # Re-open stdin. 72 | if stdin_fileno is not None: 73 | sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage 74 | 75 | # Logging needs to be setup again for each child. 76 | config.configure_logging() 77 | 78 | try: 79 | # Now we can call into `Server.run(sockets=sockets)` 80 | target(sockets=sockets) 81 | except KeyboardInterrupt: # pragma: no cover 82 | # supress the exception to avoid a traceback from subprocess.Popen 83 | # the parent already expects us to end, so no vital information is lost 84 | pass 85 | -------------------------------------------------------------------------------- /uvicorn/_types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) Django Software Foundation and individual contributors. 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of Django nor the names of its contributors may be used 16 | to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | """ 30 | 31 | from __future__ import annotations 32 | 33 | import sys 34 | import types 35 | from collections.abc import Awaitable, Iterable, MutableMapping 36 | from typing import Any, Callable, Literal, Optional, Protocol, TypedDict, Union 37 | 38 | if sys.version_info >= (3, 11): # pragma: py-lt-311 39 | from typing import NotRequired 40 | else: # pragma: py-gte-311 41 | from typing_extensions import NotRequired 42 | 43 | # WSGI 44 | Environ = MutableMapping[str, Any] 45 | ExcInfo = tuple[type[BaseException], BaseException, Optional[types.TracebackType]] 46 | StartResponse = Callable[[str, Iterable[tuple[str, str]], Optional[ExcInfo]], None] 47 | WSGIApp = Callable[[Environ, StartResponse], Union[Iterable[bytes], BaseException]] 48 | 49 | 50 | # ASGI 51 | class ASGIVersions(TypedDict): 52 | spec_version: str 53 | version: Literal["2.0"] | Literal["3.0"] 54 | 55 | 56 | class HTTPScope(TypedDict): 57 | type: Literal["http"] 58 | asgi: ASGIVersions 59 | http_version: str 60 | method: str 61 | scheme: str 62 | path: str 63 | raw_path: bytes 64 | query_string: bytes 65 | root_path: str 66 | headers: Iterable[tuple[bytes, bytes]] 67 | client: tuple[str, int] | None 68 | server: tuple[str, int | None] | None 69 | state: NotRequired[dict[str, Any]] 70 | extensions: NotRequired[dict[str, dict[object, object]]] 71 | 72 | 73 | class WebSocketScope(TypedDict): 74 | type: Literal["websocket"] 75 | asgi: ASGIVersions 76 | http_version: str 77 | scheme: str 78 | path: str 79 | raw_path: bytes 80 | query_string: bytes 81 | root_path: str 82 | headers: Iterable[tuple[bytes, bytes]] 83 | client: tuple[str, int] | None 84 | server: tuple[str, int | None] | None 85 | subprotocols: Iterable[str] 86 | state: NotRequired[dict[str, Any]] 87 | extensions: NotRequired[dict[str, dict[object, object]]] 88 | 89 | 90 | class LifespanScope(TypedDict): 91 | type: Literal["lifespan"] 92 | asgi: ASGIVersions 93 | state: NotRequired[dict[str, Any]] 94 | 95 | 96 | WWWScope = Union[HTTPScope, WebSocketScope] 97 | Scope = Union[HTTPScope, WebSocketScope, LifespanScope] 98 | 99 | 100 | class HTTPRequestEvent(TypedDict): 101 | type: Literal["http.request"] 102 | body: bytes 103 | more_body: bool 104 | 105 | 106 | class HTTPResponseDebugEvent(TypedDict): 107 | type: Literal["http.response.debug"] 108 | info: dict[str, object] 109 | 110 | 111 | class HTTPResponseStartEvent(TypedDict): 112 | type: Literal["http.response.start"] 113 | status: int 114 | headers: NotRequired[Iterable[tuple[bytes, bytes]]] 115 | trailers: NotRequired[bool] 116 | 117 | 118 | class HTTPResponseBodyEvent(TypedDict): 119 | type: Literal["http.response.body"] 120 | body: bytes 121 | more_body: NotRequired[bool] 122 | 123 | 124 | class HTTPResponseTrailersEvent(TypedDict): 125 | type: Literal["http.response.trailers"] 126 | headers: Iterable[tuple[bytes, bytes]] 127 | more_trailers: bool 128 | 129 | 130 | class HTTPServerPushEvent(TypedDict): 131 | type: Literal["http.response.push"] 132 | path: str 133 | headers: Iterable[tuple[bytes, bytes]] 134 | 135 | 136 | class HTTPDisconnectEvent(TypedDict): 137 | type: Literal["http.disconnect"] 138 | 139 | 140 | class WebSocketConnectEvent(TypedDict): 141 | type: Literal["websocket.connect"] 142 | 143 | 144 | class WebSocketAcceptEvent(TypedDict): 145 | type: Literal["websocket.accept"] 146 | subprotocol: NotRequired[str | None] 147 | headers: NotRequired[Iterable[tuple[bytes, bytes]]] 148 | 149 | 150 | class _WebSocketReceiveEventBytes(TypedDict): 151 | type: Literal["websocket.receive"] 152 | bytes: bytes 153 | text: NotRequired[None] 154 | 155 | 156 | class _WebSocketReceiveEventText(TypedDict): 157 | type: Literal["websocket.receive"] 158 | bytes: NotRequired[None] 159 | text: str 160 | 161 | 162 | WebSocketReceiveEvent = Union[_WebSocketReceiveEventBytes, _WebSocketReceiveEventText] 163 | 164 | 165 | class _WebSocketSendEventBytes(TypedDict): 166 | type: Literal["websocket.send"] 167 | bytes: bytes 168 | text: NotRequired[None] 169 | 170 | 171 | class _WebSocketSendEventText(TypedDict): 172 | type: Literal["websocket.send"] 173 | bytes: NotRequired[None] 174 | text: str 175 | 176 | 177 | WebSocketSendEvent = Union[_WebSocketSendEventBytes, _WebSocketSendEventText] 178 | 179 | 180 | class WebSocketResponseStartEvent(TypedDict): 181 | type: Literal["websocket.http.response.start"] 182 | status: int 183 | headers: Iterable[tuple[bytes, bytes]] 184 | 185 | 186 | class WebSocketResponseBodyEvent(TypedDict): 187 | type: Literal["websocket.http.response.body"] 188 | body: bytes 189 | more_body: NotRequired[bool] 190 | 191 | 192 | class WebSocketDisconnectEvent(TypedDict): 193 | type: Literal["websocket.disconnect"] 194 | code: int 195 | reason: NotRequired[str | None] 196 | 197 | 198 | class WebSocketCloseEvent(TypedDict): 199 | type: Literal["websocket.close"] 200 | code: NotRequired[int] 201 | reason: NotRequired[str | None] 202 | 203 | 204 | class LifespanStartupEvent(TypedDict): 205 | type: Literal["lifespan.startup"] 206 | 207 | 208 | class LifespanShutdownEvent(TypedDict): 209 | type: Literal["lifespan.shutdown"] 210 | 211 | 212 | class LifespanStartupCompleteEvent(TypedDict): 213 | type: Literal["lifespan.startup.complete"] 214 | 215 | 216 | class LifespanStartupFailedEvent(TypedDict): 217 | type: Literal["lifespan.startup.failed"] 218 | message: str 219 | 220 | 221 | class LifespanShutdownCompleteEvent(TypedDict): 222 | type: Literal["lifespan.shutdown.complete"] 223 | 224 | 225 | class LifespanShutdownFailedEvent(TypedDict): 226 | type: Literal["lifespan.shutdown.failed"] 227 | message: str 228 | 229 | 230 | WebSocketEvent = Union[WebSocketReceiveEvent, WebSocketDisconnectEvent, WebSocketConnectEvent] 231 | 232 | 233 | ASGIReceiveEvent = Union[ 234 | HTTPRequestEvent, 235 | HTTPDisconnectEvent, 236 | WebSocketConnectEvent, 237 | WebSocketReceiveEvent, 238 | WebSocketDisconnectEvent, 239 | LifespanStartupEvent, 240 | LifespanShutdownEvent, 241 | ] 242 | 243 | 244 | ASGISendEvent = Union[ 245 | HTTPResponseStartEvent, 246 | HTTPResponseBodyEvent, 247 | HTTPResponseTrailersEvent, 248 | HTTPServerPushEvent, 249 | HTTPDisconnectEvent, 250 | WebSocketAcceptEvent, 251 | WebSocketSendEvent, 252 | WebSocketResponseStartEvent, 253 | WebSocketResponseBodyEvent, 254 | WebSocketCloseEvent, 255 | LifespanStartupCompleteEvent, 256 | LifespanStartupFailedEvent, 257 | LifespanShutdownCompleteEvent, 258 | LifespanShutdownFailedEvent, 259 | ] 260 | 261 | 262 | ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]] 263 | ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]] 264 | 265 | 266 | class ASGI2Protocol(Protocol): 267 | def __init__(self, scope: Scope) -> None: ... # pragma: no cover 268 | 269 | async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover 270 | 271 | 272 | ASGI2Application = type[ASGI2Protocol] 273 | ASGI3Application = Callable[ 274 | [ 275 | Scope, 276 | ASGIReceiveCallable, 277 | ASGISendCallable, 278 | ], 279 | Awaitable[None], 280 | ] 281 | ASGIApplication = Union[ASGI2Application, ASGI3Application] 282 | -------------------------------------------------------------------------------- /uvicorn/importer.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | from typing import Any 3 | 4 | 5 | class ImportFromStringError(Exception): 6 | pass 7 | 8 | 9 | def import_from_string(import_str: Any) -> Any: 10 | if not isinstance(import_str, str): 11 | return import_str 12 | 13 | module_str, _, attrs_str = import_str.partition(":") 14 | if not module_str or not attrs_str: 15 | message = 'Import string "{import_str}" must be in format ":".' 16 | raise ImportFromStringError(message.format(import_str=import_str)) 17 | 18 | try: 19 | module = importlib.import_module(module_str) 20 | except ModuleNotFoundError as exc: 21 | if exc.name != module_str: 22 | raise exc from None 23 | message = 'Could not import module "{module_str}".' 24 | raise ImportFromStringError(message.format(module_str=module_str)) 25 | 26 | instance = module 27 | try: 28 | for attr_str in attrs_str.split("."): 29 | instance = getattr(instance, attr_str) 30 | except AttributeError: 31 | message = 'Attribute "{attrs_str}" not found in module "{module_str}".' 32 | raise ImportFromStringError(message.format(attrs_str=attrs_str, module_str=module_str)) 33 | 34 | return instance 35 | -------------------------------------------------------------------------------- /uvicorn/lifespan/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/uvicorn/lifespan/__init__.py -------------------------------------------------------------------------------- /uvicorn/lifespan/off.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | from uvicorn import Config 6 | 7 | 8 | class LifespanOff: 9 | def __init__(self, config: Config) -> None: 10 | self.should_exit = False 11 | self.state: dict[str, Any] = {} 12 | 13 | async def startup(self) -> None: 14 | pass 15 | 16 | async def shutdown(self) -> None: 17 | pass 18 | -------------------------------------------------------------------------------- /uvicorn/lifespan/on.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import logging 5 | from asyncio import Queue 6 | from typing import Any, Union 7 | 8 | from uvicorn import Config 9 | from uvicorn._types import ( 10 | LifespanScope, 11 | LifespanShutdownCompleteEvent, 12 | LifespanShutdownEvent, 13 | LifespanShutdownFailedEvent, 14 | LifespanStartupCompleteEvent, 15 | LifespanStartupEvent, 16 | LifespanStartupFailedEvent, 17 | ) 18 | 19 | LifespanReceiveMessage = Union[LifespanStartupEvent, LifespanShutdownEvent] 20 | LifespanSendMessage = Union[ 21 | LifespanStartupFailedEvent, 22 | LifespanShutdownFailedEvent, 23 | LifespanStartupCompleteEvent, 24 | LifespanShutdownCompleteEvent, 25 | ] 26 | 27 | 28 | STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol." 29 | 30 | 31 | class LifespanOn: 32 | def __init__(self, config: Config) -> None: 33 | if not config.loaded: 34 | config.load() 35 | 36 | self.config = config 37 | self.logger = logging.getLogger("uvicorn.error") 38 | self.startup_event = asyncio.Event() 39 | self.shutdown_event = asyncio.Event() 40 | self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue() 41 | self.error_occured = False 42 | self.startup_failed = False 43 | self.shutdown_failed = False 44 | self.should_exit = False 45 | self.state: dict[str, Any] = {} 46 | 47 | async def startup(self) -> None: 48 | self.logger.info("Waiting for application startup.") 49 | 50 | loop = asyncio.get_event_loop() 51 | main_lifespan_task = loop.create_task(self.main()) # noqa: F841 52 | # Keep a hard reference to prevent garbage collection 53 | # See https://github.com/encode/uvicorn/pull/972 54 | startup_event: LifespanStartupEvent = {"type": "lifespan.startup"} 55 | await self.receive_queue.put(startup_event) 56 | await self.startup_event.wait() 57 | 58 | if self.startup_failed or (self.error_occured and self.config.lifespan == "on"): 59 | self.logger.error("Application startup failed. Exiting.") 60 | self.should_exit = True 61 | else: 62 | self.logger.info("Application startup complete.") 63 | 64 | async def shutdown(self) -> None: 65 | if self.error_occured: 66 | return 67 | self.logger.info("Waiting for application shutdown.") 68 | shutdown_event: LifespanShutdownEvent = {"type": "lifespan.shutdown"} 69 | await self.receive_queue.put(shutdown_event) 70 | await self.shutdown_event.wait() 71 | 72 | if self.shutdown_failed or (self.error_occured and self.config.lifespan == "on"): 73 | self.logger.error("Application shutdown failed. Exiting.") 74 | self.should_exit = True 75 | else: 76 | self.logger.info("Application shutdown complete.") 77 | 78 | async def main(self) -> None: 79 | try: 80 | app = self.config.loaded_app 81 | scope: LifespanScope = { 82 | "type": "lifespan", 83 | "asgi": {"version": self.config.asgi_version, "spec_version": "2.0"}, 84 | "state": self.state, 85 | } 86 | await app(scope, self.receive, self.send) 87 | except BaseException as exc: 88 | self.asgi = None 89 | self.error_occured = True 90 | if self.startup_failed or self.shutdown_failed: 91 | return 92 | if self.config.lifespan == "auto": 93 | msg = "ASGI 'lifespan' protocol appears unsupported." 94 | self.logger.info(msg) 95 | else: 96 | msg = "Exception in 'lifespan' protocol\n" 97 | self.logger.error(msg, exc_info=exc) 98 | finally: 99 | self.startup_event.set() 100 | self.shutdown_event.set() 101 | 102 | async def send(self, message: LifespanSendMessage) -> None: 103 | assert message["type"] in ( 104 | "lifespan.startup.complete", 105 | "lifespan.startup.failed", 106 | "lifespan.shutdown.complete", 107 | "lifespan.shutdown.failed", 108 | ) 109 | 110 | if message["type"] == "lifespan.startup.complete": 111 | assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR 112 | assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR 113 | self.startup_event.set() 114 | 115 | elif message["type"] == "lifespan.startup.failed": 116 | assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR 117 | assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR 118 | self.startup_event.set() 119 | self.startup_failed = True 120 | if message.get("message"): 121 | self.logger.error(message["message"]) 122 | 123 | elif message["type"] == "lifespan.shutdown.complete": 124 | assert self.startup_event.is_set(), STATE_TRANSITION_ERROR 125 | assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR 126 | self.shutdown_event.set() 127 | 128 | elif message["type"] == "lifespan.shutdown.failed": 129 | assert self.startup_event.is_set(), STATE_TRANSITION_ERROR 130 | assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR 131 | self.shutdown_event.set() 132 | self.shutdown_failed = True 133 | if message.get("message"): 134 | self.logger.error(message["message"]) 135 | 136 | async def receive(self) -> LifespanReceiveMessage: 137 | return await self.receive_queue.get() 138 | -------------------------------------------------------------------------------- /uvicorn/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import http 4 | import logging 5 | import sys 6 | from copy import copy 7 | from typing import Literal 8 | 9 | import click 10 | 11 | TRACE_LOG_LEVEL = 5 12 | 13 | 14 | class ColourizedFormatter(logging.Formatter): 15 | """ 16 | A custom log formatter class that: 17 | 18 | * Outputs the LOG_LEVEL with an appropriate color. 19 | * If a log call includes an `extra={"color_message": ...}` it will be used 20 | for formatting the output, instead of the plain text message. 21 | """ 22 | 23 | level_name_colors = { 24 | TRACE_LOG_LEVEL: lambda level_name: click.style(str(level_name), fg="blue"), 25 | logging.DEBUG: lambda level_name: click.style(str(level_name), fg="cyan"), 26 | logging.INFO: lambda level_name: click.style(str(level_name), fg="green"), 27 | logging.WARNING: lambda level_name: click.style(str(level_name), fg="yellow"), 28 | logging.ERROR: lambda level_name: click.style(str(level_name), fg="red"), 29 | logging.CRITICAL: lambda level_name: click.style(str(level_name), fg="bright_red"), 30 | } 31 | 32 | def __init__( 33 | self, 34 | fmt: str | None = None, 35 | datefmt: str | None = None, 36 | style: Literal["%", "{", "$"] = "%", 37 | use_colors: bool | None = None, 38 | ): 39 | if use_colors in (True, False): 40 | self.use_colors = use_colors 41 | else: 42 | self.use_colors = sys.stdout.isatty() 43 | super().__init__(fmt=fmt, datefmt=datefmt, style=style) 44 | 45 | def color_level_name(self, level_name: str, level_no: int) -> str: 46 | def default(level_name: str) -> str: 47 | return str(level_name) # pragma: no cover 48 | 49 | func = self.level_name_colors.get(level_no, default) 50 | return func(level_name) 51 | 52 | def should_use_colors(self) -> bool: 53 | return True # pragma: no cover 54 | 55 | def formatMessage(self, record: logging.LogRecord) -> str: 56 | recordcopy = copy(record) 57 | levelname = recordcopy.levelname 58 | seperator = " " * (8 - len(recordcopy.levelname)) 59 | if self.use_colors: 60 | levelname = self.color_level_name(levelname, recordcopy.levelno) 61 | if "color_message" in recordcopy.__dict__: 62 | recordcopy.msg = recordcopy.__dict__["color_message"] 63 | recordcopy.__dict__["message"] = recordcopy.getMessage() 64 | recordcopy.__dict__["levelprefix"] = levelname + ":" + seperator 65 | return super().formatMessage(recordcopy) 66 | 67 | 68 | class DefaultFormatter(ColourizedFormatter): 69 | def should_use_colors(self) -> bool: 70 | return sys.stderr.isatty() # pragma: no cover 71 | 72 | 73 | class AccessFormatter(ColourizedFormatter): 74 | status_code_colours = { 75 | 1: lambda code: click.style(str(code), fg="bright_white"), 76 | 2: lambda code: click.style(str(code), fg="green"), 77 | 3: lambda code: click.style(str(code), fg="yellow"), 78 | 4: lambda code: click.style(str(code), fg="red"), 79 | 5: lambda code: click.style(str(code), fg="bright_red"), 80 | } 81 | 82 | def get_status_code(self, status_code: int) -> str: 83 | try: 84 | status_phrase = http.HTTPStatus(status_code).phrase 85 | except ValueError: 86 | status_phrase = "" 87 | status_and_phrase = f"{status_code} {status_phrase}" 88 | if self.use_colors: 89 | 90 | def default(code: int) -> str: 91 | return status_and_phrase # pragma: no cover 92 | 93 | func = self.status_code_colours.get(status_code // 100, default) 94 | return func(status_and_phrase) 95 | return status_and_phrase 96 | 97 | def formatMessage(self, record: logging.LogRecord) -> str: 98 | recordcopy = copy(record) 99 | ( 100 | client_addr, 101 | method, 102 | full_path, 103 | http_version, 104 | status_code, 105 | ) = recordcopy.args # type: ignore[misc] 106 | status_code = self.get_status_code(int(status_code)) # type: ignore[arg-type] 107 | request_line = f"{method} {full_path} HTTP/{http_version}" 108 | if self.use_colors: 109 | request_line = click.style(request_line, bold=True) 110 | recordcopy.__dict__.update( 111 | { 112 | "client_addr": client_addr, 113 | "request_line": request_line, 114 | "status_code": status_code, 115 | } 116 | ) 117 | return super().formatMessage(recordcopy) 118 | -------------------------------------------------------------------------------- /uvicorn/loops/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/uvicorn/loops/__init__.py -------------------------------------------------------------------------------- /uvicorn/loops/asyncio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import sys 4 | 5 | logger = logging.getLogger("uvicorn.error") 6 | 7 | 8 | def asyncio_setup(use_subprocess: bool = False) -> None: 9 | if sys.platform == "win32" and use_subprocess: 10 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # pragma: full coverage 11 | -------------------------------------------------------------------------------- /uvicorn/loops/auto.py: -------------------------------------------------------------------------------- 1 | def auto_loop_setup(use_subprocess: bool = False) -> None: 2 | try: 3 | import uvloop # noqa 4 | except ImportError: # pragma: no cover 5 | from uvicorn.loops.asyncio import asyncio_setup as loop_setup 6 | 7 | loop_setup(use_subprocess=use_subprocess) 8 | else: # pragma: no cover 9 | from uvicorn.loops.uvloop import uvloop_setup 10 | 11 | uvloop_setup(use_subprocess=use_subprocess) 12 | -------------------------------------------------------------------------------- /uvicorn/loops/uvloop.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import uvloop 4 | 5 | 6 | def uvloop_setup(use_subprocess: bool = False) -> None: 7 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 8 | -------------------------------------------------------------------------------- /uvicorn/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/uvicorn/middleware/__init__.py -------------------------------------------------------------------------------- /uvicorn/middleware/asgi2.py: -------------------------------------------------------------------------------- 1 | from uvicorn._types import ( 2 | ASGI2Application, 3 | ASGIReceiveCallable, 4 | ASGISendCallable, 5 | Scope, 6 | ) 7 | 8 | 9 | class ASGI2Middleware: 10 | def __init__(self, app: "ASGI2Application"): 11 | self.app = app 12 | 13 | async def __call__(self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None: 14 | instance = self.app(scope) 15 | await instance(receive, send) 16 | -------------------------------------------------------------------------------- /uvicorn/middleware/message_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | from uvicorn._types import ( 5 | ASGI3Application, 6 | ASGIReceiveCallable, 7 | ASGIReceiveEvent, 8 | ASGISendCallable, 9 | ASGISendEvent, 10 | WWWScope, 11 | ) 12 | from uvicorn.logging import TRACE_LOG_LEVEL 13 | 14 | PLACEHOLDER_FORMAT = { 15 | "body": "<{length} bytes>", 16 | "bytes": "<{length} bytes>", 17 | "text": "<{length} chars>", 18 | "headers": "<...>", 19 | } 20 | 21 | 22 | def message_with_placeholders(message: Any) -> Any: 23 | """ 24 | Return an ASGI message, with any body-type content omitted and replaced 25 | with a placeholder. 26 | """ 27 | new_message = message.copy() 28 | for attr in PLACEHOLDER_FORMAT.keys(): 29 | if message.get(attr) is not None: 30 | content = message[attr] 31 | placeholder = PLACEHOLDER_FORMAT[attr].format(length=len(content)) 32 | new_message[attr] = placeholder 33 | return new_message 34 | 35 | 36 | class MessageLoggerMiddleware: 37 | def __init__(self, app: "ASGI3Application"): 38 | self.task_counter = 0 39 | self.app = app 40 | self.logger = logging.getLogger("uvicorn.asgi") 41 | 42 | def trace(message: Any, *args: Any, **kwargs: Any) -> None: 43 | self.logger.log(TRACE_LOG_LEVEL, message, *args, **kwargs) 44 | 45 | self.logger.trace = trace # type: ignore 46 | 47 | async def __call__( 48 | self, 49 | scope: "WWWScope", 50 | receive: "ASGIReceiveCallable", 51 | send: "ASGISendCallable", 52 | ) -> None: 53 | self.task_counter += 1 54 | 55 | task_counter = self.task_counter 56 | client = scope.get("client") 57 | prefix = "%s:%d - ASGI" % (client[0], client[1]) if client else "ASGI" 58 | 59 | async def inner_receive() -> "ASGIReceiveEvent": 60 | message = await receive() 61 | logged_message = message_with_placeholders(message) 62 | log_text = "%s [%d] Receive %s" 63 | self.logger.trace( # type: ignore 64 | log_text, prefix, task_counter, logged_message 65 | ) 66 | return message 67 | 68 | async def inner_send(message: "ASGISendEvent") -> None: 69 | logged_message = message_with_placeholders(message) 70 | log_text = "%s [%d] Send %s" 71 | self.logger.trace( # type: ignore 72 | log_text, prefix, task_counter, logged_message 73 | ) 74 | await send(message) 75 | 76 | logged_scope = message_with_placeholders(scope) 77 | log_text = "%s [%d] Started scope=%s" 78 | self.logger.trace(log_text, prefix, task_counter, logged_scope) # type: ignore 79 | try: 80 | await self.app(scope, inner_receive, inner_send) 81 | except BaseException as exc: 82 | log_text = "%s [%d] Raised exception" 83 | self.logger.trace(log_text, prefix, task_counter) # type: ignore 84 | raise exc from None 85 | else: 86 | log_text = "%s [%d] Completed" 87 | self.logger.trace(log_text, prefix, task_counter) # type: ignore 88 | -------------------------------------------------------------------------------- /uvicorn/middleware/proxy_headers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import ipaddress 4 | 5 | from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope 6 | 7 | 8 | class ProxyHeadersMiddleware: 9 | """Middleware for handling known proxy headers 10 | 11 | This middleware can be used when a known proxy is fronting the application, 12 | and is trusted to be properly setting the `X-Forwarded-Proto` and 13 | `X-Forwarded-For` headers with the connecting client information. 14 | 15 | Modifies the `client` and `scheme` information so that they reference 16 | the connecting client, rather that the connecting proxy. 17 | 18 | References: 19 | - 20 | - 21 | """ 22 | 23 | def __init__(self, app: ASGI3Application, trusted_hosts: list[str] | str = "127.0.0.1") -> None: 24 | self.app = app 25 | self.trusted_hosts = _TrustedHosts(trusted_hosts) 26 | 27 | async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 28 | if scope["type"] == "lifespan": 29 | return await self.app(scope, receive, send) 30 | 31 | client_addr = scope.get("client") 32 | client_host = client_addr[0] if client_addr else None 33 | 34 | if client_host in self.trusted_hosts: 35 | headers = dict(scope["headers"]) 36 | 37 | if b"x-forwarded-proto" in headers: 38 | x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip() 39 | 40 | if x_forwarded_proto in {"http", "https", "ws", "wss"}: 41 | if scope["type"] == "websocket": 42 | scope["scheme"] = x_forwarded_proto.replace("http", "ws") 43 | else: 44 | scope["scheme"] = x_forwarded_proto 45 | 46 | if b"x-forwarded-for" in headers: 47 | x_forwarded_for = headers[b"x-forwarded-for"].decode("latin1") 48 | host = self.trusted_hosts.get_trusted_client_host(x_forwarded_for) 49 | 50 | if host: 51 | # If the x-forwarded-for header is empty then host is an empty string. 52 | # Only set the client if we actually got something usable. 53 | # See: https://github.com/encode/uvicorn/issues/1068 54 | 55 | # We've lost the connecting client's port information by now, 56 | # so only include the host. 57 | port = 0 58 | scope["client"] = (host, port) 59 | 60 | return await self.app(scope, receive, send) 61 | 62 | 63 | def _parse_raw_hosts(value: str) -> list[str]: 64 | return [item.strip() for item in value.split(",")] 65 | 66 | 67 | class _TrustedHosts: 68 | """Container for trusted hosts and networks""" 69 | 70 | def __init__(self, trusted_hosts: list[str] | str) -> None: 71 | self.always_trust: bool = trusted_hosts in ("*", ["*"]) 72 | 73 | self.trusted_literals: set[str] = set() 74 | self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set() 75 | self.trusted_networks: set[ipaddress.IPv4Network | ipaddress.IPv6Network] = set() 76 | 77 | # Notes: 78 | # - We separate hosts from literals as there are many ways to write 79 | # an IPv6 Address so we need to compare by object. 80 | # - We don't convert IP Address to single host networks (e.g. /32 / 128) as 81 | # it more efficient to do an address lookup in a set than check for 82 | # membership in each network. 83 | # - We still allow literals as it might be possible that we receive a 84 | # something that isn't an IP Address e.g. a unix socket. 85 | 86 | if not self.always_trust: 87 | if isinstance(trusted_hosts, str): 88 | trusted_hosts = _parse_raw_hosts(trusted_hosts) 89 | 90 | for host in trusted_hosts: 91 | # Note: because we always convert invalid IP types to literals it 92 | # is not possible for the user to know they provided a malformed IP 93 | # type - this may lead to unexpected / difficult to debug behaviour. 94 | 95 | if "/" in host: 96 | # Looks like a network 97 | try: 98 | self.trusted_networks.add(ipaddress.ip_network(host)) 99 | except ValueError: 100 | # Was not a valid IP Network 101 | self.trusted_literals.add(host) 102 | else: 103 | try: 104 | self.trusted_hosts.add(ipaddress.ip_address(host)) 105 | except ValueError: 106 | # Was not a valid IP Address 107 | self.trusted_literals.add(host) 108 | 109 | def __contains__(self, host: str | None) -> bool: 110 | if self.always_trust: 111 | return True 112 | 113 | if not host: 114 | return False 115 | 116 | try: 117 | ip = ipaddress.ip_address(host) 118 | if ip in self.trusted_hosts: 119 | return True 120 | return any(ip in net for net in self.trusted_networks) 121 | 122 | except ValueError: 123 | return host in self.trusted_literals 124 | 125 | def get_trusted_client_host(self, x_forwarded_for: str) -> str: 126 | """Extract the client host from x_forwarded_for header 127 | 128 | In general this is the first "untrusted" host in the forwarded for list. 129 | """ 130 | x_forwarded_for_hosts = _parse_raw_hosts(x_forwarded_for) 131 | 132 | if self.always_trust: 133 | return x_forwarded_for_hosts[0] 134 | 135 | # Note: each proxy appends to the header list so check it in reverse order 136 | for host in reversed(x_forwarded_for_hosts): 137 | if host not in self: 138 | return host 139 | 140 | # All hosts are trusted meaning that the client was also a trusted proxy 141 | # See https://github.com/encode/uvicorn/issues/1068#issuecomment-855371576 142 | return x_forwarded_for_hosts[0] 143 | -------------------------------------------------------------------------------- /uvicorn/middleware/wsgi.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import concurrent.futures 5 | import io 6 | import sys 7 | import warnings 8 | from collections import deque 9 | from collections.abc import Iterable 10 | 11 | from uvicorn._types import ( 12 | ASGIReceiveCallable, 13 | ASGIReceiveEvent, 14 | ASGISendCallable, 15 | ASGISendEvent, 16 | Environ, 17 | ExcInfo, 18 | HTTPRequestEvent, 19 | HTTPResponseBodyEvent, 20 | HTTPResponseStartEvent, 21 | HTTPScope, 22 | StartResponse, 23 | WSGIApp, 24 | ) 25 | 26 | 27 | def build_environ(scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO) -> Environ: 28 | """ 29 | Builds a scope and request message into a WSGI environ object. 30 | """ 31 | script_name = scope.get("root_path", "").encode("utf8").decode("latin1") 32 | path_info = scope["path"].encode("utf8").decode("latin1") 33 | if path_info.startswith(script_name): 34 | path_info = path_info[len(script_name) :] 35 | environ = { 36 | "REQUEST_METHOD": scope["method"], 37 | "SCRIPT_NAME": script_name, 38 | "PATH_INFO": path_info, 39 | "QUERY_STRING": scope["query_string"].decode("ascii"), 40 | "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], 41 | "wsgi.version": (1, 0), 42 | "wsgi.url_scheme": scope.get("scheme", "http"), 43 | "wsgi.input": body, 44 | "wsgi.errors": sys.stdout, 45 | "wsgi.multithread": True, 46 | "wsgi.multiprocess": True, 47 | "wsgi.run_once": False, 48 | } 49 | 50 | # Get server name and port - required in WSGI, not in ASGI 51 | server = scope.get("server") 52 | if server is None: 53 | server = ("localhost", 80) 54 | environ["SERVER_NAME"] = server[0] 55 | environ["SERVER_PORT"] = server[1] 56 | 57 | # Get client IP address 58 | client = scope.get("client") 59 | if client is not None: 60 | environ["REMOTE_ADDR"] = client[0] 61 | 62 | # Go through headers and make them into environ entries 63 | for name, value in scope.get("headers", []): 64 | name_str: str = name.decode("latin1") 65 | if name_str == "content-length": 66 | corrected_name = "CONTENT_LENGTH" 67 | elif name_str == "content-type": 68 | corrected_name = "CONTENT_TYPE" 69 | else: 70 | corrected_name = "HTTP_%s" % name_str.upper().replace("-", "_") 71 | # HTTPbis say only ASCII chars are allowed in headers, but we latin1 72 | # just in case 73 | value_str: str = value.decode("latin1") 74 | if corrected_name in environ: 75 | corrected_name_environ = environ[corrected_name] 76 | assert isinstance(corrected_name_environ, str) 77 | value_str = corrected_name_environ + "," + value_str 78 | environ[corrected_name] = value_str 79 | return environ 80 | 81 | 82 | class _WSGIMiddleware: 83 | def __init__(self, app: WSGIApp, workers: int = 10): 84 | warnings.warn( 85 | "Uvicorn's native WSGI implementation is deprecated, you should switch to a2wsgi (`pip install a2wsgi`).", 86 | DeprecationWarning, 87 | ) 88 | self.app = app 89 | self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=workers) 90 | 91 | async def __call__( 92 | self, 93 | scope: HTTPScope, 94 | receive: ASGIReceiveCallable, 95 | send: ASGISendCallable, 96 | ) -> None: 97 | assert scope["type"] == "http" 98 | instance = WSGIResponder(self.app, self.executor, scope) 99 | await instance(receive, send) 100 | 101 | 102 | class WSGIResponder: 103 | def __init__( 104 | self, 105 | app: WSGIApp, 106 | executor: concurrent.futures.ThreadPoolExecutor, 107 | scope: HTTPScope, 108 | ): 109 | self.app = app 110 | self.executor = executor 111 | self.scope = scope 112 | self.status = None 113 | self.response_headers = None 114 | self.send_event = asyncio.Event() 115 | self.send_queue: deque[ASGISendEvent | None] = deque() 116 | self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() 117 | self.response_started = False 118 | self.exc_info: ExcInfo | None = None 119 | 120 | async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 121 | message: HTTPRequestEvent = await receive() # type: ignore[assignment] 122 | body = io.BytesIO(message.get("body", b"")) 123 | more_body = message.get("more_body", False) 124 | if more_body: 125 | body.seek(0, io.SEEK_END) 126 | while more_body: 127 | body_message: HTTPRequestEvent = ( 128 | await receive() # type: ignore[assignment] 129 | ) 130 | body.write(body_message.get("body", b"")) 131 | more_body = body_message.get("more_body", False) 132 | body.seek(0) 133 | environ = build_environ(self.scope, message, body) 134 | self.loop = asyncio.get_event_loop() 135 | wsgi = self.loop.run_in_executor(self.executor, self.wsgi, environ, self.start_response) 136 | sender = self.loop.create_task(self.sender(send)) 137 | try: 138 | await asyncio.wait_for(wsgi, None) 139 | finally: 140 | self.send_queue.append(None) 141 | self.send_event.set() 142 | await asyncio.wait_for(sender, None) 143 | if self.exc_info is not None: 144 | raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2]) 145 | 146 | async def sender(self, send: ASGISendCallable) -> None: 147 | while True: 148 | if self.send_queue: 149 | message = self.send_queue.popleft() 150 | if message is None: 151 | return 152 | await send(message) 153 | else: 154 | await self.send_event.wait() 155 | self.send_event.clear() 156 | 157 | def start_response( 158 | self, 159 | status: str, 160 | response_headers: Iterable[tuple[str, str]], 161 | exc_info: ExcInfo | None = None, 162 | ) -> None: 163 | self.exc_info = exc_info 164 | if not self.response_started: 165 | self.response_started = True 166 | status_code_str, _ = status.split(" ", 1) 167 | status_code = int(status_code_str) 168 | headers = [(name.encode("ascii"), value.encode("ascii")) for name, value in response_headers] 169 | http_response_start_event: HTTPResponseStartEvent = { 170 | "type": "http.response.start", 171 | "status": status_code, 172 | "headers": headers, 173 | } 174 | self.send_queue.append(http_response_start_event) 175 | self.loop.call_soon_threadsafe(self.send_event.set) 176 | 177 | def wsgi(self, environ: Environ, start_response: StartResponse) -> None: 178 | for chunk in self.app(environ, start_response): # type: ignore 179 | response_body: HTTPResponseBodyEvent = { 180 | "type": "http.response.body", 181 | "body": chunk, 182 | "more_body": True, 183 | } 184 | self.send_queue.append(response_body) 185 | self.loop.call_soon_threadsafe(self.send_event.set) 186 | 187 | empty_body: HTTPResponseBodyEvent = { 188 | "type": "http.response.body", 189 | "body": b"", 190 | "more_body": False, 191 | } 192 | self.send_queue.append(empty_body) 193 | self.loop.call_soon_threadsafe(self.send_event.set) 194 | 195 | 196 | try: 197 | from a2wsgi import WSGIMiddleware 198 | except ModuleNotFoundError: # pragma: no cover 199 | WSGIMiddleware = _WSGIMiddleware # type: ignore[misc, assignment] 200 | -------------------------------------------------------------------------------- /uvicorn/protocols/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/uvicorn/protocols/__init__.py -------------------------------------------------------------------------------- /uvicorn/protocols/http/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/uvicorn/protocols/http/__init__.py -------------------------------------------------------------------------------- /uvicorn/protocols/http/auto.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | AutoHTTPProtocol: type[asyncio.Protocol] 6 | try: 7 | import httptools # noqa 8 | except ImportError: # pragma: no cover 9 | from uvicorn.protocols.http.h11_impl import H11Protocol 10 | 11 | AutoHTTPProtocol = H11Protocol 12 | else: # pragma: no cover 13 | from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol 14 | 15 | AutoHTTPProtocol = HttpToolsProtocol 16 | -------------------------------------------------------------------------------- /uvicorn/protocols/http/flow_control.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope 4 | 5 | CLOSE_HEADER = (b"connection", b"close") 6 | 7 | HIGH_WATER_LIMIT = 65536 8 | 9 | 10 | class FlowControl: 11 | def __init__(self, transport: asyncio.Transport) -> None: 12 | self._transport = transport 13 | self.read_paused = False 14 | self.write_paused = False 15 | self._is_writable_event = asyncio.Event() 16 | self._is_writable_event.set() 17 | 18 | async def drain(self) -> None: 19 | await self._is_writable_event.wait() # pragma: full coverage 20 | 21 | def pause_reading(self) -> None: 22 | if not self.read_paused: 23 | self.read_paused = True 24 | self._transport.pause_reading() 25 | 26 | def resume_reading(self) -> None: 27 | if self.read_paused: 28 | self.read_paused = False 29 | self._transport.resume_reading() 30 | 31 | def pause_writing(self) -> None: 32 | if not self.write_paused: # pragma: full coverage 33 | self.write_paused = True 34 | self._is_writable_event.clear() 35 | 36 | def resume_writing(self) -> None: 37 | if self.write_paused: # pragma: full coverage 38 | self.write_paused = False 39 | self._is_writable_event.set() 40 | 41 | 42 | async def service_unavailable(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: 43 | await send( 44 | { 45 | "type": "http.response.start", 46 | "status": 503, 47 | "headers": [ 48 | (b"content-type", b"text/plain; charset=utf-8"), 49 | (b"content-length", b"19"), 50 | (b"connection", b"close"), 51 | ], 52 | } 53 | ) 54 | await send({"type": "http.response.body", "body": b"Service Unavailable", "more_body": False}) 55 | -------------------------------------------------------------------------------- /uvicorn/protocols/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import urllib.parse 5 | 6 | from uvicorn._types import WWWScope 7 | 8 | 9 | class ClientDisconnected(OSError): ... 10 | 11 | 12 | def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None: 13 | socket_info = transport.get_extra_info("socket") 14 | if socket_info is not None: 15 | try: 16 | info = socket_info.getpeername() 17 | return (str(info[0]), int(info[1])) if isinstance(info, tuple) else None 18 | except OSError: # pragma: no cover 19 | # This case appears to inconsistently occur with uvloop 20 | # bound to a unix domain socket. 21 | return None 22 | 23 | info = transport.get_extra_info("peername") 24 | if info is not None and isinstance(info, (list, tuple)) and len(info) == 2: 25 | return (str(info[0]), int(info[1])) 26 | return None 27 | 28 | 29 | def get_local_addr(transport: asyncio.Transport) -> tuple[str, int] | None: 30 | socket_info = transport.get_extra_info("socket") 31 | if socket_info is not None: 32 | info = socket_info.getsockname() 33 | 34 | return (str(info[0]), int(info[1])) if isinstance(info, tuple) else None 35 | info = transport.get_extra_info("sockname") 36 | if info is not None and isinstance(info, (list, tuple)) and len(info) == 2: 37 | return (str(info[0]), int(info[1])) 38 | return None 39 | 40 | 41 | def is_ssl(transport: asyncio.Transport) -> bool: 42 | return bool(transport.get_extra_info("sslcontext")) 43 | 44 | 45 | def get_client_addr(scope: WWWScope) -> str: 46 | client = scope.get("client") 47 | if not client: 48 | return "" 49 | return "%s:%d" % client 50 | 51 | 52 | def get_path_with_query_string(scope: WWWScope) -> str: 53 | path_with_query_string = urllib.parse.quote(scope["path"]) 54 | if scope["query_string"]: 55 | path_with_query_string = "{}?{}".format(path_with_query_string, scope["query_string"].decode("ascii")) 56 | return path_with_query_string 57 | -------------------------------------------------------------------------------- /uvicorn/protocols/websockets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encode/uvicorn/5e33d430f13622c8363fe74d97963ab37f3df3c2/uvicorn/protocols/websockets/__init__.py -------------------------------------------------------------------------------- /uvicorn/protocols/websockets/auto.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | from typing import Callable 5 | 6 | AutoWebSocketsProtocol: Callable[..., asyncio.Protocol] | None 7 | try: 8 | import websockets # noqa 9 | except ImportError: # pragma: no cover 10 | try: 11 | import wsproto # noqa 12 | except ImportError: 13 | AutoWebSocketsProtocol = None 14 | else: 15 | from uvicorn.protocols.websockets.wsproto_impl import WSProtocol 16 | 17 | AutoWebSocketsProtocol = WSProtocol 18 | else: 19 | from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol 20 | 21 | AutoWebSocketsProtocol = WebSocketProtocol 22 | -------------------------------------------------------------------------------- /uvicorn/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /uvicorn/supervisors/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from uvicorn.supervisors.basereload import BaseReload 6 | from uvicorn.supervisors.multiprocess import Multiprocess 7 | 8 | if TYPE_CHECKING: 9 | ChangeReload: type[BaseReload] 10 | else: 11 | try: 12 | from uvicorn.supervisors.watchfilesreload import WatchFilesReload as ChangeReload 13 | except ImportError: # pragma: no cover 14 | from uvicorn.supervisors.statreload import StatReload as ChangeReload 15 | 16 | __all__ = ["Multiprocess", "ChangeReload"] 17 | -------------------------------------------------------------------------------- /uvicorn/supervisors/basereload.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import signal 6 | import sys 7 | import threading 8 | from collections.abc import Iterator 9 | from pathlib import Path 10 | from socket import socket 11 | from types import FrameType 12 | from typing import Callable 13 | 14 | import click 15 | 16 | from uvicorn._subprocess import get_subprocess 17 | from uvicorn.config import Config 18 | 19 | HANDLED_SIGNALS = ( 20 | signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. 21 | signal.SIGTERM, # Unix signal 15. Sent by `kill `. 22 | ) 23 | 24 | logger = logging.getLogger("uvicorn.error") 25 | 26 | 27 | class BaseReload: 28 | def __init__( 29 | self, 30 | config: Config, 31 | target: Callable[[list[socket] | None], None], 32 | sockets: list[socket], 33 | ) -> None: 34 | self.config = config 35 | self.target = target 36 | self.sockets = sockets 37 | self.should_exit = threading.Event() 38 | self.pid = os.getpid() 39 | self.is_restarting = False 40 | self.reloader_name: str | None = None 41 | 42 | def signal_handler(self, sig: int, frame: FrameType | None) -> None: # pragma: full coverage 43 | """ 44 | A signal handler that is registered with the parent process. 45 | """ 46 | if sys.platform == "win32" and self.is_restarting: 47 | self.is_restarting = False 48 | else: 49 | self.should_exit.set() 50 | 51 | def run(self) -> None: 52 | self.startup() 53 | for changes in self: 54 | if changes: 55 | logger.warning( 56 | "%s detected changes in %s. Reloading...", 57 | self.reloader_name, 58 | ", ".join(map(_display_path, changes)), 59 | ) 60 | self.restart() 61 | 62 | self.shutdown() 63 | 64 | def pause(self) -> None: 65 | if self.should_exit.wait(self.config.reload_delay): 66 | raise StopIteration() 67 | 68 | def __iter__(self) -> Iterator[list[Path] | None]: 69 | return self 70 | 71 | def __next__(self) -> list[Path] | None: 72 | return self.should_restart() 73 | 74 | def startup(self) -> None: 75 | message = f"Started reloader process [{self.pid}] using {self.reloader_name}" 76 | color_message = "Started reloader process [{}] using {}".format( 77 | click.style(str(self.pid), fg="cyan", bold=True), 78 | click.style(str(self.reloader_name), fg="cyan", bold=True), 79 | ) 80 | logger.info(message, extra={"color_message": color_message}) 81 | 82 | for sig in HANDLED_SIGNALS: 83 | signal.signal(sig, self.signal_handler) 84 | 85 | self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets) 86 | self.process.start() 87 | 88 | def restart(self) -> None: 89 | if sys.platform == "win32": # pragma: py-not-win32 90 | self.is_restarting = True 91 | assert self.process.pid is not None 92 | os.kill(self.process.pid, signal.CTRL_C_EVENT) 93 | 94 | # This is a workaround to ensure the Ctrl+C event is processed 95 | sys.stdout.write(" ") # This has to be a non-empty string 96 | sys.stdout.flush() 97 | else: # pragma: py-win32 98 | self.process.terminate() 99 | self.process.join() 100 | 101 | self.process = get_subprocess(config=self.config, target=self.target, sockets=self.sockets) 102 | self.process.start() 103 | 104 | def shutdown(self) -> None: 105 | if sys.platform == "win32": 106 | self.should_exit.set() # pragma: py-not-win32 107 | else: 108 | self.process.terminate() # pragma: py-win32 109 | self.process.join() 110 | 111 | for sock in self.sockets: 112 | sock.close() 113 | 114 | message = f"Stopping reloader process [{str(self.pid)}]" 115 | color_message = "Stopping reloader process [{}]".format(click.style(str(self.pid), fg="cyan", bold=True)) 116 | logger.info(message, extra={"color_message": color_message}) 117 | 118 | def should_restart(self) -> list[Path] | None: 119 | raise NotImplementedError("Reload strategies should override should_restart()") 120 | 121 | 122 | def _display_path(path: Path) -> str: 123 | try: 124 | return f"'{path.relative_to(Path.cwd())}'" 125 | except ValueError: 126 | return f"'{path}'" 127 | -------------------------------------------------------------------------------- /uvicorn/supervisors/multiprocess.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import signal 6 | import threading 7 | from multiprocessing import Pipe 8 | from socket import socket 9 | from typing import Any, Callable 10 | 11 | import click 12 | 13 | from uvicorn._subprocess import get_subprocess 14 | from uvicorn.config import Config 15 | 16 | SIGNALS = { 17 | getattr(signal, f"SIG{x}"): x 18 | for x in "INT TERM BREAK HUP QUIT TTIN TTOU USR1 USR2 WINCH".split() 19 | if hasattr(signal, f"SIG{x}") 20 | } 21 | 22 | logger = logging.getLogger("uvicorn.error") 23 | 24 | 25 | class Process: 26 | def __init__( 27 | self, 28 | config: Config, 29 | target: Callable[[list[socket] | None], None], 30 | sockets: list[socket], 31 | ) -> None: 32 | self.real_target = target 33 | 34 | self.parent_conn, self.child_conn = Pipe() 35 | self.process = get_subprocess(config, self.target, sockets) 36 | 37 | def ping(self, timeout: float = 5) -> bool: 38 | self.parent_conn.send(b"ping") 39 | if self.parent_conn.poll(timeout): 40 | self.parent_conn.recv() 41 | return True 42 | return False 43 | 44 | def pong(self) -> None: 45 | self.child_conn.recv() 46 | self.child_conn.send(b"pong") 47 | 48 | def always_pong(self) -> None: 49 | while True: 50 | self.pong() 51 | 52 | def target(self, sockets: list[socket] | None = None) -> Any: # pragma: no cover 53 | if os.name == "nt": # pragma: py-not-win32 54 | # Windows doesn't support SIGTERM, so we use SIGBREAK instead. 55 | # And then we raise SIGTERM when SIGBREAK is received. 56 | # https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/signal?view=msvc-170 57 | signal.signal( 58 | signal.SIGBREAK, # type: ignore[attr-defined] 59 | lambda sig, frame: signal.raise_signal(signal.SIGTERM), 60 | ) 61 | 62 | threading.Thread(target=self.always_pong, daemon=True).start() 63 | return self.real_target(sockets) 64 | 65 | def is_alive(self, timeout: float = 5) -> bool: 66 | if not self.process.is_alive(): 67 | return False # pragma: full coverage 68 | 69 | return self.ping(timeout) 70 | 71 | def start(self) -> None: 72 | self.process.start() 73 | 74 | def terminate(self) -> None: 75 | if self.process.exitcode is None: # Process is still running 76 | assert self.process.pid is not None 77 | if os.name == "nt": # pragma: py-not-win32 78 | # Windows doesn't support SIGTERM. 79 | # So send SIGBREAK, and then in process raise SIGTERM. 80 | os.kill(self.process.pid, signal.CTRL_BREAK_EVENT) # type: ignore[attr-defined] 81 | else: 82 | os.kill(self.process.pid, signal.SIGTERM) 83 | logger.info(f"Terminated child process [{self.process.pid}]") 84 | 85 | self.parent_conn.close() 86 | self.child_conn.close() 87 | 88 | def kill(self) -> None: 89 | # In Windows, the method will call `TerminateProcess` to kill the process. 90 | # In Unix, the method will send SIGKILL to the process. 91 | self.process.kill() 92 | 93 | def join(self) -> None: 94 | logger.info(f"Waiting for child process [{self.process.pid}]") 95 | self.process.join() 96 | 97 | @property 98 | def pid(self) -> int | None: 99 | return self.process.pid 100 | 101 | 102 | class Multiprocess: 103 | def __init__( 104 | self, 105 | config: Config, 106 | target: Callable[[list[socket] | None], None], 107 | sockets: list[socket], 108 | ) -> None: 109 | self.config = config 110 | self.target = target 111 | self.sockets = sockets 112 | 113 | self.processes_num = config.workers 114 | self.processes: list[Process] = [] 115 | 116 | self.should_exit = threading.Event() 117 | 118 | self.signal_queue: list[int] = [] 119 | for sig in SIGNALS: 120 | signal.signal(sig, lambda sig, frame: self.signal_queue.append(sig)) 121 | 122 | def init_processes(self) -> None: 123 | for _ in range(self.processes_num): 124 | process = Process(self.config, self.target, self.sockets) 125 | process.start() 126 | self.processes.append(process) 127 | 128 | def terminate_all(self) -> None: 129 | for process in self.processes: 130 | process.terminate() 131 | 132 | def join_all(self) -> None: 133 | for process in self.processes: 134 | process.join() 135 | 136 | def restart_all(self) -> None: 137 | for idx, process in enumerate(self.processes): 138 | process.terminate() 139 | process.join() 140 | new_process = Process(self.config, self.target, self.sockets) 141 | new_process.start() 142 | self.processes[idx] = new_process 143 | 144 | def run(self) -> None: 145 | message = f"Started parent process [{os.getpid()}]" 146 | color_message = "Started parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True)) 147 | logger.info(message, extra={"color_message": color_message}) 148 | 149 | self.init_processes() 150 | 151 | while not self.should_exit.wait(0.5): 152 | self.handle_signals() 153 | self.keep_subprocess_alive() 154 | 155 | self.terminate_all() 156 | self.join_all() 157 | 158 | message = f"Stopping parent process [{os.getpid()}]" 159 | color_message = "Stopping parent process [{}]".format(click.style(str(os.getpid()), fg="cyan", bold=True)) 160 | logger.info(message, extra={"color_message": color_message}) 161 | 162 | def keep_subprocess_alive(self) -> None: 163 | if self.should_exit.is_set(): 164 | return # parent process is exiting, no need to keep subprocess alive 165 | 166 | for idx, process in enumerate(self.processes): 167 | if process.is_alive(): 168 | continue 169 | 170 | process.kill() # process is hung, kill it 171 | process.join() 172 | 173 | if self.should_exit.is_set(): 174 | return # pragma: full coverage 175 | 176 | logger.info(f"Child process [{process.pid}] died") 177 | process = Process(self.config, self.target, self.sockets) 178 | process.start() 179 | self.processes[idx] = process 180 | 181 | def handle_signals(self) -> None: 182 | for sig in tuple(self.signal_queue): 183 | self.signal_queue.remove(sig) 184 | sig_name = SIGNALS[sig] 185 | sig_handler = getattr(self, f"handle_{sig_name.lower()}", None) 186 | if sig_handler is not None: 187 | sig_handler() 188 | else: # pragma: no cover 189 | logger.debug(f"Received signal {sig_name}, but no handler is defined for it.") 190 | 191 | def handle_int(self) -> None: 192 | logger.info("Received SIGINT, exiting.") 193 | self.should_exit.set() 194 | 195 | def handle_term(self) -> None: 196 | logger.info("Received SIGTERM, exiting.") 197 | self.should_exit.set() 198 | 199 | def handle_break(self) -> None: # pragma: py-not-win32 200 | logger.info("Received SIGBREAK, exiting.") 201 | self.should_exit.set() 202 | 203 | def handle_hup(self) -> None: # pragma: py-win32 204 | logger.info("Received SIGHUP, restarting processes.") 205 | self.restart_all() 206 | 207 | def handle_ttin(self) -> None: # pragma: py-win32 208 | logger.info("Received SIGTTIN, increasing the number of processes.") 209 | self.processes_num += 1 210 | process = Process(self.config, self.target, self.sockets) 211 | process.start() 212 | self.processes.append(process) 213 | 214 | def handle_ttou(self) -> None: # pragma: py-win32 215 | logger.info("Received SIGTTOU, decreasing number of processes.") 216 | if self.processes_num <= 1: 217 | logger.info("Already reached one process, cannot decrease the number of processes anymore.") 218 | return 219 | self.processes_num -= 1 220 | process = self.processes.pop() 221 | process.terminate() 222 | process.join() 223 | -------------------------------------------------------------------------------- /uvicorn/supervisors/statreload.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from collections.abc import Iterator 5 | from pathlib import Path 6 | from socket import socket 7 | from typing import Callable 8 | 9 | from uvicorn.config import Config 10 | from uvicorn.supervisors.basereload import BaseReload 11 | 12 | logger = logging.getLogger("uvicorn.error") 13 | 14 | 15 | class StatReload(BaseReload): 16 | def __init__( 17 | self, 18 | config: Config, 19 | target: Callable[[list[socket] | None], None], 20 | sockets: list[socket], 21 | ) -> None: 22 | super().__init__(config, target, sockets) 23 | self.reloader_name = "StatReload" 24 | self.mtimes: dict[Path, float] = {} 25 | 26 | if config.reload_excludes or config.reload_includes: 27 | logger.warning("--reload-include and --reload-exclude have no effect unless watchfiles is installed.") 28 | 29 | def should_restart(self) -> list[Path] | None: 30 | self.pause() 31 | 32 | for file in self.iter_py_files(): 33 | try: 34 | mtime = file.stat().st_mtime 35 | except OSError: # pragma: nocover 36 | continue 37 | 38 | old_time = self.mtimes.get(file) 39 | if old_time is None: 40 | self.mtimes[file] = mtime 41 | continue 42 | elif mtime > old_time: 43 | return [file] 44 | return None 45 | 46 | def restart(self) -> None: 47 | self.mtimes = {} 48 | return super().restart() 49 | 50 | def iter_py_files(self) -> Iterator[Path]: 51 | for reload_dir in self.config.reload_dirs: 52 | for path in list(reload_dir.rglob("*.py")): 53 | yield path.resolve() 54 | -------------------------------------------------------------------------------- /uvicorn/supervisors/watchfilesreload.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from socket import socket 5 | from typing import Callable 6 | 7 | from watchfiles import watch 8 | 9 | from uvicorn.config import Config 10 | from uvicorn.supervisors.basereload import BaseReload 11 | 12 | 13 | class FileFilter: 14 | def __init__(self, config: Config): 15 | default_includes = ["*.py"] 16 | self.includes = [default for default in default_includes if default not in config.reload_excludes] 17 | self.includes.extend(config.reload_includes) 18 | self.includes = list(set(self.includes)) 19 | 20 | default_excludes = [".*", ".py[cod]", ".sw.*", "~*"] 21 | self.excludes = [default for default in default_excludes if default not in config.reload_includes] 22 | self.exclude_dirs = [] 23 | for e in config.reload_excludes: 24 | p = Path(e) 25 | try: 26 | is_dir = p.is_dir() 27 | except OSError: # pragma: no cover 28 | # gets raised on Windows for values like "*.py" 29 | is_dir = False 30 | 31 | if is_dir: 32 | self.exclude_dirs.append(p) 33 | else: 34 | self.excludes.append(e) # pragma: full coverage 35 | self.excludes = list(set(self.excludes)) 36 | 37 | def __call__(self, path: Path) -> bool: 38 | for include_pattern in self.includes: 39 | if path.match(include_pattern): 40 | if str(path).endswith(include_pattern): 41 | return True # pragma: full coverage 42 | 43 | for exclude_dir in self.exclude_dirs: 44 | if exclude_dir in path.parents: 45 | return False 46 | 47 | for exclude_pattern in self.excludes: 48 | if path.match(exclude_pattern): 49 | return False # pragma: full coverage 50 | 51 | return True 52 | return False 53 | 54 | 55 | class WatchFilesReload(BaseReload): 56 | def __init__( 57 | self, 58 | config: Config, 59 | target: Callable[[list[socket] | None], None], 60 | sockets: list[socket], 61 | ) -> None: 62 | super().__init__(config, target, sockets) 63 | self.reloader_name = "WatchFiles" 64 | self.reload_dirs = [] 65 | for directory in config.reload_dirs: 66 | self.reload_dirs.append(directory) 67 | 68 | self.watch_filter = FileFilter(config) 69 | self.watcher = watch( 70 | *self.reload_dirs, 71 | watch_filter=None, 72 | stop_event=self.should_exit, 73 | # using yield_on_timeout here mostly to make sure tests don't 74 | # hang forever, won't affect the class's behavior 75 | yield_on_timeout=True, 76 | ) 77 | 78 | def should_restart(self) -> list[Path] | None: 79 | self.pause() 80 | 81 | changes = next(self.watcher) 82 | if changes: 83 | unique_paths = {Path(c[1]) for c in changes} 84 | return [p for p in unique_paths if self.watch_filter(p)] 85 | return None 86 | -------------------------------------------------------------------------------- /uvicorn/workers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import logging 5 | import signal 6 | import sys 7 | import warnings 8 | from typing import Any 9 | 10 | from gunicorn.arbiter import Arbiter 11 | from gunicorn.workers.base import Worker 12 | 13 | from uvicorn.config import Config 14 | from uvicorn.server import Server 15 | 16 | warnings.warn( 17 | "The `uvicorn.workers` module is deprecated. Please use `uvicorn-worker` package instead.\n" 18 | "For more details, see https://github.com/Kludex/uvicorn-worker.", 19 | DeprecationWarning, 20 | ) 21 | 22 | 23 | class UvicornWorker(Worker): 24 | """ 25 | A worker class for Gunicorn that interfaces with an ASGI consumer callable, 26 | rather than a WSGI callable. 27 | """ 28 | 29 | CONFIG_KWARGS: dict[str, Any] = {"loop": "auto", "http": "auto"} 30 | 31 | def __init__(self, *args: Any, **kwargs: Any) -> None: 32 | super().__init__(*args, **kwargs) 33 | 34 | logger = logging.getLogger("uvicorn.error") 35 | logger.handlers = self.log.error_log.handlers 36 | logger.setLevel(self.log.error_log.level) 37 | logger.propagate = False 38 | 39 | logger = logging.getLogger("uvicorn.access") 40 | logger.handlers = self.log.access_log.handlers 41 | logger.setLevel(self.log.access_log.level) 42 | logger.propagate = False 43 | 44 | config_kwargs: dict = { 45 | "app": None, 46 | "log_config": None, 47 | "timeout_keep_alive": self.cfg.keepalive, 48 | "timeout_notify": self.timeout, 49 | "callback_notify": self.callback_notify, 50 | "limit_max_requests": self.max_requests, 51 | "forwarded_allow_ips": self.cfg.forwarded_allow_ips, 52 | } 53 | 54 | if self.cfg.is_ssl: 55 | ssl_kwargs = { 56 | "ssl_keyfile": self.cfg.ssl_options.get("keyfile"), 57 | "ssl_certfile": self.cfg.ssl_options.get("certfile"), 58 | "ssl_keyfile_password": self.cfg.ssl_options.get("password"), 59 | "ssl_version": self.cfg.ssl_options.get("ssl_version"), 60 | "ssl_cert_reqs": self.cfg.ssl_options.get("cert_reqs"), 61 | "ssl_ca_certs": self.cfg.ssl_options.get("ca_certs"), 62 | "ssl_ciphers": self.cfg.ssl_options.get("ciphers"), 63 | } 64 | config_kwargs.update(ssl_kwargs) 65 | 66 | if self.cfg.settings["backlog"].value: 67 | config_kwargs["backlog"] = self.cfg.settings["backlog"].value 68 | 69 | config_kwargs.update(self.CONFIG_KWARGS) 70 | 71 | self.config = Config(**config_kwargs) 72 | 73 | def init_process(self) -> None: 74 | self.config.setup_event_loop() 75 | super().init_process() 76 | 77 | def init_signals(self) -> None: 78 | # Reset signals so Gunicorn doesn't swallow subprocess return codes 79 | # other signals are set up by Server.install_signal_handlers() 80 | # See: https://github.com/encode/uvicorn/issues/894 81 | for s in self.SIGNALS: 82 | signal.signal(s, signal.SIG_DFL) 83 | 84 | signal.signal(signal.SIGUSR1, self.handle_usr1) 85 | # Don't let SIGUSR1 disturb active requests by interrupting system calls 86 | signal.siginterrupt(signal.SIGUSR1, False) 87 | 88 | def _install_sigquit_handler(self) -> None: 89 | """Install a SIGQUIT handler on workers. 90 | 91 | - https://github.com/encode/uvicorn/issues/1116 92 | - https://github.com/benoitc/gunicorn/issues/2604 93 | """ 94 | 95 | loop = asyncio.get_running_loop() 96 | loop.add_signal_handler(signal.SIGQUIT, self.handle_exit, signal.SIGQUIT, None) 97 | 98 | async def _serve(self) -> None: 99 | self.config.app = self.wsgi 100 | server = Server(config=self.config) 101 | self._install_sigquit_handler() 102 | await server.serve(sockets=self.sockets) 103 | if not server.started: 104 | sys.exit(Arbiter.WORKER_BOOT_ERROR) 105 | 106 | def run(self) -> None: 107 | return asyncio.run(self._serve()) 108 | 109 | async def callback_notify(self) -> None: 110 | self.notify() 111 | 112 | 113 | class UvicornH11Worker(UvicornWorker): 114 | CONFIG_KWARGS = {"loop": "asyncio", "http": "h11"} 115 | --------------------------------------------------------------------------------