默认 gin.recovery() 会让爬虫请求“静默失败”,因其捕获 panic 后返回固定 html 响应(text/html),导致爬虫解析 json 失败且无 error code 判断依据;同时可能与鉴权/限流中间件冲突,引发多次写 header panic,故必须禁用默认 recovery 并自定义前置 customrecovery 中间件。

为什么默认 gin.Recovery() 会让爬虫请求“静默失败”
爬虫请求通常不带浏览器 UA、不走 Cookie 会话,也不渲染 JS,所以更容易触发 panic(比如空指针解包 JSON 字段、goroutine 泄漏、第三方库 panic),而默认 gin.Recovery() 一捕获就返回 HTML 页面——爬虫收到 text/html 响应体,解析 JSON 失败,又没 code 字段可判断错误类型,直接丢弃响应或重试死循环。
更麻烦的是:它和你写的鉴权/限流中间件共存时,可能在 c.AbortWithStatusJSON() 之后又被 gin.Recovery() 再写一次 header,抛出 http: multiple response.WriteHeader calls panic,导致整个请求链卡住。
- 必须显式禁用默认 recovery:
r.Use(gin.RecoveryWithWriter(io.Discard))或干脆不调gin.Default(),改用gin.New() - 自定义
CustomRecovery()必须放在r.Use()最前面,确保它第一个接管 panic -
CustomRecovery()里defer中的return不能省,否则c.Next()还会继续执行,可能二次写响应 - 日志必须用
log.Printf("PANIC: %+v", err),不是fmt.Printf,否则堆栈信息全丢
如何让爬虫请求过 Token 校验但跳过 session 和 CSRF
爬虫请求一般走 API key 或 Bearer Token,不走 cookie 登录态,所以不能套用 Web 端那套 session + CSRF 中间件。强行加进去,会导致 c.MustGet("user_id") panic 或 csrf.Token(c) 返回空字符串,最终 403 Forbidden。
正确做法是按路径区分:给爬虫接口打标签(如 /api/v1/crawl/*),再用 Gin 的路由组 + 中间件组合控制:
- 定义专用中间件:
TokenAuth()只校验Authorization: Bearer xxx或X-API-Key头,不依赖session - 对爬虫路由组单独挂载:
crawlGroup := r.Group("/api/v1/crawl"); crawlGroup.Use(TokenAuth()) - Web 路由组仍用完整鉴权:
webGroup := r.Group("/admin"); webGroup.Use(SessionAuth(), CSRF()) - 避免在
TokenAuth()里调c.Error(),直接c.AbortWithStatusJSON(401, ...),因为它是终端型中间件,后面不该再有业务逻辑
怎么拦截高频爬虫请求并动态降级
爬虫请求往往短平快、高并发、低延迟容忍度,不像人工操作有鼠标停顿。靠固定 QPS 限流(如 gin-contrib/limiter)容易误伤正常批量请求,也防不住 IP 池轮换。
推荐组合策略:IP + User-Agent + 请求路径三元组计数 + 动态响应降级:
- 用
redis.Incr()统计"limit:ip:" + c.ClientIP() + ":ua:" + c.GetHeader("User-Agent") + ":path:" + c.Request.URL.Path,TTL 设为 60 秒 - 超过阈值(如 30 次/60s)时,不直接
429,而是c.Header("X-RateLimit-Remaining", "0"); c.AbortWithStatusJSON(200, map[string]interface{}{"code": 1005, "message": "request throttled", "data": nil})—— 返回成功状态码 + 业务码,避免被爬虫框架当网络错误重试 - 对已标记“可疑”的 IP,在后续请求中跳过耗时中间件(如日志、trace 注入),直接走轻量路径
- 注意:不要用内存计数(如
sync.Map),多实例部署下无法共享状态
为什么 c.Errors 不能直接用于爬虫错误统一输出
很多开发者以为 c.Error(err) 就等于“抛错”,其实它只是把 err 推进 c.Errors 队列,不中断流程、不写响应、甚至不记录日志。爬虫请求一旦在某个中间件里 c.Error(ErrInvalidParam),但后续没中间件读取 c.Errors 并转成 JSON,这个错误就彻底消失了——前端收不到任何提示,只看到空响应或超时。
真正起作用的是“错误兜底中间件”,它必须放在所有其他中间件之后(即 r.Use(...); r.Use(ErrorHandler())):
-
ErrorHandler()里必须用if len(c.Errors) > 0判断,不是err != nil - 错误类型要能提取 HTTP 状态码,建议实现
interface{ Status() int },否则 fallback 到500 - 别直接用
c.Errors.Last().Error()当响应内容,敏感路径、SQL 片段会泄漏;应统一映射到预设code字段,如1001(参数错误)、1005(限流) - 爬虫场景下,建议响应结构始终含
code(业务码)、status(HTTP 状态码)、request_id(用于日志追踪),例如:{"code":1005,"message":"too many requests","status":200,"request_id":"req_abc123"}
c.Abort() 和 c.AbortWithStatusJSON() 的协作边界——谁该终止、谁该透传、谁负责兜底,稍有错位就会出现响应头重复写或静默丢请求。爬虫流量不像人,不会“点刷新重试”,它要么拿到数据,要么永远沉默。











