如何解决WhatsApp Cloud API Webhook重复消息循环问题?
问题背景
已通过个人手机号配置WhatsApp Cloud API Webhook,基于Node.js Express + TypeScript开发聊天机器人服务端。收到消息后根据类型触发响应逻辑,处理完成后通过POST graph.facebook.com/v16.0/${ID_CELL}/messages发送回复。按官方文档,服务端未返回200时WhatsApp会重复推送7天,但已确保出错时也返回200,仍出现重复消息推送、偶尔重复响应的情况。
相关代码
Webhook 接收处理代码
export const receiveMessage = async(req: Request, res: Response, next: NextFunction) => { let input = req.body.input[0]; let changes = input.changes[0]; let value = changes.value; let messageObject = value.messages; try{ const messages: Message = messageObject[0]; const from = message_object[0]?.from; //Type of message let typeMessage = messages.type; // First we record the log await registerEvents(entry, 200, "Receiving Webhook Whatsapp Api Cloud", null); // we validate that the message is not undefined if (messageObject === undefined) { console.log('no message found'); res.sendStatus(200); } // we validate the type of messaging if (messageType === "text") { sendStatusMessage("Writing...", from); const data = await textProcess(messages, from); SendMessageWhatsApp(data); res.sendStatus(200); } if (messageType === "audio") { console.log('is audio'); const data = await audioProcess(messages, from); SendMessageWhatsApp(data); res.sendStatus(200); } if (messageType === "button") { const data = await buttonProcess(messages, from); SendMessageWhatsApp(data); res.sendStatus(200); } if (messageType === "interactive") { const data = await interactiveProcess(messages, from); SendMessageWhatsApp(data); res.sendStatus(200); } } catch (error) { await registerEvents(input, 500, "Webhook Whatsapp Api Cloud Error", error) // Avoid lopp webhook console.log('An internal error occurred on the server but 200 should always be sent'); res.sendStatus(200) } }
消息发送代码
function SendMessageWhatsApp(message: any) { constant data = JSON.stringify(message); constant options = { host: "graph.facebook.com", path: `/v16.0/${ID_CELL}/messages`, method: "POST", body: data, headers: { "Content-type": "application/json", Authorization: `Bearer ${TOKEN_CLOUD_API_WSP}` } }; const req = https.request(options, res => { res.on("data", d => { process.stdout.write(d); }); }); req.on("error", error => { console.error(error); supabase.from('records').insert([ { message: error, status: 500, task: "Error sending message to Whatsapp", error: null } ]); }); req.write(data); req.end(); }
问题排查与解决方案
1. 立即返回200,异步处理业务逻辑
当前代码在执行await textProcess等耗时操作后才返回200,若业务处理耗时超过WhatsApp的超时阈值(通常10秒左右),平台会判定请求超时并重新推送消息。必须先返回200确认接收,再异步处理消息响应逻辑。
修改示例:
export const receiveMessage = async(req: Request, res: Response, next: NextFunction) => { // 第一步:立即返回200,确认Webhook消息接收 res.sendStatus(200); let input = req.body.input[0]; let changes = input.changes[0]; let value = changes.value; let messageObject = value.messages; try{ if (!messageObject || messageObject.length === 0) { console.log('no message found'); return; } const messages: Message = messageObject[0]; const from = messages.from; // 修复变量名拼写错误 const messageType = messages.type; await registerEvents(input, 200, "Receiving Webhook Whatsapp Api Cloud", null); // 异步处理业务逻辑,不阻塞响应 switch(messageType) { case "text": sendStatusMessage("Writing...", from); const textData = await textProcess(messages, from); SendMessageWhatsApp(textData); break; case "audio": console.log('is audio'); const audioData = await audioProcess(messages, from); SendMessageWhatsApp(audioData); break; case "button": const buttonData = await buttonProcess(messages, from); SendMessageWhatsApp(buttonData); break; case "interactive": const interactiveData = await interactiveProcess(messages, from); SendMessageWhatsApp(interactiveData); break; default: console.log('Unsupported message type:', messageType); } } catch (error) { await registerEvents(input, 500, "Webhook Whatsapp Api Cloud Error", error) console.log('An internal error occurred on the server:', error); } }
2. 修复变量名错误
原代码中const from = message_object[0]?.from;存在变量名拼写错误(应为messages.from),会导致from为undefined,后续业务逻辑出错,可能引发重复推送。
3. 消息去重:利用WhatsApp唯一消息ID
每条WhatsApp消息都有唯一的messages.id字段,可将已处理的消息ID存入缓存(如Redis)或数据库,处理前先检查是否已处理过,避免重复响应。
示例逻辑:
// 假设使用Redis缓存,需先安装redis库并配置客户端 import { createClient } from 'redis'; const redisClient = createClient(); // 在处理消息前添加去重逻辑 const messageId = messages.id; const isProcessed = await redisClient.get(`whatsapp:processed:${messageId}`); if (isProcessed) { console.log('Message already processed:', messageId); return; } // 设置缓存,过期时间设为7天(对应WhatsApp重复推送周期) await redisClient.setEx(`whatsapp:processed:${messageId}`, 7*24*3600, '1');
4. 优化消息发送函数的错误处理
原SendMessageWhatsApp函数未正确处理HTTP响应状态,若发送请求失败(如Token过期、权限不足),不会记录完整错误信息,也没有重试机制,可能导致用户未收到回复,而重复收到推送时会再次发送。
修改后的发送函数:
async function SendMessageWhatsApp(message: any) { try { const data = JSON.stringify(message); const options = { host: "graph.facebook.com", path: `/v16.0/${ID_CELL}/messages`, method: "POST", headers: { "Content-type": "application/json", Authorization: `Bearer ${TOKEN_CLOUD_API_WSP}` } }; return new Promise<void>((resolve, reject) => { const req = https.request(options, res => { let responseBody = ''; res.on("data", d => { responseBody += d; }); res.on("end", () => { // 检查响应状态码 if (res.statusCode && res.statusCode >= 400) { const error = new Error(`Failed to send message: ${res.statusCode} - ${responseBody}`); console.error(error); supabase.from('records').insert([ { message: error.message, status: res.statusCode, task: "Error sending message to Whatsapp", error: responseBody } ]); reject(error); } else { console.log('Message sent successfully:', responseBody); resolve(); } }); }); req.on("error", error => { console.error('Request error:', error); supabase.from('records').insert([ { message: error.message, status: 500, task: "Error sending message to Whatsapp", error: JSON.stringify(error) } ]); reject(error); }); req.write(data); req.end(); }); } catch (error) { console.error('SendMessageWhatsApp error:', error); supabase.from('records').insert([ { message: (error as Error).message, status: 500, task: "Error sending message to Whatsapp", error: JSON.stringify(error) } ]); throw error; } }
5. 检查Webhook订阅事件
确保Webhook仅订阅messages事件,若订阅了message_status等其他事件,可能会收到重复的状态通知,误判为新消息处理。
内容的提问来源于stack exchange,提问作者Izlia

