You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Render部署Django服务调用Daraja API时JSON解码错误求助

解决Render部署MPesa Daraja API的JSONDecodeError及回调问题

一、修复JSONDecodeError(获取令牌失败)

这个错误说明response.json()尝试解析空内容或非JSON格式的响应,核心是部署环境的请求未得到有效JSON返回,按以下步骤排查:

1. 检查环境变量与配置正确性

  • 核对Render上的SAFARICOM_GET_ACCES_TOKEN_URL:注意变量名拼写(代码里写的ACCES少了一个s,应为ACCESS?如果本地配置正确,部署时可能变量名写错),确认地址是Daraja测试环境的https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials
  • 验证MPESA_CONSUMER_KEY和MPESA_CONSUMER_SECRET:确保在Render的环境变量中没有多余空格、特殊字符未转义,且与Daraja开发者门户的密钥完全一致
  • 确认Render网络连通性:部分云平台可能限制出站请求,可在Render的部署脚本中添加curl https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials,查看日志确认能否访问端点

2. 强制输出关键日志(绕过Render免费版日志限制)

免费版Render默认不显示DEBUG级日志,修改代码提升日志级别并强制输出响应内容:

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)  # 改为INFO级别确保日志能被捕获

def get_mpesa_token():
    url = os.getenv("SAFARICOM_GET_ACCES_TOKEN_URL")
    
    try:
        response = requests.get(url, auth=HTTPBasicAuth(settings.MPESA_CONSUMER_KEY, settings.MPESA_CONSUMER_SECRET))

        # 强制输出到stdout,Render会捕获这些内容
        print(f"Token request status code: {response.status_code}")
        print(f"Token request response content: {response.text}")

        if response.status_code != 200:
            logger.error(f"Token request failed with status {response.status_code}, content: {response.text}")
            raise ValueError(f"Token request failed: {response.status_code}")

        try:
            token_data = response.json()
            token = token_data.get('access_token')
            if token:
                return token
            else:
                logger.error("Access token missing in response")
                raise ValueError("Access token not found")
        except ValueError as e:
            logger.error(f"JSON decode failed: {e}, response content: {response.text}")
            raise

    except requests.RequestException as e:
        logger.error(f"Request error: {e}")
        raise

部署后查看Render的「Logs」标签,就能看到响应内容——如果返回的是HTML错误页面(比如401未授权、403禁止访问),说明密钥错误或IP未加入Daraja白名单。

3. 处理Daraja的IP白名单限制

Daraja测试环境默认只允许特定IP访问,若Render的出站IP不在白名单中,会返回非JSON的错误响应。解决方法:

  • 在Daraja开发者门户的「My Apps」中,添加Render的出站IP(可通过curl ifconfig.me在Render的部署脚本中获取)
  • 或使用合规的代理服务转发请求到Daraja API

二、解决回调接收失败问题

1. 确保回调地址符合要求

  • 必须是HTTPS地址:Render免费版提供的xxx.onrender.com域名自带HTTPS,符合要求
  • 地址必须公开可访问:检查Render的服务是否处于「Live」状态,没有设置访问限制
  • 路径拼写正确:核对Daraja配置的回调路径与代码中的路由完全一致

2. 优化回调路由的兼容性

  • 不要设置严格的CORS规则:Daraja的服务器是第三方,需要允许跨域请求,示例路由:
    from flask import request, jsonify
    
    @app.route('/mpesa-callback', methods=['POST'])
    def mpesa_callback():
        # 直接接收请求,不需要CORS预检
        data = request.get_json()
        print(f"Received callback: {data}")
        # 处理逻辑...
        return jsonify({"ResultCode": 0, "ResultDesc": "Success"})
    
  • 支持多种请求格式:Daraja可能发送application/x-www-form-urlencoded格式的请求,添加兼容处理:
    if request.is_json:
        data = request.get_json()
    else:
        data = request.form.to_dict()
    

3. 手动测试回调逻辑

用curl或Postman模拟Daraja的回调请求,验证服务能否正常接收:

curl -X POST https://your-render-url.com/mpesa-callback \
-H "Content-Type: application/json" \
-d '{
    "MerchantRequestID": "test_merchant_id",
    "CheckoutRequestID": "test_checkout_id",
    "ResultCode": 0,
    "ResultDesc": "The service request is processed successfully."
}'

查看Render日志,确认请求被接收并处理。

三、可靠的MPesa API测试方案

1. 使用Daraja官方测试模拟器

在Safaricom Daraja开发者门户中,找到「Simulator」工具:

  • 可模拟支付发起、回调通知等全流程
  • 不需要真实手机或SIM卡,适合本地和部署环境的逻辑测试

2. 替代ngrok的本地暴露方案

用Cloudflare Tunnel(免费且稳定)暴露本地服务:

  1. 安装Cloudflare CLI:brew install cloudflare/cloudflare/cloudflared(Mac)或对应系统的安装包
  2. 登录Cloudflare:cloudflared tunnel login
  3. 启动隧道:cloudflared tunnel --url http://localhost:8000
  4. 复制生成的HTTPS域名,配置为Daraja的回调地址

3. 完善日志与监控

  • 在代码中输出所有关键请求/响应数据到stdout,Render会自动记录这些日志
  • 对于回调请求,将接收的原始数据写入日志,方便排查格式问题

内容的提问来源于stack exchange,提问作者Michael Randiga

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 23:44:53