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

从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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 20:33:26