LINE Webhook Verify 失败?正确处理空 events 数组
LINE 验证 Webhook URL 时,会发送一个 events 数组为空的 HTTP POST 请求。接收端应先验证签名,再返回 200,而不是读取消息或发送回复。如果处理器默认 events[0] 一定存在,即使正常消息能处理,Verify 测试也可能失败。反过来,Verify 成功也不能证明真实消息已进入队列或客服收件箱。
要点
- 通过验签的
events: []请求是有效的连通性检查,不代表消息丢失。 - 使用 channel secret 对原始请求体校验
x-line-signature,之后再解析 JSON。 - 连通性与业务处理必须分别测试,不能相互替代。
- LINE 原生 Webhook 与 UnifyPort 标准事件采用不同契约。
LINE 的 Verify 按钮究竟发送什么
官方Webhook URL 验证指南提供两种连通性检查方式:Webhook 测试 API,以及 LINE Developers Console 中的 Verify 按钮。它们检查 LINE 能否与接收端通信。对于不包含事件的请求,LINE 要求接收端返回 200。
下面采用 LINE 签名验证示例中的请求体结构,并非客户消息:
{"destination":"U8e742f61d673b39c7fff3cecb7536ef0","events":[]}
其中没有可包含消息、发送者或 reply token 的事件对象。不要根据 destination 创建客户记录,不要生成虚构的消息标识,也不要安排自动回复。如果需要监控,应将此类通信检查与客户消息流量分别记录。
修改 URL 前,先确认入口由谁负责。如果多个工具共享同一个官方账号,请先看多工具 Webhook 检查清单,避免为了排查解析错误,把原本正常的消息流转移到另一个接收端。
先验签,再允许空数组
LINE 的签名验证指南规定:使用 channel secret 作为密钥,对收到的请求体计算 HMAC-SHA256,再以 Base64 编码进行签名比较。这个密钥不是调用发送 API 时使用的 channel access token。
保留接收时的原始字节。先解析 JSON 再序列化,可能改变空格或转义形式,导致验签失败。events 为空也不能省略验签,因为未经授权的请求同样可以构造这种 JSON。
以下 Node.js 辅助函数演示原生 LINE 请求的验签与解析。rawBody 必须是 HTTP 框架保留的原始 Buffer,channelSecret 来自服务端密钥存储。它不是完整服务器,也没有实现持久化队列。
import { createHmac, timingSafeEqual } from 'node:crypto';
export function readLineEvents(rawBody, signature, channelSecret) {
if (!Buffer.isBuffer(rawBody) || !channelSecret) {
throw new Error('Raw body and channel secret are required');
}
const expected = createHmac('sha256', channelSecret)
.update(rawBody)
.digest('base64');
const supplied = Buffer.from(signature ?? '', 'utf8');
const calculated = Buffer.from(expected, 'utf8');
if (supplied.length !== calculated.length ||
!timingSafeEqual(supplied, calculated)) {
return { status: 401 };
}
try {
const payload = JSON.parse(rawBody.toString('utf8'));
if (!payload || !Array.isArray(payload.events)) {
return { status: 400 };
}
return { status: 200, events: payload.events };
} catch {
return { status: 400 };
}
}
这里的 400、401 是示例自行选择的防御性响应,不是 LINE API 错误码。接入时应按以下顺序处理:
- 将
x-line-signature请求头与未修改的请求体传入函数。 - 拒绝无效请求,不创建业务任务。
- 验签成功且
events.length === 0时,直接返回 HTTP200。 - 数组非空时,先持久化收到的事件,再确认接收;耗时业务放到异步任务中。存储失败时,不要仅因函数返回了该状态就发送
200。
LINE 建议异步处理事件。持久化和 worker 的实现仍由应用负责。应遍历数组,而非只读取第一个元素。
按失败环节排查
| 观察结果 | 下一步检查 |
|---|---|
| 应用没有收到请求 | 配置的域名与路径、TLS、入口路由、代理日志 |
| 签名验证失败 | channel secret 是否正确、原始字节是否保留、请求头是否传递 |
| 已验签的空请求触发异常 | 是否无条件读取首个事件、消息或 reply token |
| 应用显示成功,但 Verify 失败 | 公网入口实际返回的状态与请求耗时 |
| Verify 成功,收件箱却没有消息 | 真实事件的持久化、路由与 worker 执行 |
不要仅因一个只适配消息的处理器拒绝空数组,就轮换凭证或更换 Webhook 入口。如果真实事件已经到达,但缺少 reply token,应另行排查:LINE standby 模式指南解释的是回复权限。空验证请求连事件对象都没有,不存在可供检查的事件 mode。
分别验证连通性与消息处理
以下是建议的验收场景,不是已经完成的生产测试结果:
- 签名正确的空请求返回
200,不创建客户消息或发送任务。 - 相同空请求体在缺失签名或签名错误时被拒绝。
- 签名正确的非空请求,在确认接收前完成事件持久化。
- Verify 成功后,向目标官方账号发送一条受控测试消息,并确认它进入预期收件箱流程。
- 存储失败时,不会因为验签成功就被内部标记为处理完成。
诊断日志仅保留必要信息,例如验签结果、事件数量、HTTP 状态和观察时间。不要常规记录 channel secret、reply token 或客户消息正文。
不要混用 UnifyPort 接收契约
UnifyPort 的非官方接口通过 LINE 授权流程连接 LINE 账号,并不是官方账号的原生 Webhook 测试服务。
其 Webhook 投递契约使用 X-Device-Signature:配置 signing_secret 后,对 X-Device-Timestamp、一个点和原始请求体拼接后的内容计算 HMAC-SHA256,并以十六进制编码。标准信封包含 id、type、provider、account_id、occurred_at 和 data,不是 LINE 的 events 数组。
为两种契约使用不同路由或明确的接收配置。不能把 UnifyPort 请求中没有原生 events 字段,当成 LINE 的空验证请求。LINE Verify 成功不会验证 UnifyPort 投递、账号授权或标准消息处理。如果必须使用 LINE 官方账号身份,应保留官方 Messaging API 路径。
常见问题
events: [] 是错误吗?
在 LINE 的 Webhook URL 验证请求中不是。验签后返回 200,无需创建消息任务。字段缺失或类型错误与空数组不是同一回事。
为什么正常消息能处理,Verify 却失败?
常见实现问题是默认每个请求都有首条消息事件。应核对实际响应与异常日志;网络传输或签名错误也可能导致测试失败。
验证成功是否证明机器人能回复?
不能。它检查的是接收端通信,不是存储、worker 执行、回复资格或发送 API 的执行结果。
下一步与参考资料
官方账号接入先运行 LINE Webhook URL 检查,再追踪一条真实测试消息。独立的 UnifyPort 接入则应实现其文档规定的 Webhook 契约。
官方资料核对日期:2026-10-28。
让消息接入变成一条稳定的产品管线。
先用统一 API 跑通发送,再用标准事件把所有入站消息接回业务系统。