错误响应也是 API 契约
目标
区分业务异常和内部错误,并提供一致的问题响应(Problem Details)。
为什么重要
客户端发生错误时,一直在用正则表达式解析 detail 字符串。有一天句子改了,库存不足也被当成了支付重试来处理。在另一条路径上,异常字符串被原样公开,SQL 和内部路径因此泄露了出去。错误和成功响应一样,需要明确的契约。
步骤
- 在
/root/work/fa-problem-lab/service.py中,DomainError(code, message) 是 Exception 的子类,把 code 保存在 .code 中。str(异常) 就是 message。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/fa-problem-lab
test -e /root/work/fa-problem-lab/service.py || cp /opt/fixtures/ten_labs/fa-problem-lab/service.py /root/work/fa-problem-lab/service.py
cd /root/work/fa-problem-lab
-
在
/root/work/fa-problem-lab/service.py中,status_for(code) 里,missing=404,conflict=409,invalid=422,其余=500。 -
在
/root/work/fa-problem-lab/service.py中,public_message(code) 里,missing='Resource not found',conflict='State conflict',invalid='Invalid request',其余='Internal error'。 -
在
/root/work/fa-problem-lab/service.py中,request_id(value) 在值是只由 ASCII 字母、数字、下划线、连字符组成的 1–32 个字符的字符串时,原样返回,否则返回 'untracked'。 -
在
/root/work/fa-problem-lab/service.py中,problem(code, rid) 是只含 type='urn:labhub:problem:'+code、title 和 detail=public_message(code)、status=status_for(code)、request_id=request_id(rid) 的字典。 -
在
/root/work/fa-problem-lab/service.py中,response_for(code, rid) 是一个 JSONResponse:以 problem 为正文,以 status_for 为状态,以 application/problem+json 为 media_type,以规范化后的 rid 作为 X-Request-ID。 -
在
/root/work/fa-problem-lab/service.py中,install_handlers(app) 注册 DomainError 的处理器。它读取头 X-Request-ID,并针对 exc.code 返回 response_for。exc 的 message 不放进响应。 -
在
/root/work/fa-problem-lab/service.py中,create_app() 安装处理器,并在 GET /fail/{code} 中抛出 DomainError(code, 内部句子)。不过当 code=boom 时,抛出 RuntimeError。RuntimeError 的处理器返回 code=internal 的固定 500 问题响应。
参考
- 不联网、不安装软件包,在现有的 lab-dev 环境中进行。
- 每一步都在 45 秒的评分预算内运行。不要添加真实的 sleep 或网络调用。
- 评分会重新导入提交的模块,并用独立的输入和临时 DB 进行检查。请实现契约,而不要把预期值当作常量返回。
- FastAPI 官方文档 · pytest 官方文档 · Python sqlite3
- 局限:本实验的问题响应,是一个带有 type、title、status、detail、request_id 的教学用契约。它并不声称支持通用的国际化和完整的标准一致性。请求 id 是用于追踪的字符串,不是认证手段,在生产日志中也不应该把机密值原样记录下来。
给业务异常留下 code
在 /root/work/fa-problem-lab/service.py 中,DomainError(code, message) 是 Exception 的子类,把 code 保存在 .code 中。str(异常) 就是 message。
先做一次准备。已有的文件不会被覆盖。
mkdir -p /root/work/fa-problem-lab
test -e /root/work/fa-problem-lab/service.py || cp /opt/fixtures/ten_labs/fa-problem-lab/service.py /root/work/fa-problem-lab/service.py
cd /root/work/fa-problem-lab
把供机器判断的 code 与供人查看的内部信息分开。
保存后用 bash /opt/lab/checks/fa-problem-lab/01-contract.sh 确认。
把 code 映射为状态
在 /root/work/fa-problem-lab/service.py 中,status_for(code) 里,missing=404,conflict=409,invalid=422,其余=500。
不要把未知的 code 当作成功处理。
保存后用 bash /opt/lab/checks/fa-problem-lab/02-contract.sh 确认。
固定公开的文案
在 /root/work/fa-problem-lab/service.py 中,public_message(code) 里,missing='Resource not found',conflict='State conflict',invalid='Invalid request',其余='Internal error'。
即使异常字符串里含有 DB 地址或内部路径,也不公开。
保存后用 bash /opt/lab/checks/fa-problem-lab/03-contract.sh 确认。
限制请求 id
在 /root/work/fa-problem-lab/service.py 中,request_id(value) 在值是只由 ASCII 字母、数字、下划线、连字符组成的 1–32 个字符的字符串时,原样返回,否则返回 'untracked'。
为了不回显任意头部,要同时限制长度和字符集。
保存后用 bash /opt/lab/checks/fa-problem-lab/04-contract.sh 确认。
构造问题正文
在 /root/work/fa-problem-lab/service.py 中,problem(code, rid) 是只含 type='urn:labhub:problem:'+code、title 和 detail=public_message(code)、status=status_for(code)、request_id=request_id(rid) 的字典。
如果正文和 HTTP 状态互不一致,客户端就无法决定该相信哪个值。
保存后用 bash /opt/lab/checks/fa-problem-lab/05-contract.sh 确认。
让响应格式保持一致
在 /root/work/fa-problem-lab/service.py 中,response_for(code, rid) 是一个 JSONResponse:以 problem 为正文,以 status_for 为状态,以 application/problem+json 为 media_type,以规范化后的 rid 作为 X-Request-ID。
直接返回简单的字典,可能让错误也变成 200。
保存后用 bash /opt/lab/checks/fa-problem-lab/06-contract.sh 确认。
接上业务异常处理器
在 /root/work/fa-problem-lab/service.py 中,install_handlers(app) 注册 DomainError 的处理器。它读取头 X-Request-ID,并针对 exc.code 返回 response_for。exc 的 message 不放进响应。
不要把捕获异常的位置分散开,而要放在应用的公共边界上。
保存后用 bash /opt/lab/checks/fa-problem-lab/07-contract.sh 确认。
连预料之外的错误也要隐藏
在 /root/work/fa-problem-lab/service.py 中,create_app() 安装处理器,并在 GET /fail/{code} 中抛出 DomainError(code, 内部句子)。不过当 code=boom 时,抛出 RuntimeError。RuntimeError 的处理器返回 code=internal 的固定 500 问题响应。
在测试中关闭异常的重新抛出,确认真实的 500 响应的字节。
保存后用 bash /opt/lab/checks/fa-problem-lab/08-contract.sh 确认。