从Fastify迁移到Express.js后2Hire Webhook URL无法触发
2Hire Webhook迁移Express后触发失败排查
问题背景
近期将处理2Hire Webhook的Fastify应用迁移至Express.js,但Webhook URL无法按预期触发,收到以下错误:
{ "message": { "code": "CHALLENGE_ERROR", "errorId": "2c31cx-x-x-x-x82034a925", "details": { "cause": "CHALLENGE_FAILED" } } }
核心实现代码
const express = require('express'); const bodyParser = require('body-parser'); const crypto = require('crypto'); const dotenv = require('dotenv'); dotenv.config(); const app = express(); const SECRET = process.env.SECRET; const PORT = 8080; const signalNames = new Set(["online", "position", "distance_covered", "autonomy_percentage", "autonomy_meters", "*"]); function isSignal(signal) { return signalNames.has(signal); } function validateTopic(topic) { const parts = topic.split(":"); return parts.length === 4 && parts[0] === "vehicle" && parts[2] === "generic" && isSignal(parts[3]); } function generateSignature(message, secret) { const hmac = crypto.createHmac('sha256', secret); hmac.update(message, 'utf8'); return `sha256=${hmac.digest('hex')}`; } app.use(bodyParser.json({ verify: (req, res, buf) => { req.rawBody = buf.toString(); } })); app.get('/listener', (req, res) => { const { 'hub.mode': mode, 'hub.topic': topic, 'hub.challenge': challenge } = req.query; if (!validateTopic(topic) || mode !== 'subscribe') { res.status(400).send('Validation error'); } else { res.send(challenge); } }); app.post('/listener', (req, res) => { const signature = req.headers['x-hub-signature']; const computedSignature = generateSignature(req.rawBody, SECRET); if (!validateTopic(req.body.topic) || signature !== computedSignature) { res.status(401).send('Invalid signature or topic'); } else { console.log("Webhook Received: ", req.body); res.send('Webhook received'); } }); app.listen(PORT, () => { console.log(`Server listening on http://localhost:${PORT}`); });
可能的排查方向
- Topic验证逻辑过严:
validateTopic强制要求topic为4段格式,但2Hire的topic可能存在其他合法格式。可以先临时注释掉topic验证逻辑,测试挑战是否能通过,再根据实际收到的topic调整验证规则。 - 原始请求体处理偏差:Express的
body-parser在verify回调中转换buf为utf8字符串时,可能和2Hire发送的原始字节流存在差异。尝试直接保存原始buffer(req.rawBuffer = buf;),并在签名生成时使用该buffer而非字符串:function generateSignature(buffer, secret) { const hmac = crypto.createHmac('sha256', secret); hmac.update(buffer); return `sha256=${hmac.digest('hex')}`; } - 签名验证细节错误:确认SECRET与2Hire控制台配置完全一致(注意大小写、特殊字符)。部分服务要求签名基于原始字节流生成,而非UTF-8字符串,上述修改可覆盖这种情况。
- 响应格式不符合要求:订阅挑战的GET请求需返回纯文本格式的challenge值,确保
res.send(challenge)未添加任何额外内容(如空格、HTML标签)。 - 网络与环境限制:确保Express服务公网可达(本地测试可使用ngrok等工具),检查防火墙、反向代理是否修改了请求头或请求体,导致签名验证失败。
内容的提问来源于stack exchange,提问作者Internazionaleauto
相关产品推荐
相关产品推荐

