news 2026/9/8 15:34:14

FastAPI OpenAPI Callbacks 实战:用 `callbacks` 参数把“你的 API 将要回调的外部 API“文档化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FastAPI OpenAPI Callbacks 实战:用 `callbacks` 参数把“你的 API 将要回调的外部 API“文档化

FastAPI OpenAPI Callbacks 实战:用callbacks参数把"你的 API 将要回调的外部 API"文档化

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

这篇技术指南以 FastAPI 官方教程的 OpenAPI Callbacks 一章为主线,讲清"回调(callback)"在 API 设计中的含义、为什么需要把回调写进 OpenAPI 文档,以及如何仅用装饰器参数callbacks=就把外部开发者需要实现的外部 API 完整描述出来。读完你将掌握:回调文档代码的组织方式(APIRouter+ 仅含passpath operation)、OpenAPI 3 Key Expression(如{$callback_url}{$request.body.id})的取值规则,以及这些写法如何最终体现在/docs的 Swagger UI 与/openapi.jsoncallbacks字段中。文中所指代码与截图均来自本仓库 docs_src/openapi_callbacks/tutorial001_py310.py。

什么是 OpenAPI Callback(回调)

你可以构建这样一个 API:它的某个path operation会在运行过程中,主动向"别人(很可能是使用你 API 的那位外部开发者)所创建的 external API"发起一次请求。

当你的 API app 调用那个external API时,这个过程就被称作callback(回调):外部开发者编写的软件先向你的 API 发来请求,随后你的 API "call back"——反向向某个external API(通常正是同一位开发者写的)再发一个请求。

在这种场景下,你非常有必要文档化"那个 external API 应该长成什么样":它应该具备什么样的path operation、期望接收什么 body、应该返回什么 response 等等。这正是 FastAPI 的 OpenAPI Callbacks 特性要解决的问题。

场景示例:一个创建发票(Invoice)的 app

教程用"发票应用"串起全部概念。设想你开发了一个允许创建发票的 app,每张发票包含idtitle(可选)、customertotal

你的 API 使用者(一位外部开发者)会通过 POST 请求在你的 API 中创建一张发票。接着,你的 API(假想流程)会:

  • 把发票发送给外部开发者的某个客户;
  • 完成收款;
  • 向 API 使用者(外部开发者)回发一条通知。
    • 这一步通过你的 API向外部开发者提供的一个external API发送 POST 请求来完成——这就是"回调"。

问题在于:回调真正发生的位置在你的服务器上、你的业务代码里,但需要实现那个回调接收端(external API)的却是外部开发者。如果没有契约文档,两边很容易在字段、路径、响应格式上对不上。FastAPI 给出的解法是:用你早已熟悉的path operation写法,把回调端"应该长什么样"声明出来,并交给 Swagger UI 展示。

先看一个普通的 FastAPI app(回调之前的样子)

在加入 callback 之前,一个常规的 app 会有一个接收Invoicebody 的path operation,外加一个携带回调 URL 的 query 参数callback_url

from fastapi import APIRouter, FastAPI from pydantic import BaseModel, HttpUrl app = FastAPI() class Invoice(BaseModel): id: str title: str | None = None customer: str total: float class InvoiceEvent(BaseModel): description: str paid: bool class InvoiceEventReceived(BaseModel): ok: bool invoices_callback_router = APIRouter() @invoices_callback_router.post( "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived ) def invoice_notification(body: InvoiceEvent): pass @app.post("/invoices/", callbacks=invoices_callback_router.routes) def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None): """ Create an invoice. This will (let's imagine) let the API user (some external developer) create an invoice. And this path operation will: * Send the invoice to the client. * Collect the money from the client. * Send a notification back to the API user (the external developer), as a callback. * At this point is that the API will somehow send a POST request to the external API with the notification of the invoice event (e.g. "payment successful"). """ # Send the invoice, collect the money, send the notification (the callback) return {"msg": "Invoice received"}

(完整源码见 tutorial001_py310.py。)

这部分代码非常常规,绝大部分写法你应该都很熟悉。其中两点值得单独指出:

  1. callback_url使用了 Pydantic 的HttpUrl类型(对应pydantic的 URL 校验网络类型)。这意味着请求/invoices/时若传了callback_url,FastAPI 会自动完成 URL 格式校验,非法的 URL 会直接返回 422 校验错误——主请求的入参就能得到约束。
  2. 这里唯一的新东西,是path operation decorator参数里的callbacks=invoices_callback_router.routes。它不参与任何运行时行为,只用于生成文档。下一节说明它到底是什么。

文档化 callback:真正重要的是"契约"

实际发出回调的代码完全取决于你自己的 API app 业务,且不同 app 之间差异极大。它可能只是寥寥一两行:

callback_url = "https://example.com/api/v1/invoices/events/" httpx.post(callback_url, json={"description": "Invoice paid", "paid": True})

回调本质上就是一次 HTTP 请求;自己实现回调时,可用 HTTPX、Requests 之类的客户端库发出去即可。

但 callback 中最重要的部分是:确保你的 API 使用者(外部开发者)能按照你将要发送的数据格式,正确实现那个 external API。因此,教程接下来做的不是实现回调本身(那可能只是一行代码),而是添加代码来文档化"external API 应该如何接收来自你 API 的回调"

这段文档化代码会出现在你 API 的/docs(Swagger UI)中,让外部开发者一目了然地知道 external API 该怎么搭。

编写 callback 文档代码

需要特别澄清:这些代码永远不会在你的 app 中执行,我们只需要它来"文档化"external API 的样子。好消息是你已经会用 FastAPI 为 API 生成自动文档——现在把同样的知识反向用在"external API 该长什么样"上即可:创建出 external API 应当实现的那些path operation(也就是你的 API 将要调用的那些)。

一个非常实用的写作技巧:编写回调文档代码时,把自己代入"那位外部开发者"的视角——仿佛此刻你在实现 external API,而不是你自己的 API。临时采用这个视角,会让你更自然地判断:参数放在哪里、body 的 Pydantic model 是什么、response 的 model 是什么。

创建承载回调的APIRouter

首先新建一个APIRouter,用来容纳一个或多个回调定义:

from fastapi import APIRouter, FastAPI invoices_callback_router = APIRouter()

编写回调path operation

用同一个APIRouter定义回调path operation。它看起来和普通 FastAPIpath operation几乎一样:

  • 声明它将要接收的 body,例如body: InvoiceEvent
  • 声明它应当返回的 response,例如response_model=InvoiceEventReceived

对应到代码里就是:

class InvoiceEvent(BaseModel): description: str paid: bool class InvoiceEventReceived(BaseModel): ok: bool invoices_callback_router = APIRouter() @invoices_callback_router.post( "{$callback_url}/invoices/{$request.body.id}", response_model=InvoiceEventReceived ) def invoice_notification(body: InvoiceEvent): pass

与普通path operation相比,它有两个关键差异:

  1. 函数体不需要任何真实逻辑——你的 app 永远不会调用这段代码,它只用于生成文档,所以函数体可以只有pass。从源码结构看,这一约束与docs_src及测试中pass # pragma: nocover的写法一致(见 test_sub_callbacks.py),明确表达"仅供文档、不计入覆盖率"。
  2. path 中可以包含 OpenAPI 3 Key Expression(表达式),用变量引用"发送到你 API 的原始请求"中的参数与组成部分。这正是回调 URL 能做到"动态拼接"的原理。

理解 callback path expression

本例中回调路径是一个含表达式的字符串:

"{$callback_url}/invoices/{$request.body.id}"
  • {$callback_url}:引用原始请求里名为callback_url的参数值(这里是 query 参数);
  • {$request.body.id}:引用原始请求 JSON body 中id字段的值。

来完整推演一遍取值过程。假如外部开发者向你的 API 发出请求:

https://yourapi.com/invoices/?callback_url=https://www.external.org/events

携带如下 JSON body:

{ "id": "2expen51ve", "customer": "Mr. Richie Rich", "total": "9999" }

那么你的 API 处理完发票后,会在稍后的某个时点向callback_url(即 external API)发起这样的回调请求:

https://www.external.org/events/invoices/2expen51ve

请求体大致形如:

{ "description": "Payment celebration", "paid": true }

并期望 external API 返回类似下面的 JSON 响应体:

{ "ok": true }

请注意:最终使用的回调 URL 同时包含了callback_urlquery 参数收到的 URL(https://www.external.org/events),以及来自 JSON body 内部的发票id2expen51ve)。{$callback_url}/invoices/{$request.body.id}这个模式把两者拼接成了https://www.external.org/events/invoices/2expen51ve

把回调挂到主path operation

现在你的 callback router 中已经有了所需的回调path operation(即 external developer 需要在 external API 里实现的操作)。接下来,在你的 API 主path operation的 decorator 中通过callbacks参数传入该 router 的.routes属性:

@app.post("/invoices/", callbacks=invoices_callback_router.routes) def create_invoice(invoice: Invoice, callback_url: HttpUrl | None = None): ...

需要特别注意:传入的不是 router 本身invoices_callback_router),而是它的.routes,即invoices_callback_router.routes。FastAPI 会遍历这些 route,用于生成回调的 OpenAPI 文档。

/docs中查看效果

启动 app 并访问http://127.0.0.1:8000/docs,在POST /invoices/的文档里会多出一个Callbacks区域,直观展示 external API 应当如何实现:

如上图所示,Swagger UI 会渲染出回调名invoice_notification、路径模板{$callback_url}/invoices/{$request.body.id}、必需的 JSON 请求体以及默认的成功响应——外部开发者照此即可实现正确的回调端点。

底层实现:callbacks如何进入 OpenAPI 文档

文档层面的行为背后,是 FastAPI 运行时对callbacks的统一处理,可以沿源码走一遍调用链。

  • 接收阶段:在 fastapi/routing.py 中,APIRoute的构造与_populate_api_route_state()都会接收callbacks: list[BaseRoute] | None,并把它直接赋给路由对象(route.callbacks = callbacks)。也就是说,callbacks只是挂在 route 上的"元数据",不影响请求分发。
  • 生成阶段:在 fastapi/openapi/utils.py 的 OpenAPI 生成逻辑中,只要route.callbacks非空,就会对每个APIRoute类型回调递归调用get_openapi_path()(回调本身也是一个完整的path operation,照常生成 parameters、requestBody、responses),再以callbacks[callback.name] = {callback.path: cb_path}的形式写入operation["callbacks"]。这解释了为什么回调的 key 是函数名(如invoice_notification)、value 的 key 是带表达式的路径字符串。
  • Schema 收集阶段:同一个文件里,生成components前会把回调声明中用到的模型一并纳入——callback_flat_models.extend(get_fields_from_routes(api_route.callbacks))。因此InvoiceEventInvoiceEventReceived这些仅在回调文档中出现的 Pydantic 模型,也会被注册为 OpenAPI schema,Swagger UI 才能正确渲染请求体与响应体。
  • 测试验证:仓库中的 test_sub_callbacks.py 用TestClient请求/openapi.json并断言了完整的 schema 快照,可以看到post /invoices/的 operation 下确实包含callbacks字段,其中invoice_notification键、路径表达式、requestBody 对InvoiceEvent$ref、responses 对InvoiceEventReceived$ref都与文档描述一一对应。

扩展用法与实战建议

  • callbacks不仅可用于单个path operation。从 fastapi/routing.py 的类型与注释可以看出,APIRouter以及include_router(..., callbacks=...)同样接受回调列表,用于"该 router 内所有 path operation 共同生效"的回调。测试 test_sub_callbacks.py 演示了这种用法:在子 router 上按路径操作传入一套回调、再通过include_router(..., callbacks=events_callback_router.routes)附加另一套,最终/openapi.json中两个回调(invoice_notificationevent_callback)都被合并进同一个 operation 的callbacks字段。
  • 回调文档与回调实现解耦callbacks=只影响 OpenAPI/Swagger 文档,绝不注册为可被访问的真实路由;真实回调仍需你在业务代码里(如create_invoice函数体中)用 HTTP 客户端主动发出。文中示例的真实实现可能只是一行httpx.post(...),请勿把"文档里能看见回调"误当作"回调已经会被自动执行"。
  • 表达式字段要与实际请求对齐{$callback_url}必须对应真实 query 参数名,{$request.body.id}必须对应真实 body 中的 JSON 字段;若你的原始请求结构不同,请相应替换表达式中的变量名,并在回调端实现时保持 URL 模板与发送逻辑一致。

综上,FastAPI 的 OpenAPI Callbacks 让你可以在自己的 API 文档里"反向"描述出你将要访问的外部 API,把回调契约从口头约定变成可交互、可校验、自动渲染的 OpenAPI 规范。搭配本仓库的 教程源码、OpenAPI 生成实现 与 schema 快照测试 一同阅读,可以完整掌握从 API 定义到 OpenAPI 输出的全部机制。

【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 15:33:48

深入解析IA32_HWP_REQUEST(MSR 0x774):从P-state到硬件调频的实战指南

给一台双路服务器做功耗压测时,我碰到过一件怪事:CPU使用率已经压满了,核心理论频率却一直不肯顶满,风扇转速跟着温度曲线走,整机功耗毛刺怎么压都压不平。查到最后,问题出在操作系统和硬件对“频率由谁说了…

作者头像 李华
网站建设 2026/9/8 15:33:32

嵌入式C语言内存管理四重关:堆栈、对齐、大小端与溢出排查

前段时间帮团队面了几轮嵌入式软件工程师的候选人,发现一个特别有意思的现象:很多人简历上写着"熟练掌握C语言",项目经历里也是各种驱动、协议栈刷得满满当当。结果我一问内存管理,画风就变了——"堆就是动态分配&…

作者头像 李华
网站建设 2026/9/8 15:31:29

从芯片级精度到MW级动力:汽车电子全栈测试方案解析

Automotive Testing Expo 2026的展馆里,ITECH艾德克斯的展台这几天一直是热门打卡点。我绕着展台转了两圈,发现围在最前面的不是来拍照的媒体,全是带着笔记本和探头过来对参数的工程师。有人在回馈式负载柜前面问并机均流,有人在电…

作者头像 李华
网站建设 2026/9/8 15:31:24

三维路面不平度生成与RoadRunner导入:谐波叠加法实践指南

前两年做整车平顺性仿真,我一开始只用两条独立车辙剖面来应付路面输入,左右轮各一条一维序列,跑起来倒是能算,但真到了要把路面数据放进场景级驾驶仿真工具的时候,问题全冒出来了——车辆并不是只在两条轮辙上运动&…

作者头像 李华
网站建设 2026/9/8 15:30:09

movie-web 远程一起看电影:3 步把好友拉进同一房间

movie-web 远程一起看电影:3 步把好友拉进同一房间 【免费下载链接】movie-web movie-web 是一款用于轻松观看电影的网络应用程序。该服务的工作原理是在直观且美观的用户界面中显示来自第三方提供商的视频文件。 项目地址: https://gitcode.com/GitHub_Trending/…

作者头像 李华