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

首次Webhook请求报499错误后返回201,导致重复入库求助

问题

使用FastAPI + mysql-connector-python开发Webhook服务,部署在Nginx之后出现偶发问题:首次请求返回499错误,随后再次请求返回201成功,导致数据库中回调记录重复插入。

相关代码

@router.post("/webhook", status_code=status.HTTP_201_CREATED, include_in_schema=False)
async def create_webhook_record(request: dict):
    """ Create a Webhook Response """

    if not request:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorised Request Request is missing")

    event_type = request.get("event_type")
    if not event_type:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorised Request event type missing")

    order = request.get("order")
    if not order:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorised Request order is missing")

    transaction = request.get("transaction")
    if not transaction:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorised Request transaction is missing")

    order_id = request.get("order_id")
    transaction_id = request.get("transaction_id")
    request_status = request.get("status")
    request_message = request.get("message")

    invoice_id = order.get("invoice_id")
    total_amount = order.get("total_amount")

    transaction_status = transaction.get("status")

    request_model = WebhookIn(
        invoice_id=invoice_id,
        order_id=order_id,
        transaction_id=transaction_id,
        total_amount=total_amount,
        event_type=event_type,
        status=request_status,
        message=request_message,
        transaction_status=transaction_status
    )

    transaction_details = await api.get_transaction_by_invoice_id(
        invoice_id=request_model.invoice_id)
    if not transaction_details:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
                            detail="transaction not found.")

    merchant_id = transaction_details.get("merchant_id")
    mcode = transaction_details.get("mcode")

    webhook_response = await api.create_webhook_record(
        request=request_model, merchant_id=merchant_id, mcode=mcode)
    if webhook_response:
        merchant = await crud.get_merchant_by_id(id=merchant_id)
        is_callback = merchant.get("is_callback")
        if is_callback:
            merchant = await crud.get_callback_url_by_merchant_id(
                merchant_id=merchant_id)
            callback_url = merchant.get("callback_url")
            await api.post_callback_url(callback_url=callback_url,
                                request_model=request_model)

        return {"message": "received"}
    return {"message": "received but could  not write"}

Nginx日志

6x.1.2xx.2xx - - [05/Aug/2024:11:49:43 +0530] "POST /payments/webhook HTTP/1.1" 499 0 "-" "Ktor client" 
6x.1.2xx.2xx - - [05/Aug/2024:11:50:12 +0530] "POST /payments/webhook HTTP/1.1" 201 22 "-" "Ktor client"

排查与解决方案

一、先搞懂499错误的本质

Nginx的499状态码是客户端主动断开连接导致的:也就是FastAPI还没处理完请求、没返回响应时,发送请求的Ktor客户端就先关掉了连接。核心原因要么是客户端超时设置过短,要么是Nginx/后端处理请求的时间太长。

二、排查并解决499问题

  1. 调整Nginx超时配置
    检查Nginx中代理相关的超时参数,默认值可能不足以覆盖后端处理时间,修改配置文件:

    location /payments/webhook {
        proxy_pass http://你的FastAPI服务地址;
        # 延长连接、发送、读取超时时间
        proxy_connect_timeout 120s;
        proxy_send_timeout 120s;
        proxy_read_timeout 120s;
        # 其他原有配置
    }
    

    修改后重启Nginx:sudo systemctl restart nginx

  2. 检查Ktor客户端超时设置
    确认Ktor客户端的连接超时、读取超时是否过短(比如设置了30s,但后端处理需要40s),如果是,拉长客户端的超时时间,匹配Nginx和后端的处理能力。

  3. 定位后端处理耗时瓶颈
    在代码中添加耗时日志,找出拖慢请求的环节:

    import time
    @router.post("/webhook", status_code=status.HTTP_201_CREATED, include_in_schema=False)
    async def create_webhook_record(request: dict):
        start_time = time.time()
        # 原有代码逻辑...
        # 返回前记录耗时
        process_time = time.time() - start_time
        print(f"Webhook处理耗时: {process_time:.2f}s, invoice_id={invoice_id}")
        return {"message": "received"}
    

    重点看数据库查询、第三方回调这两步的耗时,针对性优化(比如给数据库加索引、优化回调逻辑)。

三、解决重复插入问题(核心是实现幂等性)

即使解决了499,也要保证Webhook服务的幂等性,避免客户端重试导致重复数据:

  1. 数据库加唯一约束
    在webhook记录表中,给transaction_id + event_type组合添加唯一索引,从数据库层面阻止重复插入:

    ALTER TABLE webhook_records ADD UNIQUE INDEX idx_unique_transaction_event (transaction_id, event_type);
    

    之后重复插入时会触发数据库报错,FastAPI捕获后直接返回成功即可(因为记录已经存在)。

  2. 代码层面先查后插
    在插入新记录前,先查询是否已有相同的transaction_id和event_type组合:

    # 插入前先检查是否存在
    existing_record = await api.get_webhook_record_by_transaction_event(
        transaction_id=request_model.transaction_id, 
        event_type=request_model.event_type
    )
    if existing_record:
        return {"message": "received"}
    # 不存在再执行插入
    webhook_response = await api.create_webhook_record(...)
    
  3. 异步处理第三方回调
    当前代码中,插入记录后还要调用第三方回调,这部分耗时会拉长响应时间。可以把回调逻辑放到FastAPI的后台任务中,先返回响应给客户端:

    from fastapi import BackgroundTasks
    
    @router.post("/webhook", status_code=status.HTTP_201_CREATED, include_in_schema=False)
    async def create_webhook_record(request: dict, background_tasks: BackgroundTasks):
        # 原有参数校验、查询transaction_details等逻辑...
        
        webhook_response = await api.create_webhook_record(...)
        if webhook_response:
            merchant = await crud.get_merchant_by_id(id=merchant_id)
            is_callback = merchant.get("is_callback")
            if is_callback:
                merchant = await crud.get_callback_url_by_merchant_id(merchant_id=merchant_id)
                callback_url = merchant.get("callback_url")
                # 把回调任务丢到后台执行
                background_tasks.add_task(api.post_callback_url, callback_url=callback_url, request_model=request_model)
            # 立即返回响应,不用等回调完成
            return {"message": "received"}
    

四、验证与监控

  • 部署修改后的代码和配置,观察Nginx日志是否还有499错误
  • 检查数据库是否还有重复记录,确认幂等逻辑生效
  • 持续监控请求处理耗时,确保优化后的流程稳定

内容的提问来源于stack exchange,提问作者Pabitra Pandit

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 21:52:03