Next.js webhook 为何返回 301 而不是 200?
结论
301 不是 webhook 处理函数返回的。请求还没进入 WebhookHandler,就已经被 Next.js 配置、Middleware、反向代理、CDN 或域名规则重定向了。
先看响应头中的 Location,它会告诉你请求被重定向到了哪里。常见原因有:
- webhook 地址的结尾斜杠不一致,例如
/api/webhook/被跳转到/api/webhook http被强制跳转到https- 裸域名与
www域名之间存在跳转 next.config.js配置了redirects()或trailingSlash- Middleware、Nginx、Cloudflare 或托管平台设置了重定向
- webhook 填写的是页面地址,不是真正的 API Route 地址
支付平台不一定会跟随重定向。即便跟随,也可能把 POST 改成 GET,导致 webhook 失败。因此,支付平台中配置的 URL 应直接返回 200,不要依赖重定向。
先检查 Location 响应头
使用支付平台中配置的完整 URL 进行测试:
curl -i -X POST 'https://example.com/api/webhook' \
-H 'Content-Type: application/json' \
--data '{"status":"paid"}'
如果响应类似:
HTTP/2 301
Location: https://www.example.com/api/webhook
问题出在域名规范化。
如果响应是:
HTTP/2 301
Location: https://example.com/api/webhook/
通常是结尾斜杠规则造成的。
测试时不要先加 -L 自动跟随跳转,否则可能看不到真正的问题。确认跳转目标后,把支付平台中的 webhook URL 改成最终地址。
检查 Next.js 重定向配置
查看 next.config.js 或 next.config.mjs 中是否配置了 redirects():
module.exports = {
async redirects() {
return [
{
source: '/api/:path*',
destination: '/:path*',
permanent: true,
},
];
},
};
这类规则可能误伤 /api/webhook。API Route 通常不应经过针对页面设置的规范化跳转。
还要检查 trailingSlash:
module.exports = {
trailingSlash: true,
};
启用后,应用可能要求 URL 以 / 结尾。未启用时,带 / 的地址也可能被规范化。
问题不在于必须开启还是关闭这项配置,而在于支付平台中的地址必须与应用最终接受的地址完全一致:
https://example.com/api/webhook
或:
https://example.com/api/webhook/
应使用无需重定向就能返回响应的那个地址。
检查 Middleware
如果项目中有 middleware.ts 或 middleware.js,需要确认登录、语言、域名或路径规则没有重定向 API Route。
例如,下面的认证逻辑会把 webhook 一并跳转到登录页:
import { NextResponse } from 'next/server';
export function middleware(request) {
const authenticated = false;
if (!authenticated) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
可以通过 matcher 排除 webhook:
export const config = {
matcher: ['/dashboard/:path*'],
};
也可以在逻辑中明确放行:
import { NextResponse } from 'next/server';
export function middleware(request) {
if (request.nextUrl.pathname === '/api/webhook') {
return NextResponse.next();
}
// 其他认证或重定向逻辑
return NextResponse.next();
}
国际化 Middleware 也需要检查。将 /api/webhook 重定向到 /zh/api/webhook 通常是错误的,因为 API 路径不应参与页面语言跳转。
检查部署平台和反向代理
如果本地请求直接返回 200,线上却返回 301,应重点检查 Next.js 之外的配置:
- 托管平台的域名跳转设置
- Cloudflare Redirect Rules 或 Page Rules
- Nginx、Apache、负载均衡器配置
http到https的强制跳转example.com到www.example.com的规范域名跳转- 平台配置文件中的 redirects 或 rewrites
例如,支付平台配置的是:
http://example.com/api/webhook
服务器则会强制跳转到:
https://example.com/api/webhook
这种情况不需要取消 HTTPS,直接把 webhook 地址改成 HTTPS 即可。
如果生产环境使用的规范域名是 www.example.com,支付平台也应直接使用这个域名。
修正请求体处理代码
301 通常与下面这行代码没有直接关系,因为发生重定向时,请求很可能还没进入处理函数:
const requestData = JSON.parse(req);
不过,这行代码本身也有问题。req 是请求对象,不是 JSON 字符串。在 Pages Router 的 API Route 中,Next.js 默认会根据 Content-Type 解析请求体,通常直接读取 req.body:
export default async function WebhookHandler(req, res) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST']);
return res.status(405).end('Method not allowed.');
}
try {
const requestData = req.body;
if (requestData.status === 'paid') {
// Verify the webhook signature.
// Update the database idempotently.
}
return res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook processing failed:', error);
return res.status(400).json({ received: false });
}
}
如果第三方发送的 Content-Type 不正确,req.body 可能是字符串,可以根据类型分别处理:
const requestData =
typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
不要对请求对象本身执行 JSON.parse(req)。
使用 App Router 时的写法
如果 endpoint 位于 app/api/webhook/route.js,应使用 Route Handler:
import { NextResponse } from 'next/server';
export async function POST(request) {
try {
const requestData = await request.json();
if (requestData.status === 'paid') {
// Verify the webhook signature.
// Update the database idempotently.
}
return NextResponse.json(
{ received: true },
{ status: 200 }
);
} catch (error) {
console.error('Webhook processing failed:', error);
return NextResponse.json(
{ received: false },
{ status: 400 }
);
}
}
Pages Router 和 App Router 读取请求体的方式不同,不能混用。
验证签名时不要提前解析原始请求体
许多支付平台要求使用原始请求体计算 webhook 签名。遇到这种情况,Pages Router 可能需要关闭默认的 body parser:
export const config = {
api: {
bodyParser: false,
},
};
随后读取原始字节并验证签名。具体实现必须遵循支付平台的签名算法。不要先解析 JSON 再重新序列化,因为空格、字段顺序或编码的变化都可能导致验签失败。
App Router 可以这样处理:
export async function POST(request) {
const rawBody = await request.text();
// Verify signature using rawBody.
const requestData = JSON.parse(rawBody);
return Response.json({ received: true });
}
为什么 webhook 不应该使用重定向
301 表示永久重定向,但不同 webhook 客户端的处理方式并不一致:
- 有些客户端会直接将其视为投递失败
- 有些客户端不会跟随重定向
- 有些客户端跟随跳转后可能把
POST改成GET - 原始请求体或签名相关请求头可能无法按预期保留
- 重试机制可能因此反复投递同一个事件
如果确实需要在代理层跳转并保留请求方法,307 或 308 的语义通常比 301 更合适。不过,对 webhook endpoint 来说,最稳妥的处理方式仍是不进行任何跳转。
推荐的排查顺序
- 使用
curl -i请求支付平台中配置的完整 URL。 - 查看
Location响应头,确认跳转目标。 - 将 webhook URL 直接改成最终的 HTTPS 地址,并使用规范域名和正确的斜杠形式。
- 检查
next.config.js中的redirects()和trailingSlash。 - 检查 Middleware 是否覆盖了
/api/webhook。 - 检查 CDN、反向代理和托管平台的域名重定向设置。
- 确认处理函数使用了
req.body、request.json()或验签所需的原始请求体。 - 更新数据库前先验证签名,并按照事件 ID 或订单状态实现幂等处理。
只要原始 webhook URL 不经过重定向,处理函数也能正确读取请求体,该 endpoint 就应直接返回 200。
备注:内容仅供参考。
