8 |
9 | ---
10 |
11 | [](https://github.com/encode/uvicorn/actions)
12 | [](https://pypi.python.org/pypi/uvicorn)
13 | [](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 |
114 |
115 |
116 | Here are some common ways the test suite can fail:
117 |
118 | ### Check Job Failed
119 |
120 |
121 |
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 |
--------------------------------------------------------------------------------