直接用net/http.listenandserve会失败,因标准库不处理签名校验,需手动读取原始body、解析x-hub-signature-256头、用hmac.new比对;常见错误是重复读取已eof的r.body导致解码失败。

为什么直接用 net/http.ListenAndServe 会失败
GitHub 发送的 Webhook 请求必须被正确验证签名,否则你的服务即使能收到请求,也无法确认它真的来自 GitHub。Go 标准库的 http.ServeMux 本身不处理签名校验,你得自己读取原始 body、解析 X-Hub-Signature-256 头,并用 hmac.New 对比。常见错误是:用 r.Body 读一次后又调用 json.NewDecoder(r.Body).Decode() —— 此时 body 已 EOF,解码失败且无提示。
关键点:
- 必须在验证前完整读取并缓存
r.Body,例如用io.ReadAll(r.Body) -
X-Hub-Signature-256值格式为sha256=xxx,需截取后半段再 hex 解码 - 密钥必须和 GitHub Webhook 设置页填的一致,且不能硬编码在代码里(建议从环境变量读取)
- 响应状态码必须是 200,否则 GitHub 会重试(最多 3 次),可能造成重复处理
如何安全解析 push 事件的 payload
GitHub 的 push 事件结构嵌套深、字段多,直接用 map[string]interface{} 解析易出错,也难做类型检查。推荐定义结构体,但要注意几个实际坑点:
常见错误现象:json: cannot unmarshal string into Go struct field PushEvent.head_commit.id of type int64 —— 因为某些字段(如 head_commit.id)在不同触发场景下可能是字符串或数字,GitHub 文档未保证类型稳定。
实操建议:
生成Markdown格式的Git提交摘要报告。适用于需以Markdown输出仓库详细统计的场景。输出包含项目信息、分支、提交统计、作者统计、最近提交记录及提交类型统计等,均以Markdown呈现。
- 用
json.RawMessage延迟解析不确定字段,例如HeadCommit json.RawMessage `json:"head_commit"` - 对必用字段(如
repository.full_name,after)单独提取并校验非空 - 不要依赖
commits数组长度判断是否为首次推送——created: true才表示新建分支 - 注意
before和after是 commit SHA,不是 ref 名;ref字段才是refs/heads/main这种格式
如何避免并发处理导致的状态冲突
一个仓库频繁提交时,GitHub 可能在几秒内发出多个 push 事件,如果每个请求都独立触发构建或部署,可能因资源竞争或顺序错乱导致失败。Go 默认的 HTTP handler 是并发执行的,没有内置排队机制。
简单可靠的方案是加一层轻量级队列 + 单 goroutine 消费:
- 用
chan []byte接收原始 payload,HTTP handler 只负责入队和返回 200 - 启动一个单独 goroutine 从 channel 读取、校验、解析、执行业务逻辑
- 若业务操作耗时(如调用外部 API),可加 context 超时控制,避免阻塞后续事件
- 不建议用 mutex 锁整个 handler —— 会把并发降为串行,且无法解决跨进程部署时的冲突
如何本地调试 Webhook 而不用 ngrok
GitHub 要求 Webhook 地址是公网 HTTPS,但本地开发时没必要每次改代码都推到服务器。用 curl 模拟请求是最直接的调试方式,关键是复现真实头信息和签名。
步骤:
- 先用 GitHub Webhook 页面的 “Redeliver” 功能复制一次真实请求的
X-Hub-Signature-256和 body(注意 base64 解码后保存为文件) - 用
openssl dgst -sha256 -hmac "your-secret" -hex payload.json生成本地签名,对比是否一致 - 用
curl -X POST -H "Content-Type: application/json" -H "X-GitHub-Event: push" -H "X-Hub-Signature-256: sha256=xxx" --data-binary @payload.json http://localhost:8080/webhook测试 - 注意:GitHub 的
X-GitHub-Delivery是 UUID,用于去重,你的服务可以记录它防止重复处理
Webhook 的核心不在“收到”,而在“确认来源 + 安全解析 + 控制执行节奏”。很多线上问题其实源于没意识到 GitHub 会重试、签名校验漏了前缀、或者把未验证的 JSON 直接反序列化进业务结构体。










