解决Firebase Cloud Functions作为Mpesa支付回调URL失效问题
Mpesa回调接收失败:Invalid request data 问题排查与修复
问题背景
使用Firebase Cloud Functions作为回调URL接收Safaricom Mpesa支付响应,调用API触发用户输入PIN的流程正常,但云函数无法正确接收支付响应,返回“Invalid request data”错误。
相关代码
Flutter调用Mpesa API代码
Future mPesaPaymentInAction() async { print("mpesa in action!"); try { Mpesa mpesa = Mpesa( clientKey: myClientKey, clientSecret: myClientSecret, passKey: myPassKey, environment: myEnvironment, ); await mpesa .lipaNaMpesa( amount: 1, phoneNumber: phoneNumberForPayment.text.toString(), businessShortCode: myBusinessShortCode, transactionDescription: transactionDescription, callbackUrl: callBackUrl, ) .then((result) async { ScaffoldMessenger.of(context).showSnackBar(SnackBar( duration: const Duration(seconds: 5), backgroundColor: Colors.greenAccent, content: Text( "Mpesa Pin request sent by Safaricom, Please wait.", textAlign: TextAlign.center, style: GoogleFonts.lato( fontSize: 14, ), ), )); if (result.ResponseCode.toString() == "0" && result.CustomerMessage.toString() == "Success. Request accepted for processing" && result.ResponseDescription == "Success. Request accepted for processing") { setState(() { mCheckoutRequestID_waterServices = result.CheckoutRequestID; }); try { final prefs = await SharedPreferences.getInstance(); await prefs.setString("mCheckoutRequestID_waterServices", mCheckoutRequestID_waterServices.toString()); await prefs.setString( "paymentDone_waterServices", paymentDone_waterServices); } catch (e) { print(e.toString()); } print("mpesa done!"); } else { throw Error(); } }).catchError((error) { print(error); setState(() { loadingScreen = false; }); }); } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( duration: const Duration(seconds: 5), backgroundColor: Colors.redAccent, content: Text( "Please try again.", textAlign: TextAlign.center, style: GoogleFonts.lato( fontSize: 14, ), ), )); setState(() { loadingScreen = false; phoneNumberForPayment.clear(); }); if (kDebugMode) { print("Was inside mpesa in action:- ${e.toString()}"); } } }
获取回调响应数据的Flutter方法(需删除)
Future getMpesaData(String callbackUrl) async { try { print("GETTING DATA"); final response = await http.post(Uri.parse(callbackUrl)); print(response.body.toString()); if (response.statusCode == 200) { final data = json.decode(response.body); final initiatorName = data['InitiatorName']; final securityCredential = data['SecurityCredential']; final commandID = data['CommandID']; final amount = data['Amount']; final mpesaReceiptNumber = data['MpesaReceiptNumber']; final transactionID = data['TransactionID']; final transactionDate = data['TransactionDate']; final phoneNumber = data['PhoneNumber']; final resultCode = data['ResultCode']; final resultDesc = data['ResultDesc']; print('Initiator Name: $initiatorName'); print('Security Credential: $securityCredential'); print('Command ID: $commandID'); print('Amount: $amount'); print('M-Pesa Receipt Number: $mpesaReceiptNumber'); print('Transaction ID: $transactionID'); print('Transaction Date: $transactionDate'); print('Phone Number: $phoneNumber'); print('Result Code: $resultCode'); print('Result Description: $resultDesc'); return initiatorName; } else { throw Exception('Failed to get M-Pesa data: ${response.reasonPhrase}'); } } catch (e) { print("Failed to get M-Pesa data"); } }
原Firebase云函数代码
const functions = require("firebase-functions"); const express = require("express"); const app = express(); const cors = require("cors")({ origin: true }); const bodyParser = require("body-parser"); const axios = require("axios"); app.use(cors); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); const CONSUMER_KEY = ""; const CONSUMER_SECRET = ""; const PASSKEY = ""; const SHORTCODE = "174379"; exports.mpesaCallback = functions.https.onRequest((req, res) => { const data = req.body; if (!data || !data.Body || !data.Body.stkCallback) { return res.status(400).json({ message: "Invalid request data" }); } const payment = data.Body.stkCallback; if (!payment || payment.ResultCode != 0) { return res.status(400).json({ message: "Invalid payment data" }); } axios({ url: "https://api.safaricom.co.ke/mpesa/b2c/v1/paymentresulthttp", method: "post", headers: { Authorization: "Basic " + Buffer.from(CONSUMER_KEY + ":" + CONSUMER_SECRET).toString("base64"), "Content-Type": "application/json" }, data: { InitiatorName: "", SecurityCredential: "", CommandID: "PaymentConfirmation", Amount: payment.CallbackMetadata.Item[0].Value, MpesaReceiptNumber: payment.CallbackMetadata.Item[1].Value, TransactionID: payment.CallbackMetadata.Item[2].Value, TransactionDate: payment.CallbackMetadata.Item[3].Value, PhoneNumber: payment.CallbackMetadata.Item[4].Value, ResultCode: 0, ResultDesc: "Payment successful" } }) .then(response => { console.log(response.data); return res.status(200).json({ message: "Payment confirmed" }); }) .catch(error => { console.log(error); return res.status(500).json({ message: "Error sending confirmation" }); }); }); exports.app = functions.https.onRequest(app);
报错信息
Flutter端打印
I/flutter (13136): {"message":"Invalid request data"} I/flutter (13136): Failed to get M-Pesa data
GCP云函数日志
{ "insertId": "1ny6725faqkk59", "labels": {}, "logName": "insertName", "receiveTimestamp": "2023-03-28T11:56:32.298610539Z", "resource": {}, "severity": "DEBUG", "textPayload": "Function execution took 73 ms, finished with status: 'error'", "timestamp": "2023-03-28T11:56:32.028351659Z", "trace": "projects/insertName-6de2c/traces/f1fa60c5b51f6ac7f8d2e16958dacf96" }
问题分析与修复
核心问题
- 回调URL指向错误:云函数定义了
mpesaCallback处理逻辑,但如果callBackUrl指向的是app这个未关联对应路由的Express应用,Safaricom的请求无法进入正确处理流程。 - 错误主动调用回调URL:Flutter中的
getMpesaData主动给回调URL发POST请求,违背Mpesa回调机制——回调是Safaricom主动发起的,无需客户端主动请求。 - 云函数逻辑错误:
- 错误调用B2C接口
paymentresulthttp,Lipa Na Mpesa回调只需返回200响应即可,无需额外请求。 - 参数校验过于严格且无日志,无法定位请求实际结构。
- 未容错
CallbackMetadata.Item的顺序或缺失情况。
- 错误调用B2C接口
修复步骤
1. 修正回调URL
确保callBackUrl指向mpesaCallback函数的正确地址,格式示例:https://<region>-<your-project-id>.cloudfunctions.net/mpesaCallback
2. 删除错误的getMpesaData调用
在Flutter的mPesaPaymentInAction方法中,移除final data = await getMpesaData(callBackUrl);这一行,同时可以删除整个getMpesaData函数。
3. 重构云函数代码
const functions = require("firebase-functions"); exports.mpesaCallback = functions.https.onRequest((req, res) => { // 打印请求体,用于排查结构问题 console.log("Received callback request:", JSON.stringify(req.body)); // 基础校验,同时返回Safaricom要求的格式响应 if (!req.body?.Body?.stkCallback) { console.error("Invalid request structure:", req.body); return res.status(200).json({ ResultCode: 0, ResultDesc: "Invalid request received" }); } const stkCallback = req.body.Body.stkCallback; console.log("STK Callback details:", JSON.stringify(stkCallback)); // 处理支付结果 if (stkCallback.ResultCode === 0) { // 支付成功,提取元数据 const metadataMap = {}; stkCallback.CallbackMetadata?.Item?.forEach(item => { metadataMap[item.Name] = item.Value; }); console.log("Payment successful - Data:", metadataMap); // 此处添加业务逻辑:写入数据库、发送通知等 } else { // 支付失败 console.error("Payment failed:", stkCallback.ResultDesc); } // 必须返回200响应,否则Safaricom会重复发送回调请求 return res.status(200).json({ ResultCode: 0, ResultDesc: "Success" }); });
4. 查看云函数日志
支付完成后,查看GCP云函数的日志,确认Safaricom是否发送了请求,以及请求结构是否符合预期,以此进一步排查问题。
内容的提问来源于stack exchange,提问作者waldokhalid
相关产品推荐
相关产品推荐

