如何用 React Router Resource Route 的 action 处理外部 Webhook 请求并校验签名?
GitHub、Stripe 等服务会向你的服务器推送 POST 请求,你需要在 React Router 应用中接收这些请求、验证签名,再执行业务逻辑。本文基于 React Router 官方文档 Webhooks 和 Resource Routes,给出一个可落地的做法:用 Resource Route 的 action 接收 GitHub 的 commit 推送通知,用 HMAC-SHA256 校验 X-Hub-Signature-256 请求头,拒绝非法请求。
适用前提(来自文档标注):Resource Route 特性在 framework 与 data 两种模式下可用,且要求服务端渲染场景——路由在服务端直接返回响应,而不是渲染组件。
Resource Route 如何变成 Webhook 端点
在 React Router 中,一个路由模块只要 导出 loader 或 action,但不导出默认组件(default component),就会按约定成为 resource route,由服务端直接响应请求,而不经过 UI 渲染。这一点在 docs/how-to/resource-routes.md 中有明确说明:
A route becomes a resource route by convention when its module exports a loader or action but does not export a default component.
请求方法的处理规则同样是该文档给出的:GET 请求由 loader 处理,POST、PUT、PATCH、DELETE 由 action 处理。Webhook 推送基本都是 POST,所以逻辑写在 action 里即可。
如果你的项目使用文件路由约定(@react-router/fs-routes,见 docs/how-to/file-route-conventions.md),在 app/routes/ 下放一个 github.ts(或按文档的转义规则命名为 webhook[.]github.ts 之类,避免特殊字符被路由解析)即可让对应 URL 命中这个模块;使用显式 routes.ts 配置的项目则按 routes 配置文档 的方式注册该路径。下面以模块本身为准。
编写 Webhook 路由模块
以下是 docs/how-to/webhook.md 给出的完整示例,接收 GitHub 在新 commit 推送时发出的通知:
import type { Route } from "./+types/github";
import crypto from "node:crypto";
export const action = async ({
request,
}: Route.ActionArgs) => {
if (request.method !== "POST") {
return Response.json(
{ message: "Method not allowed" },
{
status: 405,
},
);
}
const payload = await request.json();
/* Validate the webhook */
const signature = request.headers.get(
"X-Hub-Signature-256",
);
const generatedSignature = `sha256=${crypto
.createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET)
.update(JSON.stringify(payload))
.digest("hex")}`;
if (signature !== generatedSignature) {
return Response.json(
{ message: "Signature mismatch" },
{
status: 401,
},
);
}
/* process the webhook (e.g. enqueue a background job) */
return Response.json({ success: true });
};
逐段说明这段代码的每个判断条件:
- 方法检查:非 POST 请求直接返回
405和{ message: "Method not allowed" }。虽然按文档的说法action只会被 POST/PUT/PATCH/DELETE 触发,文档仍在此处显式拦截非 POST 请求。 - 读取请求体:
await request.json()得到推送的 payload。 - 计算期望签名:从请求头取
X-Hub-Signature-256的实际值;同时用环境变量GITHUB_WEBHOOK_SECRET作为密钥,对 payload 的 JSON 字符串做 HMAC-SHA256,得到sha256=前缀的十六进制摘要。 - 比对签名:两者不相等时返回
401和{ message: "Signature mismatch" },拒绝处理。 - 业务处理与成功响应:校验通过后在注释处执行你的业务逻辑(文档示例是"enqueue a background job",即入队一个后台任务),最后返回
Response.json({ success: true })。
两个使用注意点:
- 模块 没有默认导出,这是它成为 resource route 的关键,不要顺手加上
export default组件。 - 运行环境必须提供
GITHUB_WEBHOOK_SECRET环境变量,即你在 Webhook 服务方配置的那个签名密钥。
返回类型:外部消费的端点建议直接返回 Response
Resource Routes 文档对返回值给了明确的选型规则:resource route 可以返回 Response 实例,也可以返回 data() 对象,但——
- 如果 resource route 是供外部系统消费的(Webhook 就属于这类),建议返回
Response实例,让响应编码在代码中显式可见; - 如果它是被应用内的
fetcher或<Form>提交访问的,则返回data(),与 UI 路由的 loader/action 保持一致,并可通过data()/Await把 promise 流到 UI。
上面示例返回 Response.json(...) 正是遵循了第一条。
验证方式:根据响应状态判断
文档没有给出独立的检查命令,但 action 本身定义了三种可观察的判定结果,你可以据此核对端点行为:
| 请求情况 | 预期状态码 | 预期响应体(文档示例值) |
|---|---|---|
| 非 POST 请求 | 405 |
{ "message": "Method not allowed" } |
X-Hub-Signature-256 与按 GITHUB_WEBHOOK_SECRET 计算出的 HMAC 不一致 |
401 |
{ "message": "Signature mismatch" } |
| 签名校验通过 | 200 |
{ "success": true } |
也就是说:向该路由发送一次不带签名或签名错误的 POST,应该得到 401 而不是业务处理;发送签名正确的请求,才会走到 success: true。另外注意文档指出,resource route 返回 Response(包括 4xx/5xx 状态码的 Response,无论是 return 还是 throw)都算成功执行,不会触发 handleError;只有抛出 Error 这类非 Response/data() 的值才会触发 handleError 并产生 500 响应。签名校验失败属于"API 已正常产出了一个 401 Response",不会上报为服务器错误——这和 fetch() 对 4xx/5xx 不 reject 的行为一致。
边界与限制
- 模式限制:Resource Route 仅在 framework 与 data 模式可用(源文档标注
[MODES: framework, data])。 - Error Boundary 的适用条件:只有当 resource route 是从 UI 内被访问(
fetcher调用或<Form>提交)时,throw才会冒泡到最近的ErrorBoundary;像 Webhook 这种外部直接请求的路径不适用该机制。 - 链接到 resource route:在 UI 中链接到 resource route 时要用
<a>或<Link reloadDocument>,否则 React Router 会尝试客户端路由并抓取 payload,文档说明这时会得到一个帮助性错误提示。 - 仓库的集成测试 integration/resource-routes-test.ts 覆盖了 resource route 的各类返回与错误行为(例如响应头透传、
data()返回/抛出、POST 到无 action 路由返回 405 等),可作为服务端行为的对照参考。
如果后续需要处理非 GitHub 的 Webhook,源文档只以 GitHub 为例,其他服务方的签名头名称与算法需要按对应服务的规则自行核对,这一点文档未作说明。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00