TT Lab
开始
学习 学习路径 课程

FastAPI — 类型就是契约

错误响应也是 API 契约

在 TT Lab 中继续学习

目标

区分业务异常和内部错误,并提供一致的问题响应(Problem Details)。

为什么重要

客户端发生错误时,一直在用正则表达式解析 detail 字符串。有一天句子改了,库存不足也被当成了支付重试来处理。在另一条路径上,异常字符串被原样公开,SQL 和内部路径因此泄露了出去。错误和成功响应一样,需要明确的契约。

步骤

  1. 在 /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
  1. 在 /root/work/fa-problem-lab/service.py 中,status_for(code) 里,missing=404,conflict=409,invalid=422,其余=500。

  2. 在 /root/work/fa-problem-lab/service.py 中,public_message(code) 里,missing='Resource not found',conflict='State conflict',invalid='Invalid request',其余='Internal error'。

  3. 在 /root/work/fa-problem-lab/service.py 中,request_id(value) 在值是只由 ASCII 字母、数字、下划线、连字符组成的 1–32 个字符的字符串时,原样返回,否则返回 'untracked'。

  4. 在 /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) 的字典。

  5. 在 /root/work/fa-problem-lab/service.py 中,response_for(code, rid) 是一个 JSONResponse:以 problem 为正文,以 status_for 为状态,以 application/problem+json 为 media_type,以规范化后的 rid 作为 X-Request-ID。

  6. 在 /root/work/fa-problem-lab/service.py 中,install_handlers(app) 注册 DomainError 的处理器。它读取头 X-Request-ID,并针对 exc.code 返回 response_for。exc 的 message 不放进响应。

  7. 在 /root/work/fa-problem-lab/service.py 中,create_app() 安装处理器,并在 GET /fail/{code} 中抛出 DomainError(code, 内部句子)。不过当 code=boom 时,抛出 RuntimeError。RuntimeError 的处理器返回 code=internal 的固定 500 问题响应。

参考

给业务异常留下 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 确认。