03 引擎反向代理与头清理 本节摘要:服务端作为「唯一中枢」,不仅要处理自己的业务,还要把对底层引擎的请求透明地代理过去。本节讲清这套反向代理:服务端如何识别「该代理给引擎」的请求、如何剥除敏感头、如何注入工作区凭据、如何清理响应。这套「透明反代」让引擎感觉不到中间层存在,同时让服务端保持对访问的控制。 一、为什么服务端要代理引擎 回忆第 2 章拓扑:所有对引擎的访问都经服务端代理,不直接连引擎。为什么? 统一鉴权:服务端代理时先鉴权(第 4 章),保证只有授权请求到引擎。 统一治理:服务端可以加额外逻辑(如 viewer 强制只读,第 4 章)。 隐藏引擎细节:客户端不用知道引擎的端口/凭据,只认服务端。 所以「访问引擎」=「访问服务端的某路径,服务端转给引擎」。这就是反向代理。
本节摘要:服务端作为「唯一中枢」,不仅要处理自己的业务,还要把对底层引擎的请求透明地代理过去。本节讲清这套反向代理:服务端如何识别「该代理给引擎」的请求、如何剥除敏感头、如何注入工作区凭据、如何清理响应。这套「透明反代」让引擎感觉不到中间层存在,同时让服务端保持对访问的控制。
回忆第 2 章拓扑:所有对引擎的访问都经服务端代理,不直接连引擎。为什么?
所以「访问引擎」=「访问服务端的某路径,服务端转给引擎」。这就是反向代理。
服务端怎么知道一个请求「该自己处理」还是「该代理给引擎」?靠路径模式。某些路径(如 /引擎/* 或工作区挂载点)被识别为「引擎相关」,走代理:
请求进来 │ ▼ 解析路径 │ ├─ 匹配「引擎挂载点」(如 /workspace/:id/引擎/*) │ └─ 走反向代理 │ └─ 其他路径 └─ 走服务端自己的路由匹配
这种「路径分流」让引擎请求和服务端请求在同一端口共存,各走各的。
代理时一个关键动作——剥除敏感头。客户端发给服务端的请求,可能带着「给服务端的」头(如授权头、主机头)。这些头不能原样转发给引擎——引擎可能误解,或泄露信息。
所以服务端代理时,会剥除这些:
客户端请求: { 头: { Authorization, Host, Origin, ... }, ... } │ ▼ 服务端代理:剥除敏感头 │ 转发给引擎: { 头: { (剥掉了 Authorization/Host/Origin) } }
剥除后,服务端注入工作区自己的引擎凭据。因为引擎不认客户端给服务端的凭据,它认「工作区配置的引擎凭据」:
剥除后: { 头: {} } │ ▼ 服务端注入工作区凭据 │ ├─ x-opencode-directory: 工作区目录标识 └─ 工作区自带的 authHeader │ 转发给引擎: { 头: { x-opencode-directory, authHeader } }
这样引擎收到的请求,就像「工作区直接发的」——带着正确的目录标识和凭据。引擎完全感觉不到中间有个服务端。
💡 透明反代的精髓:剥除客户端头 + 注入工作区头,让引擎「以为」是工作区直接在调它。这就是「透明」——中间层存在,但对引擎不可见。
代理时请求体也要正确处理:
有些特殊路径(如与会话命令相关)走「fire-and-forget」——发了不管结果(因为是单向通知)。
引擎返回响应后,服务端代理也要清理响应头——特别是「逐跳头(hop-by-hop)」:
引擎响应: { 头: { content-encoding, transfer-encoding, content-length, ... } } │ ▼ 服务端清理逐跳头 │ 返回客户端: { 头: { (清理了逐跳头) } }
逐跳头(如 content-encoding、transfer-encoding、content-length)是「单跳专用」的——客户端到服务端一跳、服务端到引擎一跳,各自处理。如果不清理,客户端可能误用引擎那跳的头,导致解码错误。
代理时还有一个与第 4 章权限的衔接点——viewer 强制只读。代理前,服务端检查令牌 scope:
代理请求前: │ ▼ 检查 scope │ ├─ viewer 且方法非 GET/HEAD ──► 拒绝(403,只读) │ └─ 其他 ──► 继续代理
这就是第 4 章说的「viewer 在反代层把关」——它不是前端隐藏按钮,而是在服务端代理引擎请求时强制只读。这是真安全的权限治理。
引擎反代讲清了,最后一节讲「未命中」时怎么兜底——静态 UI 与跨域。