Skip to content

五个可复用的中间件模式 ​

Robyn 的 before_request / after_request 看着简单,但组合起来能覆盖绝大多数横切需求。这篇给出五个能直接搬进项目的写法,以及每个坑在哪。

开始前先记住两条语义(完整说明见 中间件):

  1. before_request 接收并返回 request,可以修改它
  2. 一旦某个 before_request 返回了 Response,后续中间件和路由处理器都会被跳过,但 after_request 仍会执行

第二点是实现鉴权短路的基础。

一、请求 ID:让日志串起来 ​

python
import uuid
from robyn import Request, Response

@app.before_request("/*")
async def attach_request_id(request: Request):
    request.headers.set("x-request-id", request.headers.get("x-request-id") or uuid.uuid4().hex)
    return request

@app.after_request("/*")
def echo_request_id(request: Request, response: Response):
    response.headers.set("x-request-id", request.headers.get("x-request-id"))
    return response

注意 after_request 可以同时接收 request 和 response——这是把请求侧信息写回响应头的唯一途径。上游如果已经有 x-request-id(比如 CDN 注入的)就直接透传,不要覆盖,否则链路会断。

二、鉴权:用返回 Response 来短路 ​

python
def parse_token(request):
    auth = request.headers.get("authorization")
    return auth.removeprefix("Bearer ").strip() if auth else None

@app.before_request("/api/*")
async def require_auth(request: Request):
    if request.url.path.startswith("/api/public"):
        return request                      # 放行白名单
    token = parse_token(request)
    if not token or not verify(token):
        return Response(status_code=401, body='{"error": "unauthorized"}',
                        headers={"Content-Type": "application/json"})
    request.headers.set("x-user-id", get_user_id(token))
    return request

把解析出的用户 ID 写进 request headers 传给下一层,比在处理器里再解一次 token 干净得多。

如果你的鉴权来自 Robyn 自带的身份认证体系,可以直接用 auth_required=True 配合 app.authentication_handlers(见 身份验证),它本质上也是一个 before_request 钩子,可以与自定义中间件叠加使用。

三、限流:别忘了多进程 ​

单进程版本用内存即可:

python
import time
from collections import defaultdict, deque

WINDOW, LIMIT = 60, 100
_hits: dict[str, deque] = defaultdict(deque)

@app.before_request("/api/*")
async def rate_limit(request: Request):
    now = time.time()
    q = _hits[request.ip_addr]
    while q and now - q[0] > WINDOW:
        q.popleft()
    if len(q) >= LIMIT:
        return Response(status_code=429, body="too many requests",
                        headers={"Retry-After": str(int(WINDOW))})
    q.append(now)
    return request

但这个版本在 --processes > 1 下会失效:每个进程各自计数,实际放行量是 LIMIT × 进程数,而且同一个 IP 落到不同进程会得到忽紧忽松的体验。生产请换成 Redis:

python
@app.before_request("/api/*")
async def rate_limit(request: Request):
    key = f"rl:{request.ip_addr}:{int(time.time()) // WINDOW}"
    count = await redis.incr(key)
    if count == 1:
        await redis.expire(key, WINDOW)
    if count > LIMIT:
        return Response(status_code=429, body="too many requests")
    return request

把 request.url.path 一起拼进 key,就能把窗口从"每 IP"细化到"每 IP 每接口",避免某个重接口吃光配额。

四、响应统一包装与耗时统计 ​

python
import time

@app.before_request("/*")
async def start_timer(request: Request):
    request.headers.set("x-start-time", str(time.perf_counter()))
    return request

@app.after_request("/*")
def add_timing(request: Request, response: Response):
    start = float(request.headers.get("x-start-time") or 0)
    response.headers.set("x-response-time-ms", f"{(time.perf_counter() - start) * 1000:.2f}")
    response.headers.set("x-powered-by", "robyn")
    return response

高频场景下别在中间件里 print(),同步 IO 会拖慢整条链路;把日志丢进队列异步消费,或直接交给 stdout 采集。

五、全局异常兜底 ​

python
from robyn import Response

@app.exception
def handle_exception(error):
    return Response(status_code=500, body='{"error": "internal server error"}',
                    headers={"Content-Type": "application/json"})

生产环境不要回显 error 的原文,否则异常堆栈会直接暴露给客户端——开发时用 --dev 看就行。

六、组合顺序与性能建议 ​

  • before_request 按注册顺序执行,鉴权类要放在最前,日志类次之

  • 中间件是每个请求都跑的,里面做任何同步 IO 都会被放大 N 倍

  • 路径匹配支持 /* 通配;不需要全局生效时请写具体的路由前缀,减少无谓调用

  • 中间件无法拿到最终 JSON Body 做签名/加密这类需求时,改在单个处理器里显式处理更清晰

  • 中间件

  • 身份验证

  • 异常

  • Robyn 性能调优:从默认值到真实压测

基于 MIT 许可发布 · 隐私政策