中间件
Starlette 包含几个中间件类,用于添加应用于整个应用程序的行为。这些都是作为标准的 ASGI 中间件类实现的,并且可以应用于 Starlette 或任何其他 ASGI 应用程序。
使用中间件
Starlette 应用程序类允许您以一种确保其始终被异常处理程序包裹的方式包含 ASGI 中间件。
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)
默认情况下,每个 Starlette 应用程序会自动包含两段中间件:
ServerErrorMiddleware
- 确保应用程序异常可以返回自定义的 500 页面,或在 DEBUG 模式下显示应用程序的跟踪信息。这始终是最外层的中间件层。ExceptionMiddleware
- 添加异常处理程序,以便特定类型的预期异常情况可以与处理程序函数相关联。例如,在端点内引发HTTPException(status_code=404)
将最终呈现自定义的 404 页面。
中间件是从上到下进行评估的,因此在我们的示例应用程序中,执行流程将如下所示:
- 中间件
ServerErrorMiddleware
TrustedHostMiddleware
HTTPSRedirectMiddleware
ExceptionMiddleware
- 路由选择
- 端点
在 Starlette 包中可使用以下中间件实现:
CORS 中间件
为向外发送的响应添加适当的 CORS 标头,以允许来自浏览器的跨源请求。
CORSMiddleware 实现所使用的默认参数默认是具有限制性的,因此您需要明确地启用特定的源、方法或标头,以便浏览器在跨域上下文中被允许使用它们。
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)
以下论点得到支持:
allow_origins
- 应被允许进行跨源请求的源列表。例如['https://example.org', 'https://www.example.org']
。您可以使用['*']
来允许任何源。allow_origin_regex
- 一个用于匹配应被允许进行跨源请求的源的正则表达式字符串。例如:'https://.*\.example\.org'
。allow_methods
- 一份应允许用于跨源请求的 HTTP 方法的列表。默认为['GET']
。您可以使用['*']
来允许所有标准方法。allow_headers
- 一份应支持用于跨源请求的 HTTP 请求标头的列表。默认为[]
。您可以使用['*']
来允许所有标头。对于 CORS 请求,Accept
、Accept-Language
、Content-Language
和Content-Type
标头始终是被允许的。allow_credentials
- 表明应支持跨源请求的 Cookie。默认为False
。此外,为允许凭证,allow_origins
、allow_methods
和allow_headers
不能设置为['*']
,它们都必须明确指定。expose_headers
- 表明应使浏览器能够访问的任何响应标头。默认为[]
。max_age
- 为浏览器缓存 CORS 响应设置以秒为单位的最长时间。默认为600
。
中间件响应两种特定类型的 HTTP 请求……
跨域资源共享(CORS)预检请求
这些是带有 Origin
和 Access-Control-Request-Method
标头的任何 OPTIONS
请求。在这种情况下,中间件将拦截传入的请求,并使用适当的 CORS 标头进行响应,以及出于信息目的的 200 或 400 响应。
简单请求
任何带有 Origin
标头的请求。在这种情况下,中间件将像往常一样传递请求,但会在响应中包含适当的 CORS 标头。
会话中间件
添加基于签名 Cookie 的 HTTP 会话。会话信息可读但不可修改。
使用 request.session
字典接口访问或修改会话数据。
以下论点得到支持:
secret_key
- 应该是一个随机字符串。session_cookie
- 默认值为“会话”。max_age
- 会话过期时间(以秒为单位)。默认为 2 周。如果设置为None
,则 Cookie 的有效期将与浏览器会话持续时间相同。same_site
- SameSite 标志可防止浏览器在跨站请求时发送会话 Cookie。默认值为'lax'
。path
- 为会话 Cookie 设置的路径。默认为'/'
。https_only
- 表示应设置安全标志(仅可与 HTTPS 一起使用)。默认为False
。-
domain
- 用于在子域或跨域之间共享 Cookie 的 Cookie 域。浏览器将域默认设置为设置 Cookie 的同一主机,不包括子域(参考)。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)
HTTPS 重定向中间件
强制要求所有传入请求必须为 https
或 wss
。任何传入到 http
或 ws
的请求将被重定向到安全方案。
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)
对于此中间件类,没有配置选项。
可信主机中间件
强制要求所有传入请求具有正确设置的 Host
标头,以防范 HTTP 主机标头攻击。
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)
以下论点得到支持:
allowed_hosts
- 应被允许作为主机名的域名列表。诸如*.example.com
这样的通配符域名可用于匹配子域。要允许任何主机名,可使用allowed_hosts=["*"]
或省略中间件。www_redirect
- 如果设置为 True,对允许主机的非 www 版本的请求将被重定向到其 www 对应版本。默认为True
。
如果传入的请求未正确验证,则将发送 400 响应。
GZip 中间件
处理任何在 Accept-Encoding
头中包含 "gzip"
的请求的 GZip 响应。
中间件将处理标准响应和流响应。
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)
以下论点得到支持:
minimum_size
- 对于小于此最小字节大小的响应,不要进行 GZip 压缩。默认值为500
。compresslevel
- 在 GZip 压缩期间使用。它是一个从 1 到 9 的整数。默认值为9
。值越低,压缩速度越快,但文件大小越大;值越高,压缩速度越慢,但文件大小越小。
中间件不会对已经设置了 Content-Encoding
的响应进行 GZip 压缩,以防止它们被编码两次。
BaseHTTP 中间件
一个允许您针对请求/响应接口编写 ASGI 中间件的抽象类。
使用情况
要使用 BaseHTTPMiddleware
实现一个中间件类,您必须重写 async def dispatch(request, call_next)
方法。
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)
如果您想为中间件类提供配置选项,您应该重写 __init__
方法,确保第一个参数是 app
,任何其余参数是可选的关键字参数。如果您这样做,请确保在实例上设置 app
属性。
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)
中间件类不应在 __init__
方法之外修改其状态。相反,您应该将任何状态保留在 dispatch
方法的本地,或者明确地传递它,而不是改变中间件实例的状态。
局限性
目前, BaseHTTPMiddleware
存在一些已知的限制:
- 使用
BaseHTTPMiddleware
将防止对contextlib.ContextVar
的更改向上传播。也就是说,如果您在端点中为ContextVar
设置一个值,并尝试从中继件中读取它,您会发现该值与您在端点中设置的值不同(有关此行为的示例,请参阅此测试)。
为克服这些限制,请使用纯 ASGI 中间件,如下所示。
纯 ASGI 中间件
ASGI 规范使得可以直接使用 ASGI 接口实现 ASGI 中间件,作为一系列 ASGI 应用程序,依次调用下一个。事实上,Starlette 附带的中间件类就是这样实现的。
这种较低级别的方法对行为提供了更大的控制,并增强了跨框架和服务器的互操作性。它还克服了 BaseHTTPMiddleware
的局限性。
编写纯 ASGI 中间件
创建 ASGI 中间件最常见的方法是使用一个类。
class ASGIMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
await self.app(scope, receive, send)
上述中间件是最基本的 ASGI 中间件。它在其构造函数中接收一个父 ASGI 应用程序作为参数,并实现一个 async __call__
方法,该方法会调用该父应用程序。
一些实现方式,如 asgi-cors
,使用一种替代风格,运用函数:
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
无论如何,ASGI 中间件必须是可调用对象,需接受三个参数: scope
、 receive
和 send
。
scope
是一个保存有关连接信息的字典,其中scope["type"]
可能是:"http"
:用于 HTTP 请求。"websocket"
:用于 WebSocket 连接。"lifespan"
:用于 ASGI 生命周期消息。
receive
和send
可用于与 ASGI 服务器交换 ASGI 事件消息——下文将对此进行更多介绍。这些消息的类型和内容取决于范围类型。在 ASGI 规范中了解更多信息。
使用纯 ASGI 中间件
纯 ASGI 中间件可以像任何其他中间件一样使用:
from starlette.applications import Starlette
from starlette.middleware import Middleware
from .middleware import ASGIMiddleware
routes = ...
middleware = [
Middleware(ASGIMiddleware),
]
app = Starlette(..., middleware=middleware)
另见使用中间件。
类型注释
有两种为中间件添加注释的方法:使用 Starlette 本身或 asgiref
。
-
使用 Starlette:适用于大多数常见用例。
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)
-
使用
asgiref
: 以实现更严格的类型提示。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)
常见模式
仅处理某些请求
ASGI 中间件可根据 scope
的内容应用特定行为。
例如,若仅处理 HTTP 请求,可这样写……
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)
同样地,仅 WebSocket 的中间件会在 scope["type"] != "websocket"
上进行防护。
中间件还可能根据请求方法、URL、标头等的不同而有不同的表现。
复用 Starlette 组件
Starlette 提供了几种数据结构,这些数据结构接受 ASGI scope
、 receive
和/或 send
参数,使您能够在更高的抽象级别上工作。此类数据结构包括 Request
、 Headers
、 QueryParams
、 URL
等。
例如,您可以实例化一个 Request
以更轻松地检查 HTTP 请求:
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)
您还可以重复使用响应,它们也是 ASGI 应用程序。
发送热切回应
检查连接 scope
可让您有条件地调用到不同的 ASGI 应用程序。一个用例可能是在不调用应用程序的情况下发送响应。
例如,此中间件使用字典根据请求的路径执行永久重定向。如果您需要重构路由 URL 模式,这可用于实现对旧版 URL 的持续支持。
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)
示例用法看起来会是这样:
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)
检查或修改请求
请求信息可以通过操作 scope
来访问或更改。有关此模式的完整示例,请参阅 Uvicorn 的 ProxyHeadersMiddleware
,它在前端代理后面提供服务时会检查和调整 scope
。
此外,包装 receive
ASGI 可调用对象使您能够通过操作 http.request
ASGI 事件消息来访问或修改 HTTP 请求正文。
例如,此中间件会计算并记录传入请求主体的大小……
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)
同样地,WebSocket 中间件可能会操作 websocket.receive
ASGI 事件消息,以检查或更改传入的 WebSocket 数据。
对于更改 HTTP 请求正文的示例,请参阅 msgpack-asgi
。
检查或修改响应
将 send
ASGI 可调用对象进行包装,可让您检查或修改底层应用程序发送的 HTTP 响应。要实现此目的,请对 http.response.start
或 http.response.body
ASGI 事件消息做出反应。
例如,此中间件添加了一些固定的额外响应标头:
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)
另请参阅 asgi-logger
,其中有一个检查 HTTP 响应并记录可配置的 HTTP 访问日志行的示例。
同样地,WebSocket 中间件可能会操作 websocket.send
ASGI 事件消息,以检查或更改传出的 WebSocket 数据。
请注意,如果您更改响应正文,则需要更新响应 Content-Length
标头以匹配新的响应正文长度。有关完整示例,请参阅 brotli-asgi
。
将信息传递到端点
如果您需要与基础应用程序或端点共享信息,可以将其存储到 scope
字典中。请注意,这是一种约定——例如,Starlette 使用此方法与端点共享路由信息——但它不是 ASGI 规范的一部分。如果您这样做,请确保使用不太可能被其他中间件或应用程序使用的键来避免冲突。
例如,当包含以下中间件时,端点将能够访问 request.scope["asgi_transaction_id"]
。
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)
清理和错误处理
您可以将应用程序包装在一个 try/except/finally
块或上下文管理器中,以执行清理操作或进行错误处理。
例如,以下中间件可能会收集指标并处理应用程序异常……
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
另请参阅 timing-asgi
以获取此模式的完整示例。
潜在的问题
ASGI 中间件应该是无状态的
因为 ASGI 旨在处理并发请求,任何特定于连接的状态都应限定在 __call__
实现范围内。如果不这样做,通常会导致跨请求的变量读/写冲突,并且很可能会出现错误。
例如,如果响应中存在 X-Mock
标头,则此操作将有条件地替换响应正文……
✅ 做
```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)
```
❌ 不要
```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)
```
另请参阅 GZipMiddleware
以获取完整的示例实现,该实现可解决此潜在问题。
进一步阅读
此文档应足以作为如何创建 ASGI 中间件的良好基础。
然而,有很多关于这个主题的优秀文章:
在其他框架中使用中间件
要将 ASGI 中间件围绕其他 ASGI 应用程序进行包装,您应该使用更通用的模式来包装应用程序实例:
app = TrustedHostMiddleware(app, allowed_hosts=['example.com'])
您也可以对 Starlette 应用程序实例进行此操作,但最好使用 middleware=<List of Middleware instances>
风格,因为它将:
- 确保一切都包裹在一个最外层的
ServerErrorMiddleware
中。 - 保留顶级的
app
实例。
将中间件应用于路由组
中间件也可以添加到 Mount
实例中,这使您能够将中间件应用于一组路由或子应用程序:
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)
请注意,以这种方式使用的中间件并未像应用于 Starlette
应用程序的中间件那样被包裹在异常处理中间件中。这通常不是问题,因为它仅适用于检查或修改 Response
的中间件,即便如此,您可能也不想将此逻辑应用于错误响应。如果您确实希望仅在某些路由上将中间件逻辑应用于错误响应,则有几个选项:
- 在
Mount
上添加一个ExceptionMiddleware
- 在您的中间件中添加一个
try/except
块,并从那里返回错误响应 - 将标记和处理拆分为两个中间件,一个放置在
Mount
上,将响应标记为需要处理(例如通过设置scope["log-response"] = True
),另一个应用于Starlette
应用程序来完成繁重的工作。
Route
/ WebSocket
类还接受一个 middleware
参数,该参数允许您将中间件应用于单个路由:
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)
您还可以将中间件应用于 Router
类,这使您能够将中间件应用于一组路由:
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)])
第三方中间件
asgi-auth-github )
此中间件为任何 ASGI 应用程序添加身份验证,要求用户使用其 GitHub 帐户(通过 OAuth)登录。访问权限可以限制为特定用户或特定 GitHub 组织或团队的成员。
asgi-csrf
用于防范 CSRF 攻击的中间件。此中间件实现了双重提交 Cookie 模式,其中设置一个 Cookie,然后将其与 csrftoken 隐藏表单字段或 x-csrftoken
HTTP 标头进行比较。
Authlib 中间件
Starlette 会话中间件的直接替代品,使用 authlib 的 jwt 模块。
Bugsnag 中间件
一个用于将异常记录到 Bugsnag 的中间件类。
CSRF 中间件
用于防范 CSRF 攻击的中间件。此中间件实现了双重提交 Cookie 模式,其中设置一个 Cookie,然后将其与一个 x-csrftoken
HTTP 标头进行比较。
早期数据中间件
用于检测和拒绝 TLSv1.3 早期数据请求的中间件和装饰器。
prometheus中间件
一个用于捕获与请求和响应相关的 Prometheus 指标的中间件类,包括正在进行的请求、时间等……
代理标头中间件
Uvicorn 包含一个中间件类,用于在使用代理服务器时根据 X-Forwarded-Proto
和 X-Forwarded-For
标头确定客户端 IP 地址。对于更复杂的代理配置,您可能需要调整此中间件。
速率限制中间件
一种速率限制中间件。正则表达式匹配 URL;灵活的规则;高度可定制。非常易于使用。
RequestId 中间件
用于读取/生成请求 ID 并将其附加到应用程序日志的中间件类。
Rollbar 中间件
一个用于将异常、错误和日志消息记录到 Rollbar 的中间件类。
StarletteOpentracing
一个中间件类,可向与 OpenTracing.io 兼容的跟踪器发送跟踪信息,并可用于分析和监控分布式应用程序。
安全 Cookie 中间件
用于向 Starlette 应用程序添加自动 Cookie 加密和解密的可定制中间件,对现有的基于 Cookie 的中间件提供额外支持。
定时中间件
一个中间件类,用于为通过它的每个请求发出计时信息(CPU 时间和挂钟时间)。包括如何将这些计时作为 statsd 指标发出的示例。
WSGIMiddleware
一个负责将 WSGI 应用程序转换为 ASGI 应用程序的中间件类。