1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882
|
Starlette includes several middleware classes for adding behavior that is applied across
your entire application. These are all implemented as standard ASGI
middleware classes, and can be applied either to Starlette or to any other ASGI application.
## Using middleware
The Starlette application class allows you to include the ASGI middleware
in a way that ensures that it remains wrapped by the exception handler.
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
routes = ...
# Ensure that all requests include an 'example.com' or
# '*.example.com' host header, and strictly enforce https-only access.
middleware = [
Middleware(
TrustedHostMiddleware,
allowed_hosts=['example.com', '*.example.com'],
),
Middleware(HTTPSRedirectMiddleware)
]
app = Starlette(routes=routes, middleware=middleware)
```
Every Starlette application automatically includes two pieces of middleware by default:
* `ServerErrorMiddleware` - Ensures that application exceptions may return a custom 500 page, or display an application traceback in DEBUG mode. This is *always* the outermost middleware layer.
* `ExceptionMiddleware` - Adds exception handlers, so that particular types of expected exception cases can be associated with handler functions. For example raising `HTTPException(status_code=404)` within an endpoint will end up rendering a custom 404 page.
Middleware is evaluated from top-to-bottom, so the flow of execution in our example
application would look like this:
* Middleware
* `ServerErrorMiddleware`
* `TrustedHostMiddleware`
* `HTTPSRedirectMiddleware`
* `ExceptionMiddleware`
* Routing
* Endpoint
The following middleware implementations are available in the Starlette package:
## CORSMiddleware
Adds appropriate [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) to outgoing responses in order to allow cross-origin requests from browsers.
The default parameters used by the CORSMiddleware implementation are restrictive by default,
so you'll need to explicitly enable particular origins, methods, or headers, in order
for browsers to be permitted to use them in a Cross-Domain context.
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
routes = ...
middleware = [
Middleware(CORSMiddleware, allow_origins=['*'])
]
app = Starlette(routes=routes, middleware=middleware)
```
The following arguments are supported:
* `allow_origins` - A list of origins that should be permitted to make cross-origin requests. eg. `['https://example.org', 'https://www.example.org']`. You can use `['*']` to allow any origin.
* `allow_origin_regex` - A regex string to match against origins that should be permitted to make cross-origin requests. eg. `'https://.*\.example\.org'`.
* `allow_methods` - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to `['GET']`. You can use `['*']` to allow all standard methods.
* `allow_headers` - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to `[]`. You can use `['*']` to allow all headers. The `Accept`, `Accept-Language`, `Content-Language` and `Content-Type` headers are always allowed for CORS requests.
* `allow_credentials` - Indicate that cookies should be supported for cross-origin requests. Defaults to `False`. Also, `allow_origins`, `allow_methods` and `allow_headers` cannot be set to `['*']` for credentials to be allowed, all of them must be explicitly specified.
* `expose_headers` - Indicate any response headers that should be made accessible to the browser. Defaults to `[]`.
* `max_age` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to `600`.
The middleware responds to two particular types of HTTP request...
#### CORS preflight requests
These are any `OPTIONS` request with `Origin` and `Access-Control-Request-Method` headers.
In this case the middleware will intercept the incoming request and respond with
appropriate CORS headers, and either a 200 or 400 response for informational purposes.
#### Simple requests
Any request with an `Origin` header. In this case the middleware will pass the
request through as normal, but will include appropriate CORS headers on the response.
### CORSMiddleware Global Enforcement
When using CORSMiddleware with your Starlette application, it's important to ensure that CORS headers are applied even to error responses generated by unhandled exceptions. The recommended solution is to wrap the entire Starlette application with CORSMiddleware. This approach guarantees that even if an exception is caught by ServerErrorMiddleware (or other outer error-handling middleware), the response will still include the proper `Access-Control-Allow-Origin` header.
For example, instead of adding CORSMiddleware as an inner `middleware` via the Starlette middleware parameter, you can wrap your application as follows:
```python
from starlette.applications import Starlette
from starlette.middleware.cors import CORSMiddleware
import uvicorn
app = Starlette()
app = CORSMiddleware(app=app, allow_origins=["*"])
# ... your routes and middleware configuration ...
if __name__ == '__main__':
uvicorn.run(
app,
host='0.0.0.0',
port=8000
)
```
## SessionMiddleware
Adds signed cookie-based HTTP sessions. Session information is readable but not modifiable.
Access or modify the session data using the `request.session` dictionary interface.
The following arguments are supported:
* `secret_key` - Should be a random string.
* `session_cookie` - Defaults to "session".
* `max_age` - Session expiry time in seconds. Defaults to 2 weeks. If set to `None` then the cookie will last as long as the browser session.
* `same_site` - SameSite flag prevents the browser from sending session cookie along with cross-site requests. Defaults to `'lax'`.
* `path` - The path set for the session cookie. Defaults to `'/'`.
* `https_only` - Indicate that Secure flag should be set (can be used with HTTPS only). Defaults to `False`.
* `domain` - Domain of the cookie used to share cookie between subdomains or cross-domains. The browser defaults the domain to the same host that set the cookie, excluding subdomains ([reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#domain_attribute)).
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
routes = ...
middleware = [
Middleware(SessionMiddleware, secret_key=..., https_only=True)
]
app = Starlette(routes=routes, middleware=middleware)
```
## HTTPSRedirectMiddleware
Enforces that all incoming requests must either be `https` or `wss`. Any incoming
requests to `http` or `ws` will be redirected to the secure scheme instead.
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
routes = ...
middleware = [
Middleware(HTTPSRedirectMiddleware)
]
app = Starlette(routes=routes, middleware=middleware)
```
There are no configuration options for this middleware class.
## TrustedHostMiddleware
Enforces that all incoming requests have a correctly set `Host` header, in order
to guard against HTTP Host Header attacks.
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
routes = ...
middleware = [
Middleware(TrustedHostMiddleware, allowed_hosts=['example.com', '*.example.com'])
]
app = Starlette(routes=routes, middleware=middleware)
```
The following arguments are supported:
* `allowed_hosts` - A list of domain names that should be allowed as hostnames. Wildcard
domains such as `*.example.com` are supported for matching subdomains. To allow any
hostname either use `allowed_hosts=["*"]` or omit the middleware.
* `www_redirect` - If set to True, requests to non-www versions of the allowed hosts will be redirected to their www counterparts. Defaults to `True`.
If an incoming request does not validate correctly then a 400 response will be sent.
## GZipMiddleware
Handles GZip responses for any request that includes `"gzip"` in the `Accept-Encoding` header.
The middleware will handle both standard and streaming responses.
??? info "Buffer on streaming responses"
On streaming responses, the middleware will buffer the response before compressing it.
The idea is that we don't want to compress every small chunk of data, as it would be inefficient.
Instead, we buffer the response until it reaches a certain size, and then compress it.
This may cause a delay in the response, as the middleware waits for the buffer to fill up before compressing it.
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware
routes = ...
middleware = [
Middleware(GZipMiddleware, minimum_size=1000, compresslevel=9)
]
app = Starlette(routes=routes, middleware=middleware)
```
The following arguments are supported:
* `minimum_size` - Do not GZip responses that are smaller than this minimum size in bytes. Defaults to `500`.
* `compresslevel` - Used during GZip compression. It is an integer ranging from 1 to 9. Defaults to `9`. Lower value results in faster compression but larger file sizes, while higher value results in slower compression but smaller file sizes.
The middleware won't GZip responses that already have either a `Content-Encoding` set, to prevent them from
being encoded twice, or a `Content-Type` set to `text/event-stream`, to avoid compressing server-sent events.
## BaseHTTPMiddleware
An abstract class that allows you to write ASGI middleware against a request/response
interface.
### Usage
To implement a middleware class using `BaseHTTPMiddleware`, you must override the
`async def dispatch(request, call_next)` method.
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
class CustomHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers['Custom'] = 'Example'
return response
routes = ...
middleware = [
Middleware(CustomHeaderMiddleware)
]
app = Starlette(routes=routes, middleware=middleware)
```
If you want to provide configuration options to the middleware class you should
override the `__init__` method, ensuring that the first argument is `app`, and
any remaining arguments are optional keyword arguments. Make sure to set the `app`
attribute on the instance if you do this.
```python
class CustomHeaderMiddleware(BaseHTTPMiddleware):
def __init__(self, app, header_value='Example'):
super().__init__(app)
self.header_value = header_value
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers['Custom'] = self.header_value
return response
middleware = [
Middleware(CustomHeaderMiddleware, header_value='Customized')
]
app = Starlette(routes=routes, middleware=middleware)
```
Middleware classes should not modify their state outside of the `__init__` method.
Instead you should keep any state local to the `dispatch` method, or pass it
around explicitly, rather than mutating the middleware instance.
### Limitations
Currently, the `BaseHTTPMiddleware` has some known limitations:
- Using `BaseHTTPMiddleware` will prevent changes to [`contextvars.ContextVar`](https://docs.python.org/3/library/contextvars.html#contextvars.ContextVar)s from propagating upwards. That is, if you set a value for a `ContextVar` in your endpoint and try to read it from a middleware you will find that the value is not the same value you set in your endpoint (see [this test](https://github.com/Kludex/starlette/blob/621abc747a6604825190b93467918a0ec6456a24/tests/middleware/test_base.py#L192-L223) for an example of this behavior). Importantly, this also means that if a `BaseHTTPMiddleware` is positioned earlier in the middleware stack, it will disrupt `contextvars` propagation for any subsequent Pure ASGI Middleware that relies on them.
To overcome these limitations, use [pure ASGI middleware](#pure-asgi-middleware), as shown below.
## Pure ASGI Middleware
The [ASGI spec](https://asgi.readthedocs.io/en/latest/) makes it possible to implement ASGI middleware using the ASGI interface directly, as a chain of ASGI applications that call into the next one. In fact, this is how middleware classes shipped with Starlette are implemented.
This lower-level approach provides greater control over behavior and enhanced interoperability across frameworks and servers. It also overcomes the [limitations of `BaseHTTPMiddleware`](#limitations).
### Writing pure ASGI middleware
The most common way to create an ASGI middleware is with a class.
```python
class ASGIMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
await self.app(scope, receive, send)
```
The middleware above is the most basic ASGI middleware. It receives a parent ASGI application as an argument for its constructor, and implements an `async __call__` method which calls into that parent application.
Some implementations such as [`asgi-cors`](https://github.com/simonw/asgi-cors/blob/10ef64bfcc6cd8d16f3014077f20a0fb8544ec39/asgi_cors.py) use an alternative style, using functions:
```python
import functools
def asgi_middleware():
def asgi_decorator(app):
@functools.wraps(app)
async def wrapped_app(scope, receive, send):
await app(scope, receive, send)
return wrapped_app
return asgi_decorator
```
In any case, ASGI middleware must be callables that accept three arguments: `scope`, `receive`, and `send`.
* `scope` is a dict holding information about the connection, where `scope["type"]` may be:
* [`"http"`](https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope): for HTTP requests.
* [`"websocket"`](https://asgi.readthedocs.io/en/latest/specs/www.html#websocket-connection-scope): for WebSocket connections.
* [`"lifespan"`](https://asgi.readthedocs.io/en/latest/specs/lifespan.html#scope): for ASGI lifespan messages.
* `receive` and `send` can be used to exchange ASGI event messages with the ASGI server — more on this below. The type and contents of these messages depend on the scope type. Learn more in the [ASGI specification](https://asgi.readthedocs.io/en/latest/specs/index.html).
### Using pure ASGI middleware
Pure ASGI middleware can be used like any other middleware:
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from .middleware import ASGIMiddleware
routes = ...
middleware = [
Middleware(ASGIMiddleware),
]
app = Starlette(..., middleware=middleware)
```
See also [Using middleware](#using-middleware).
### Type annotations
There are two ways of annotating a middleware: using Starlette itself or [`asgiref`](https://github.com/django/asgiref).
* Using Starlette: for most common use cases.
```python
from starlette.types import ASGIApp, Message, Scope, Receive, Send
class ASGIMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
return await self.app(scope, receive, send)
async def send_wrapper(message: Message) -> None:
# ... Do something
await send(message)
await self.app(scope, receive, send_wrapper)
```
* Using [`asgiref`](https://github.com/django/asgiref): for more rigorous type hinting.
```python
from asgiref.typing import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope
from asgiref.typing import ASGIReceiveEvent, ASGISendEvent
class ASGIMiddleware:
def __init__(self, app: ASGI3Application) -> None:
self.app = app
async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def send_wrapper(message: ASGISendEvent) -> None:
# ... Do something
await send(message)
return await self.app(scope, receive, send_wrapper)
```
### Common patterns
#### Processing certain requests only
ASGI middleware can apply specific behavior according to the contents of `scope`.
For example, to only process HTTP requests, write this...
```python
class ASGIMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
... # Do something here!
await self.app(scope, receive, send)
```
Likewise, WebSocket-only middleware would guard on `scope["type"] != "websocket"`.
The middleware may also act differently based on the request method, URL, headers, etc.
#### Reusing Starlette components
Starlette provides several data structures that accept the ASGI `scope`, `receive` and/or `send` arguments, allowing you to work at a higher level of abstraction. Such data structures include [`Request`](requests.md#request), [`Headers`](requests.md#headers), [`QueryParams`](requests.md#query-parameters), [`URL`](requests.md#url), etc.
For example, you can instantiate a `Request` to more easily inspect an HTTP request:
```python
from starlette.requests import Request
class ASGIMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
request = Request(scope)
... # Use `request.method`, `request.url`, `request.headers`, etc.
await self.app(scope, receive, send)
```
You can also reuse [responses](responses.md), which are ASGI applications as well.
#### Sending eager responses
Inspecting the connection `scope` allows you to conditionally call into a different ASGI app. One use case might be sending a response without calling into the app.
As an example, this middleware uses a dictionary to perform permanent redirects based on the requested path. This could be used to implement ongoing support of legacy URLs in case you need to refactor route URL patterns.
```python
from starlette.datastructures import URL
from starlette.responses import RedirectResponse
class RedirectsMiddleware:
def __init__(self, app, path_mapping: dict):
self.app = app
self.path_mapping = path_mapping
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
url = URL(scope=scope)
if url.path in self.path_mapping:
url = url.replace(path=self.path_mapping[url.path])
response = RedirectResponse(url, status_code=301)
await response(scope, receive, send)
return
await self.app(scope, receive, send)
```
Example usage would look like this:
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
routes = ...
redirections = {
"/v1/resource/": "/v2/resource/",
# ...
}
middleware = [
Middleware(RedirectsMiddleware, path_mapping=redirections),
]
app = Starlette(routes=routes, middleware=middleware)
```
#### Inspecting or modifying the request
Request information can be accessed or changed by manipulating the `scope`. For a full example of this pattern, see Uvicorn's [`ProxyHeadersMiddleware`](https://github.com/encode/uvicorn/blob/fd4386fefb8fe8a4568831a7d8b2930d5fb61455/uvicorn/middleware/proxy_headers.py) which inspects and tweaks the `scope` when serving behind a frontend proxy.
Besides, wrapping the `receive` ASGI callable allows you to access or modify the HTTP request body by manipulating [`http.request`](https://asgi.readthedocs.io/en/latest/specs/www.html#request-receive-event) ASGI event messages.
As an example, this middleware computes and logs the size of the incoming request body...
```python
class LoggedRequestBodySizeMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
body_size = 0
async def receive_logging_request_body_size():
nonlocal body_size
message = await receive()
assert message["type"] == "http.request"
body_size += len(message.get("body", b""))
if not message.get("more_body", False):
print(f"Size of request body was: {body_size} bytes")
return message
await self.app(scope, receive_logging_request_body_size, send)
```
Likewise, WebSocket middleware may manipulate [`websocket.receive`](https://asgi.readthedocs.io/en/latest/specs/www.html#receive-receive-event) ASGI event messages to inspect or alter incoming WebSocket data.
For an example that changes the HTTP request body, see [`msgpack-asgi`](https://github.com/florimondmanca/msgpack-asgi).
#### Inspecting or modifying the response
Wrapping the `send` ASGI callable allows you to inspect or modify the HTTP response sent by the underlying application. To do so, react to [`http.response.start`](https://asgi.readthedocs.io/en/latest/specs/www.html#response-start-send-event) or [`http.response.body`](https://asgi.readthedocs.io/en/latest/specs/www.html#response-body-send-event) ASGI event messages.
As an example, this middleware adds some fixed extra response headers:
```python
from starlette.datastructures import MutableHeaders
class ExtraResponseHeadersMiddleware:
def __init__(self, app, headers):
self.app = app
self.headers = headers
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
return await self.app(scope, receive, send)
async def send_with_extra_headers(message):
if message["type"] == "http.response.start":
headers = MutableHeaders(scope=message)
for key, value in self.headers:
headers.append(key, value)
await send(message)
await self.app(scope, receive, send_with_extra_headers)
```
See also [`asgi-logger`](https://github.com/Kludex/asgi-logger/blob/main/asgi_logger/middleware.py) for an example that inspects the HTTP response and logs a configurable HTTP access log line.
Likewise, WebSocket middleware may manipulate [`websocket.send`](https://asgi.readthedocs.io/en/latest/specs/www.html#send-send-event) ASGI event messages to inspect or alter outgoing WebSocket data.
Note that if you change the response body, you will need to update the response `Content-Length` header to match the new response body length. See [`brotli-asgi`](https://github.com/fullonic/brotli-asgi) for a complete example.
#### Passing information to endpoints
If you need to share information with the underlying app or endpoints, you may store it into the `scope` dictionary. Note that this is a convention -- for example, Starlette uses this to share routing information with endpoints -- but it is not part of the ASGI specification. If you do so, be sure to avoid conflicts by using keys that have low chances of being used by other middleware or applications.
For example, when including the middleware below, endpoints would be able to access `request.scope["asgi_transaction_id"]`.
```python
import uuid
class TransactionIDMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
scope["asgi_transaction_id"] = uuid.uuid4()
await self.app(scope, receive, send)
```
#### Cleanup and error handling
You can wrap the application in a `try/except/finally` block or a context manager to perform cleanup operations or do error handling.
For example, the following middleware might collect metrics and process application exceptions...
```python
import time
class MonitoringMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
start = time.time()
try:
await self.app(scope, receive, send)
except Exception as exc:
... # Process the exception
raise
finally:
end = time.time()
elapsed = end - start
... # Submit `elapsed` as a metric to a monitoring backend
```
See also [`timing-asgi`](https://github.com/steinnes/timing-asgi) for a full example of this pattern.
### Gotchas
#### ASGI middleware should be stateless
Because ASGI is designed to handle concurrent requests, any connection-specific state should be scoped to the `__call__` implementation. Not doing so would typically lead to conflicting variable reads/writes across requests, and most likely bugs.
As an example, this would conditionally replace the response body, if an `X-Mock` header is present in the response...
=== "✅ Do"
```python
from starlette.datastructures import Headers
class MockResponseBodyMiddleware:
def __init__(self, app, content):
self.app = app
self.content = content
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
# A flag that we will turn `True` if the HTTP response
# has the 'X-Mock' header.
# ✅: Scoped to this function.
should_mock = False
async def maybe_send_with_mock_content(message):
nonlocal should_mock
if message["type"] == "http.response.start":
headers = Headers(raw=message["headers"])
should_mock = headers.get("X-Mock") == "1"
await send(message)
elif message["type"] == "http.response.body":
if should_mock:
message = {"type": "http.response.body", "body": self.content}
await send(message)
await self.app(scope, receive, maybe_send_with_mock_content)
```
=== "❌ Don't"
```python hl_lines="7-8"
from starlette.datastructures import Headers
class MockResponseBodyMiddleware:
def __init__(self, app, content):
self.app = app
self.content = content
# ❌: This variable would be read and written across requests!
self.should_mock = False
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
async def maybe_send_with_mock_content(message):
if message["type"] == "http.response.start":
headers = Headers(raw=message["headers"])
self.should_mock = headers.get("X-Mock") == "1"
await send(message)
elif message["type"] == "http.response.body":
if self.should_mock:
message = {"type": "http.response.body", "body": self.content}
await send(message)
await self.app(scope, receive, maybe_send_with_mock_content)
```
See also [`GZipMiddleware`](https://github.com/Kludex/starlette/blob/9ef1b91c9c043197da6c3f38aa153fd874b95527/starlette/middleware/gzip.py) for a full example implementation that navigates this potential gotcha.
### Further reading
This documentation should be enough to have a good basis on how to create an ASGI middleware.
Nonetheless, there are great articles about the subject:
- [Introduction to ASGI: Emergence of an Async Python Web Ecosystem](https://florimond.dev/en/posts/2019/08/introduction-to-asgi-async-python-web/)
- [How to write ASGI middleware](https://pgjones.dev/blog/how-to-write-asgi-middleware-2021/)
## Using middleware in other frameworks
To wrap ASGI middleware around other ASGI applications, you should use the
more general pattern of wrapping the application instance:
```python
app = TrustedHostMiddleware(app, allowed_hosts=['example.com'])
```
You can do this with a Starlette application instance too, but it is preferable
to use the `middleware=<List of Middleware instances>` style, as it will:
* Ensure that everything remains wrapped in a single outermost `ServerErrorMiddleware`.
* Preserves the top-level `app` instance.
## Applying middleware to groups of routes
Middleware can also be added to `Mount` instances, which allows you to apply middleware to a group of routes or a sub-application:
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.routing import Mount, Route
routes = [
Mount(
"/",
routes=[
Route(
"/example",
endpoint=...,
)
],
middleware=[Middleware(GZipMiddleware)]
)
]
app = Starlette(routes=routes)
```
Note that middleware used in this way is *not* wrapped in exception handling middleware like the middleware applied to the `Starlette` application is.
This is often not a problem because it only applies to middleware that inspect or modify the `Response`, and even then you probably don't want to apply this logic to error responses.
If you do want to apply the middleware logic to error responses only on some routes you have a couple of options:
* Add an `ExceptionMiddleware` onto the `Mount`
* Add a `try/except` block to your middleware and return an error response from there
* Split up marking and processing into two middlewares, one that gets put on `Mount` which marks the response as needing processing (for example by setting `scope["log-response"] = True`) and another applied to the `Starlette` application that does the heavy lifting.
The `Route`/`WebSocket` class also accepts a `middleware` argument, which allows you to apply middleware to a single route:
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.routing import Route
routes = [
Route(
"/example",
endpoint=...,
middleware=[Middleware(GZipMiddleware)]
)
]
app = Starlette(routes=routes)
```
You can also apply middleware to the `Router` class, which allows you to apply middleware to a group of routes:
```python
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.routing import Route, Router
routes = [
Route("/example", endpoint=...),
Route("/another", endpoint=...),
]
router = Router(routes=routes, middleware=[Middleware(GZipMiddleware)])
```
## Third party middleware
#### [asgi-auth-github](https://github.com/simonw/asgi-auth-github)
This middleware adds authentication to any ASGI application, requiring users to sign in
using their GitHub account (via [OAuth](https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/)).
Access can be restricted to specific users or to members of specific GitHub organizations or teams.
#### [asgi-csrf](https://github.com/simonw/asgi-csrf)
Middleware for protecting against CSRF attacks. This middleware implements the Double Submit Cookie pattern, where a cookie is set, then it is compared to a csrftoken hidden form field or an `x-csrftoken` HTTP header.
#### [AuthlibMiddleware](https://github.com/aogier/starlette-authlib)
A drop-in replacement for Starlette session middleware, using [authlib's jwt](https://docs.authlib.org/en/latest/jose/jwt.html)
module.
#### [BugsnagMiddleware](https://github.com/ashinabraham/starlette-bugsnag)
A middleware class for logging exceptions to [Bugsnag](https://www.bugsnag.com/).
#### [CSRFMiddleware](https://github.com/frankie567/starlette-csrf)
Middleware for protecting against CSRF attacks. This middleware implements the Double Submit Cookie pattern, where a cookie is set, then it is compared to an `x-csrftoken` HTTP header.
#### [EarlyDataMiddleware](https://github.com/HarrySky/starlette-early-data)
Middleware and decorator for detecting and denying [TLSv1.3 early data](https://tools.ietf.org/html/rfc8470) requests.
#### [PrometheusMiddleware](https://github.com/perdy/starlette-prometheus)
A middleware class for capturing Prometheus metrics related to requests and responses, including in progress requests, timing...
#### [ProxyHeadersMiddleware](https://github.com/encode/uvicorn/blob/main/uvicorn/middleware/proxy_headers.py)
Uvicorn includes a middleware class for determining the client IP address,
when proxy servers are being used, based on the `X-Forwarded-Proto` and `X-Forwarded-For` headers. For more complex proxy configurations, you might want to adapt this middleware.
#### [RateLimitMiddleware](https://github.com/abersheeran/asgi-ratelimit)
A rate limit middleware. Regular expression matches url; flexible rules; highly customizable. Very easy to use.
#### [RequestIdMiddleware](https://github.com/snok/asgi-correlation-id)
A middleware class for reading/generating request IDs and attaching them to application logs.
#### [RollbarMiddleware](https://docs.rollbar.com/docs/starlette)
A middleware class for logging exceptions, errors, and log messages to [Rollbar](https://www.rollbar.com).
#### [StarletteOpentracing](https://github.com/acidjunk/starlette-opentracing)
A middleware class that emits tracing info to [OpenTracing.io](https://opentracing.io/) compatible tracers and
can be used to profile and monitor distributed applications.
#### [SecureCookiesMiddleware](https://github.com/thearchitector/starlette-securecookies)
Customizable middleware for adding automatic cookie encryption and decryption to Starlette applications, with
extra support for existing cookie-based middleware.
#### [TimingMiddleware](https://github.com/steinnes/timing-asgi)
A middleware class to emit timing information (cpu and wall time) for each request which
passes through it. Includes examples for how to emit these timings as statsd metrics.
#### [WSGIMiddleware](https://github.com/abersheeran/a2wsgi)
A middleware class in charge of converting a WSGI application into an ASGI one.
|