Next.js中res.redirect is not a function错误及支付重定向问题
问题分析与解决方案
错误根源
- API路由类型不匹配:你在Next.js App Router的API路由(
app/api/.../route.ts)中,错误使用了Pages Router的NextApiRequest/NextApiResponse类型。App Router的API响应对象没有redirect方法,也不支持writeHead这类旧写法,导致TypeError。 - 前端跳转逻辑问题:前端用
fetch调用API时,后端返回的重定向只会作用于fetch请求本身,不会触发浏览器页面跳转,所以即使支付发起成功,用户也看不到支付网关页面。
解决方案(推荐方案:后端返回支付URL,前端跳转)
这种方式更灵活,便于处理错误和用户提示。
1. 修正后端route.ts(适配App Router)
import { NextRequest, NextResponse } from 'next/server'; import * as crypto from 'crypto'; import axios from 'axios'; export async function POST(req: NextRequest) { try { console.log('支付初始化'); // 解析请求体(App Router需手动调用json()) const body = await req.json(); const merchantTransactionId = body.transactionId || `TXN-${Date.now()}`; const data = { merchantId: process.env.PHONEPE_MERCHANT_ID, merchantTransactionId, merchantUserId: body.MUID || 'default-user', name: body.name || '测试用户', amount: 23 * 100, // 单位为分 redirectUrl: `http://localhost:3000/api/status/${merchantTransactionId}`, // 注意端口与你的Next.js服务一致 redirectMode: 'POST', mobileNumber: body.mobileNumber || '9000000000', paymentInstrument: { type: 'PAY_PAGE', }, }; // 生成PhonePe要求的校验参数 const payload = JSON.stringify(data); const payloadMain = Buffer.from(payload).toString('base64'); const keyIndex = process.env.PHONEPE_KEY_INDEX; const string = payloadMain + '/pg/v1/pay' + process.env.PHONEPE_SALT_KEY; const sha256 = crypto.createHash('sha256').update(string).digest('hex'); const checksum = sha256 + '###' + keyIndex; const prod_URL = 'https://api-preprod.phonepe.com/apis/hermes/pg/v1/pay'; const options = { method: 'POST', url: prod_URL, headers: { accept: 'application/json', 'Content-Type': 'application/json', 'X-VERIFY': checksum, }, data: { request: payloadMain, }, }; // 异步调用PhonePe接口(必须await,避免请求未完成就返回) const response = await axios.request(options); const paymentUrl = response.data.data.instrumentResponse.redirectInfo.url; // 返回支付URL给前端 return NextResponse.json({ paymentUrl }); } catch (error) { console.error('支付初始化失败:', error); return NextResponse.json( { message: error instanceof Error ? error.message : '服务器内部错误' }, { status: 500 } ); } }
2. 修正前端页面(拿到URL后跳转)
'use client'; import { Button } from '@/components/ui/button'; import React from 'react'; function Page() { async function createPayment() { try { const response = await fetch(`/api/payments/phonepe`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, // 传递支付所需参数(根据业务需求调整) body: JSON.stringify({ transactionId: `TXN-${Date.now()}`, MUID: 'user-123', name: '张三', mobileNumber: '9000000000', }), }); if (!response.ok) { throw new Error('支付初始化失败'); } const data = await response.json(); // 跳转到PhonePe支付页面 window.location.href = data.paymentUrl; } catch (error) { console.error('错误:', error); alert('支付启动失败,请稍后重试'); } } return ( <div> <Button onClick={createPayment}>结算付款</Button> </div> ); } export default Page;
备选方案:表单提交+后端直接重定向
如果不需要前端额外处理逻辑,可改用表单提交,让浏览器直接处理后端重定向:
1. 前端页面修改为表单提交
'use client'; import { Button } from '@/components/ui/button'; import React from 'react'; function Page() { return ( <div> <form action="/api/payments/phonepe" method="POST"> {/* 隐藏字段传递参数 */} <input type="hidden" name="transactionId" value={`TXN-${Date.now()}`} /> <input type="hidden" name="MUID" value="user-123" /> <Button type="submit">结算付款</Button> </form> </div> ); } export default Page;
2. 后端route.ts修改为直接重定向
import { NextRequest, NextResponse } from 'next/server'; import * as crypto from 'crypto'; import axios from 'axios'; export async function POST(req: NextRequest) { try { console.log('支付初始化'); // 解析表单数据 const formData = await req.formData(); const merchantTransactionId = formData.get('transactionId') as string || `TXN-${Date.now()}`; const data = { merchantId: process.env.PHONEPE_MERCHANT_ID, merchantTransactionId, merchantUserId: formData.get('MUID') as string || 'default-user', name: '测试用户', amount: 23 * 100, redirectUrl: `http://localhost:3000/api/status/${merchantTransactionId}`, redirectMode: 'POST', mobileNumber: '9000000000', paymentInstrument: { type: 'PAY_PAGE', }, }; // 生成校验参数(同推荐方案) const payload = JSON.stringify(data); const payloadMain = Buffer.from(payload).toString('base64'); const keyIndex = process.env.PHONEPE_KEY_INDEX; const string = payloadMain + '/pg/v1/pay' + process.env.PHONEPE_SALT_KEY; const sha256 = crypto.createHash('sha256').update(string).digest('hex'); const checksum = sha256 + '###' + keyIndex; const prod_URL = 'https://api-preprod.phonepe.com/apis/hermes/pg/v1/pay'; const options = { method: 'POST', url: prod_URL, headers: { accept: 'application/json', 'Content-Type': 'application/json', 'X-VERIFY': checksum, }, data: { request: payloadMain, }, }; const response = await axios.request(options); const paymentUrl = response.data.data.instrumentResponse.redirectInfo.url; // 直接重定向到支付页面 return NextResponse.redirect(paymentUrl); } catch (error) { console.error('支付初始化失败:', error); return NextResponse.json( { message: error instanceof Error ? error.message : '服务器内部错误' }, { status: 500 } ); } }
注意事项
- 确保
.env.local中正确配置PHONEPE_MERCHANT_ID、PHONEPE_KEY_INDEX、PHONEPE_SALT_KEY三个环境变量 - 交易ID需保证全局唯一,建议用
时间戳+用户ID生成,不要写死固定值 - 回调地址
redirectUrl需与你的服务域名/端口一致,且要能处理PhonePe的回调请求
内容的提问来源于stack exchange,提问作者Rishabh
相关产品推荐
相关产品推荐

