已关闭
[Feature]: ADX sandbox HTTP接口 未注册请求方式路由化405响应 #7
WanYihui创建于  9月10日关闭于  9月10日
WanYihui
9月10日 创建

🚀 背景描述

adx executor 的 sandbox HTTP 接口(executor/src/yr/agentexecutor/http_server.py)当前只为 GET/POST/PUT/DELETE 四种方法定义了 do_GET/do_POST/do_PUT/do_DELETE 处理器。当调用方用未注册的请求方式(如 PATCH)访问任意 sandbox URL 时,请求根本不会进入任何 do_* 方法,而是落到 Python BaseHTTPRequestHandler 的内部兜底:send_error(501, "Unsupported method ('PATCH')"),返回一段 HTML 错误页:

<!DOCTYPE HTML>
<html lang="en">
    <head><title>Error response</title></head>
    <body>
        <h1>Error response</h1>
        <p>Error code: 501</p>
        <p>Message: Unsupported method ('PATCH').</p>
    </body>
</html>

存在三个问题:

  1. 响应格式不统一:该 HTTP server 的全部业务错误路径(/v1/sandbox/* 的参数校验 400、资源不存在 404、业务失败 500)都返回 {"message": "..."} JSON(见 _sandbox_request 的 except 链),唯独方法未注册这条路径返回 HTML——调用方(AgentServer)需要处理两种响应格式,解析 HTML 易出错。
  2. 语义不准:501 Not Implemented 的含义是"服务器不认识该方法",而实际场景是"服务器认识该方法但此资源未注册该 method+URL 组合",正确语义是 405 Method Not Allowed(4xx 客户端错误)。
  3. 可扩展性受约束的放置位置错误:若按"给 Handler 枚举一组允许的方法、不在白名单的拒绝"的思路修(如定义 do_PATCH = do_HEAD = ... = 拒绝函数),方法约束就固化在了 Handler 的方法定义层——未来给 dispatch 增加 PATCH 等新请求方式的业务分支时,还要同步修改这层拦截代码,两处维护。

正确的设计原则:约束长在路由匹配上,不长在方法枚举上。"方法 + URL 组合在路由表中没有匹配项"才报错;已注册方法的新分支只改 _dispatch_sandbox 的 dispatch 逻辑,未注册方法的兜底层零改动。

设计思路

一、未注册方法统一入口 _route_or_404(不拦,先路由)

在 _ExecutorRequestHandler 中新增统一入口方法,并把未定义 do_* 的常见方法(PATCH/HEAD/OPTIONS/TRACE/CONNECT)指向它:

def _route_or_404(self) -> None:
    """未定义 HTTP 方法(PATCH/HEAD/OPTIONS/...)的统一入口。
    不做方法白名单:先按 sandbox 路由匹配 method+path,匹配不上才报错。
    未来新增请求方式时在 _dispatch_sandbox 加分支,此处无需改动。"""
    parsed = urlsplit(self.path)
    path = parsed.path
    if path == _SANDBOX_PREFIX or path.startswith(f"{_SANDBOX_PREFIX}/"):
        self._sandbox_request(self.command, path, parse_qs(parsed.query))
        return
    self._write_json(HTTPStatus.NOT_FOUND, {"message": "endpoint not found"})

do_PATCH = do_HEAD = do_OPTIONS = do_TRACE = do_CONNECT = _route_or_404

为什么这么改:这层的职责是"让未注册方法的请求也能进入统一的路由判定",而不是"拒绝它们"。请求带着真实 method(如 PATCH)进入 _sandbox_request → _dispatch_sandbox,由 dispatch 末尾的"无匹配分支"统一裁决。这样方法约束完全收敛到 dispatch 一处;加 PATCH 业务分支时只改 _dispatch_sandbox(if method == "PATCH" and sub == "files" and action == "write": ...),此层不动。类属性赋值(do_PATCH = _route_or_404)是 BaseHTTPRequestHandler 按 do_<METHOD> 命名查找处理器的标准接入点,不触碰框架内部。

二、dispatch 末尾的"无匹配分支"从 ValueError 升级为专用异常

_dispatch_sandbox 的最后一行当前是:

raise ValueError("unsupported sandbox operation")

改为专用异常类,携带 method 信息:

class SandboxMethodNotAllowedError(ValueError):
    """Method+URL combination has no registered sandbox route (HTTP 405)."""
    def __init__(self, method: str):
        super().__init__(f"method {method} not allowed for sandbox resource")
        self.method = method

# _dispatch_sandbox 末尾:
raise SandboxMethodNotAllowedError(method)

为什么这么改:现有 except 链里 ValueError 映射 400,但"方法未注册"的正确语义是 405——若继续复用 ValueError,无法区分"参数错(400)"与"method+URL 组合未注册(405)"。专用异常继承 ValueError(保持向后兼容:既有 except ValueError 兜底仍然接得住,防止漏改 except 链时退化成 500),但 _sandbox_request 的 except 链在 ValueError 分支之前优先捕获它映射 405。

三、_sandbox_request except 链加 405 映射 + _write_json 支持 extra_headers

# _sandbox_request 的 except 链中,在 FileNotFoundError 404 分支之前插入:
except SandboxMethodNotAllowedError as exc:
    self._write_json(
        HTTPStatus.METHOD_NOT_ALLOWED,
        {"message": str(exc)},
        extra_headers={"Allow": "GET, POST, PUT, DELETE"},
    )

_write_json 增加可选参数 extra_headers: Optional[dict[str, str]] = None,在写响应头时逐个 send_header;所有既有调用点不传该参数,行为不变。

为什么这么改:RFC 9110 §15.5.6 要求 405 响应必须携带 Allow 头,声明目标资源支持的方法集合。Allow: GET, POST, PUT, DELETE 描述的是当前已注册的方法集合,新增请求方式时该值随 dispatch 分支一起更新——它是"路由表现状的陈述",不是约束本身,与"约束不放方法层"的原则一致。若认为维护 Allow 枚举仍是负担,可去掉该头(RFC 仅强烈建议而非绝对强制,405 不带 Allow 仍可工作),实现时二选一,推荐带上。

四、行为对照(改后)

请求 响应
PATCH /v1/sandbox/sandboxes/{id}/files/write 405 {"message": "method PATCH not allowed for sandbox resource"} + Allow: GET, POST, PUT, DELETE
PATCH /healthz(不匹配 sandbox 资源树) 404 {"message": "endpoint not found"}
PATCH /v1/files/download(不匹配 sandbox 资源树) 404 {"message": "endpoint not found"}
GET/POST/PUT/DELETE 已注册路由 不变(200/400/404/500 各语义照旧)
完全未知的 HTTP 方法(如 FOO) 维持 Python 内部 send_error(501) HTML——该请求不经过任何 do_* 与本方案代码,不属于路由语义范畴。若未来也要 JSON 化,需重写 send_error,改动面大,本次不做
未来新增 PATCH 业务分支 仅改 _dispatch_sandbox 加 if method == "PATCH" ... 分支并更新 Allow 头;_route_or_404 与 except 链零改动

涉及到的对外API

变更:所有 sandbox 端点对未注册请求方式的错误响应

  • 请求示例(改前,任何 sandbox URL + PATCH/HEAD/OPTIONS 等未注册方法):
PATCH /v1/sandbox/sandboxes/{instance_id}/files/write?path=/tmp/t1.txt&mode=w HTTP/1.1

hello files

返回(501,HTML):

<!DOCTYPE HTML>
<html lang="en"><head><title>Error response</title></head>
<body><h1>Error response</h1><p>Error code: 501</p>
<p>Message: Unsupported method ('PATCH').</p></body></html>
  • 请求示例(改后,同请求):
PATCH /v1/sandbox/sandboxes/{instance_id}/files/write?path=/tmp/t1.txt&mode=w HTTP/1.1

hello files

返回(405,JSON):

{"message": "method PATCH not allowed for sandbox resource"}

响应头新增:Allow: GET, POST, PUT, DELETE、Content-Type: application/json。

  • POST /v1/sandbox/sandboxes、DELETE /v1/sandbox/sandboxes/{id}、POST .../execute、GET .../files/read|list|search、PUT .../files/write 的已注册请求方式行为不变:参数说明、必填性、默认值均与主设计文档(《ADX 提供沙箱 SDK API,支持工具沙箱调用.md》)一致,本次不改动任何已注册路由的请求/响应结构。

新增内部接口(非 HTTP):SandboxMethodNotAllowedError(ValueError) 异常类,仅 http_server.py 内部使用,无外部消费方。

与其他模块的相关性描述

  • yr.agentexecutor.runtime(AgentExecutorRuntime):无影响。仅消费 ExecutorHTTPServer 的 start()/stop(),HTTP 内部的错误响应格式变化对其透明。
  • yr.agentexecutor.sandbox(SDK 副本):无影响。SDK 门面层不感知 HTTP 方法路由,SandboxMethodNotAllowedError 在 http_server 层抛出与捕获,不会穿透到 SDK。
  • yr.agentexecutor.sandbox_manager:无影响。405 的判定发生在 _dispatch_sandbox 分支匹配阶段,不触达 manager 的任何方法。
  • 调用方(Jiuwen AgentServer / 测试脚本):影响。未注册方法的响应从 501 HTML 变为 405 JSON——若调用方有对 501/HTML 的容错逻辑需排查(当前测试脚本无此依赖)。
  • frontend / functionsystem:无影响。sandbox HTTP 接口是 adx executor 进程内行为,不经过这两个组件。
  • tests/executor/test_http_server.py:影响。需新增 405/404 路由化用例(见测试设计);既有用例不受影响(全部使用已注册方法)。

测试设计与测试计划

单元测试(tests/executor/test_http_server.py 新增,注入 _LocalSandbox 的既有 fixture 复用):

  • test_patch_sandbox_url_returns_405_json:对已创建的测试沙箱发 PATCH .../files/write,断言状态码 405、响应体为 JSON 且 message 含 "not allowed for sandbox resource"、响应头含 Allow: GET, POST, PUT, DELETE、Content-Type: application/json(验证设计思路一+二+三的完整链路)。
  • test_head_sandbox_url_returns_405_json:HEAD 方法同 URL,同样断言 405 JSON(验证 do_HEAD 等其余未注册方法走同一入口)。
  • test_patch_non_sandbox_path_returns_404_json:PATCH /healthz 断言 404 + {"message": "endpoint not found"}(验证不匹配 sandbox 资源树时走 404 而非 405)。
  • test_registered_methods_unaffected:GET/POST/PUT/DELETE 各发一个既有用例(如 healthz GET 200、create POST 200),断言行为不变(回归保护)。

无法单测覆盖的路径:完全未知方法(如 FOO)的 501 HTML 走 Python http.server 模块内部的 parse_request → send_error 路径,不经过任何 do_* 与本方案代码,单测仅能断言"现状如此",不纳入本方案验证范围。

集成测试计划(真实 adx 镜像内,复用 /home/wyh/test/adx-refractor/files.sh 反向用例区新增):

  • patch_write_method_not_allowed:-X PATCH "${FILES_URL}/write?path=/tmp/t1.txt&mode=w" -d 'x',期望 405,message 匹配 not allowed for sandbox resource。
  • patch_wrong_path_404:-X PATCH "${BASE_URL}/healthz",期望 404,message 匹配 endpoint not found。
  • 部署顺序:先部署含本方案的 executor 包,再更新测试脚本并全量重跑 run-all.sh,确认既有用例无回归。

其他信息

感谢您的贡献 🎉!

likedislike
openJiuwen-bot成员
9月10日 评论:

欢迎来到 openJiuwen 社区

Hey @pikachuwan , 感谢你对社区的贡献.

机器人使用手册

有关指令的使用,可以点击 此处 查看详情。开发人员可以在每个PR或Issue下方评论特定指令来触发机器人任务。

likedislike
WWanYihui
9月10日 关联了pull request:feat: ADX sandbox HTTP interface returns 405 response for unregistered request method routing
songlichengsonglicheng成员
9月10日 关闭了 issue
songlichengsonglicheng成员
9月10日 issue状态由 TODO 改变为 DONE