Skip to content

Commit d9354ba

Browse files
OhYeeclaude
andcommitted
refactor(server): STS 中间件改为纯 ASGI 实现
按 PR review 建议,将 StsRefreshMiddleware 从 BaseHTTPMiddleware 改为纯 ASGI 中间件(__call__ 包裹 await self.app(scope, receive, send),Headers(scope=scope) 取头,with use_sts_from_headers 在 app 整体结束后复位)。 收益:overlay 与请求同任务、同生命周期,对 endpoint / StreamingResponse body / run_in_threadpool 同步处理器 / **响应后的 background task** 全程可见;避免 BaseHTTPMiddleware 的额外 task/stream 包装及其流式/断连/异常传播的已知坑。 逻辑不变(复用 use_sts_from_headers)。新增 background-task 回归测试。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: OhYee <oyohyee@oyohyee.com>
1 parent 037b250 commit d9354ba

2 files changed

Lines changed: 61 additions & 18 deletions

File tree

agentrun/server/sts_middleware.py

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@
55
(:mod:`agentrun.utils.credential_context`),使本次请求内所有 ``Config`` /
66
client 的取证都拿到最新 STS;请求结束时复位。
77
8-
为何用中间件 / Why middleware:
9-
在 ``call_next`` 之前 ``set`` 的 contextvar 会被拷贝进下游 endpoint 子任务,
10-
并对 ``StreamingResponse`` 的 body 生成器、``run_in_threadpool`` 中的同步
11-
处理器同样可见,因此能覆盖整条请求(含 SSE 流)。已在 Starlette 0.48 +
12-
FastAPI 0.118 上实测验证。
8+
为何用纯 ASGI 中间件 / Why a plain ASGI middleware:
9+
本中间件只做一件事——设置 / 复位一个 contextvar,故用**纯 ASGI** 实现
10+
(``__call__`` 包裹 ``await self.app(scope, receive, send)``),而非
11+
``BaseHTTPMiddleware``。优点:overlay 与请求**同任务、同生命周期**——对
12+
endpoint、``StreamingResponse`` 的 body、``run_in_threadpool`` 的同步处理器、
13+
以及**响应后的 background task** 全程可见,并在 app 完全结束后于 ``finally``
14+
复位;同时避免 ``BaseHTTPMiddleware`` 的额外 task/stream 包装及其在流式 /
15+
断连 / 异常传播上的已知坑。
16+
17+
注入时机与有效期 / Injection lifetime:
18+
STS 在请求入口注入一份、整条请求固定不变(头只到达一次)。流式响应全程使用
19+
这份入口 STS;仅当**单条请求持续时间超过 STS 有效期**时才会中途过期——属按
20+
请求头注入模型的固有上限,正常请求 / 流远短于有效期,不受影响。
1321
1422
头名可配置 / Configurable header names:
1523
构造参数 > 环境变量 > 默认值(``x-fc-*``)。头名大小写不敏感。
@@ -32,10 +40,8 @@
3240
import os
3341
from typing import Optional
3442

35-
from starlette.middleware.base import BaseHTTPMiddleware
36-
from starlette.requests import Request
37-
from starlette.responses import Response
38-
from starlette.types import ASGIApp
43+
from starlette.datastructures import Headers
44+
from starlette.types import ASGIApp, Receive, Scope, Send
3945

4046
from agentrun.utils.credential_context import use_sts_from_headers
4147

@@ -52,8 +58,8 @@ def _detect_enabled() -> bool:
5258
return flag.strip().lower() in ("1", "true", "yes", "on")
5359

5460

55-
class StsRefreshMiddleware(BaseHTTPMiddleware):
56-
"""从请求头解析最新 STS 并注入请求级 overlay。"""
61+
class StsRefreshMiddleware:
62+
"""纯 ASGI 中间件:从请求头解析最新 STS 并注入请求级 overlay。"""
5763

5864
def __init__(
5965
self,
@@ -64,7 +70,7 @@ def __init__(
6470
access_key_secret_header: Optional[str] = None,
6571
security_token_header: Optional[str] = None,
6672
) -> None:
67-
super().__init__(app)
73+
self.app = app
6874
# enabled=None 时按环境变量决定(默认启用,
6975
# AGENTRUN_STS_REFRESH_ENABLED 设为假值时关闭)。
7076
self._enabled = _detect_enabled() if enabled is None else enabled
@@ -73,16 +79,21 @@ def __init__(
7379
self._sk_header = access_key_secret_header
7480
self._sts_header = security_token_header
7581

76-
async def dispatch(self, request: Request, call_next) -> Response:
77-
if not self._enabled:
78-
return await call_next(request)
82+
async def __call__(
83+
self, scope: Scope, receive: Receive, send: Send
84+
) -> None:
85+
if scope["type"] != "http" or not self._enabled:
86+
await self.app(scope, receive, send)
87+
return
7988

80-
# 直接复用公开上下文管理器:解析请求头 -> 注入 overlay -> 退出复位
89+
# 复用公开上下文管理器:解析请求头 -> 注入 overlay -> app 整体跑完后复位
8190
# 三元组不齐全时 use_sts_from_headers 不覆盖(透传),与手动注入完全一致。
91+
# 纯 ASGI:overlay 在同一任务内对 endpoint / 流式 body / 同步处理器 /
92+
# 响应后的 background task 全程可见,``with`` 在 app 结束后才退出复位。
8293
with use_sts_from_headers(
83-
request.headers,
94+
Headers(scope=scope),
8495
access_key_id_header=self._ak_header,
8596
access_key_secret_header=self._sk_header,
8697
security_token_header=self._sts_header,
8798
):
88-
return await call_next(request)
99+
await self.app(scope, receive, send)

tests/unittests/test_sts_refresh.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,3 +509,35 @@ def test_public_exports_available():
509509
):
510510
assert hasattr(agentrun, name), f"{name} not exported"
511511
assert name in agentrun.__all__, f"{name} missing from __all__"
512+
513+
514+
def test_middleware_background_task_sees_overlay():
515+
"""纯 ASGI:响应后的 background task 仍能读到请求级 overlay。
516+
517+
BaseHTTPMiddleware 下 background task 可能在 overlay 复位后才运行;纯 ASGI
518+
中间件持有 overlay 直到 app(含 background)整体结束,故覆盖到位。
519+
"""
520+
from fastapi import FastAPI
521+
from fastapi.responses import JSONResponse
522+
from fastapi.testclient import TestClient
523+
from starlette.background import BackgroundTask
524+
525+
from agentrun.server.sts_middleware import StsRefreshMiddleware
526+
527+
captured: dict = {}
528+
529+
def _bg():
530+
cfg = Config()
531+
captured["ak"] = cfg.get_access_key_id()
532+
captured["sts"] = cfg.get_security_token()
533+
534+
app = FastAPI()
535+
app.add_middleware(StsRefreshMiddleware, enabled=True)
536+
537+
@app.get("/bg")
538+
async def _ep():
539+
return JSONResponse({"ok": True}, background=BackgroundTask(_bg))
540+
541+
client = TestClient(app)
542+
client.get("/bg", headers=_HEADERS)
543+
assert captured == {"ak": "H_AK", "sts": "H_STS"}, captured

0 commit comments

Comments
 (0)