55(:mod:`agentrun.utils.credential_context`),使本次请求内所有 ``Config`` /
66client 的取证都拿到最新 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-*``)。头名大小写不敏感。
3240import os
3341from 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
4046from 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 )
0 commit comments