首次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问题
调整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检查Ktor客户端超时设置
确认Ktor客户端的连接超时、读取超时是否过短(比如设置了30s,但后端处理需要40s),如果是,拉长客户端的超时时间,匹配Nginx和后端的处理能力。定位后端处理耗时瓶颈
在代码中添加耗时日志,找出拖慢请求的环节: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服务的幂等性,避免客户端重试导致重复数据:
数据库加唯一约束
在webhook记录表中,给transaction_id + event_type组合添加唯一索引,从数据库层面阻止重复插入:ALTER TABLE webhook_records ADD UNIQUE INDEX idx_unique_transaction_event (transaction_id, event_type);之后重复插入时会触发数据库报错,FastAPI捕获后直接返回成功即可(因为记录已经存在)。
代码层面先查后插
在插入新记录前,先查询是否已有相同的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(...)异步处理第三方回调
当前代码中,插入记录后还要调用第三方回调,这部分耗时会拉长响应时间。可以把回调逻辑放到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

