登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go Colly OnError 中如何区分传输失败与 HTTP 错误

来源:17golang原创

时间:2026-10-13 15:54:40 485浏览 收藏

我在用 Colly 抓取列表页时,最容易误判的一类日志是“请求失败”:有时是真正的 DNS、连接或超时问题,有时只是服务端返回了 404、429 或 500。两者都可能进入 OnError,但处理方式完全不同。实用的判断顺序是:先看 Response 是否存在,再看 StatusCode 是否代表 HTTP 状态异常,最后用 errors.As 和 errors.Is 判断传输错误的具体原因。

官方地址:https://github.com/gocolly/colly

不要用“err != nil”直接等同于网络断线。对于 HTTP 错误,Colly 通常仍能把状态码放在 Response 中;对于传输失败,响应可能为空。默认配置下,非 2xx 会触发 OnError,如果启用 ParseHTTPErrorResponse,则可以把非 2xx 当作可解析响应继续走响应处理。
碰到这个场景你直接记这几个判断点就行
  • Response != nil 且 StatusCode >= 300,优先按 HTTP 错误处理。
  • Response == nil 或没有有效状态码时,再检查 *url.Error、超时和取消原因。
  • 不要把 4xx、5xx 无条件重试;先区分客户端请求、限流、服务端暂时故障和永久不存在。

先看 OnError 的两个参数

Colly 的错误回调类型是 func(*Response, error)。第一个参数不是“请求一定成功”的证明,而是 Colly 在能够拿到 HTTP 响应或已经建立部分响应上下文时提供的对象;第二个参数是当前阶段的错误。官方源码在错误处理时会把缺失的响应补成带有 Request 和 Context 的对象,但这不等于它拥有真实的状态码和响应体。

Colly OnError 中 Collector、Request、Response、error 以及传输失败和 HTTP 状态异常之间的静态关系说明图
图1:OnError 两个回调参数及错误来源的静态说明图,不是运行截图。

因此,日志代码不要只打印 err.Error()。先把 URL、方法、状态码和错误类型放到同一条记录里,后面才能回答“服务器拒绝了请求”还是“请求根本没有拿到响应”。

c := colly.NewCollector()

c.OnError(func(resp *colly.Response, err error) {
	// Response 可能没有真实的 HTTP 状态,默认值不能被当成成功响应。
	status := 0
	requestURL := ""
	if resp != nil {
		status = resp.StatusCode
		if resp.Request != nil && resp.Request.URL != nil {
			requestURL = resp.Request.URL.String()
		}
	}

	// 先记录原始 error,再按状态码决定它属于哪一类。
	log.Printf("colly error url=%s status=%d err=%v", requestURL, status, err)
})

这里的 status == 0 只是“当前没有可用 HTTP 状态码”的日志标记,不是 Colly 定义的状态码。它可能对应连接失败,也可能对应请求检查、重定向或回调阶段的其他错误,仍要结合 error 判断。

用状态码和错误包装区分来源

我更愿意把分类拆成两层:第一层只回答“有没有 HTTP 响应”,第二层才回答“没有响应时,传输失败的具体原因是什么”。这样不会因为某个库把底层错误包了一层,就误把所有情况归到同一类。

func classifyCollyError(resp *colly.Response, err error) string {
	// 只要拿到明确的非 2xx 状态,就先按 HTTP 错误归类。
	if resp != nil && resp.StatusCode >= 300 {
		return "http-error"
	}

	// url.Error 常见于 HTTP 客户端包装的请求失败,保留它便于日志展开。
	var urlErr *url.Error
	if errors.As(err, &urlErr) {
		if errors.Is(urlErr.Err, context.DeadlineExceeded) {
			return "transport-timeout"
		}
		return "transport-error"
	}

	// 未匹配到包装类型时仍保留一个通用类别,避免丢失未知错误。
	if errors.Is(err, context.DeadlineExceeded) {
		return "transport-timeout"
	}
	return "other-error"
}

这段判断的重点不是类别名称,而是顺序:HTTP 状态码优先于错误字符串;底层原因优先用 errors.Is 和 errors.As,不要依赖“timeout”“connection refused”这类可能变化的文本。对于 Response == nil 的情况,url.Error 往往能补充请求方法和目标地址。

还要留意一个容易忽略的边界:OnError 并不只可能接收网络阶段的错误。HTML/XML 处理回调返回错误时,Colly 也会调用错误处理路径;此时 Response 可能存在且状态码是 2xx。若业务要专门统计传输失败,应把“状态码分类”和“解析/业务回调错误”分开计数。

非 2xx 要不要继续读取响应体

默认情况下,Colly 只按成功响应继续处理;官方配置中的 ParseHTTPErrorResponse 用来允许解析非 2xx 响应。两种策略都合理,取决于目标站点把错误体当不当作业务数据。

// 需要读取 404/429/500 响应体时,显式开启非 2xx 解析。
c := colly.NewCollector(colly.ParseHTTPErrorResponse())

c.OnResponse(func(resp *colly.Response) {
	// 这里可以读取状态码和错误页返回的结构化内容。
	log.Printf("received status=%d url=%s", resp.StatusCode, resp.Request.URL)
})

c.OnError(func(resp *colly.Response, err error) {
	// 开启该选项后,单纯的 HTTP 状态异常不再自动走这里。
	log.Printf("request failed status=%d err=%v", resp.StatusCode, err)
})

开启后不要误解为“所有非 2xx 都成功了”。它只是改变 Colly 的回调路径,让响应可以进入 OnResponse、HTML 或 XML 处理;业务层仍然要检查 StatusCode。例如,429 需要保留限流信息并延迟,404 可能应该记录为资源不存在,500 才可能进入有限次数的重试。

Colly OnError 按状态码、url.Error、超时原因连接到重试和结构化日志的静态分类说明图
图2:错误分类、可恢复动作与诊断记录之间的静态关系说明图,不是监控截图。

把重试和日志决策放在分类之后

在我的抓取任务里,重试是分类之后的动作,而不是 OnError 一进来就调用 Retry。网络超时、临时连接失败可以有条件重试;参数错误、禁止访问、明显的 404 不应靠重试解决。HTTP 429 是否重试,则要结合响应头和站点约定,不能只看数字。

c.OnError(func(resp *colly.Response, err error) {
	category := classifyCollyError(resp, err)

	// 记录状态码和分类,便于把一次重试与原始请求关联起来。
	log.Printf("fetch category=%s status=%d err=%v", category, statusCode(resp), err)

	// 只对明确的传输类暂时故障重试,避免 404 或参数错误形成重试风暴。
	if category == "transport-timeout" || category == "transport-error" {
		if resp != nil && resp.Request != nil {
			_ = resp.Request.Retry()
		}
	}
})

func statusCode(resp *colly.Response) int {
	// 用 0 表示没有可用的真实 HTTP 状态码。
	if resp == nil {
		return 0
	}
	return resp.StatusCode
}

示例只展示决策位置,没有把重试次数、退避和幂等性藏起来。正式任务还应在 Context 或外部计数器中限制次数,并记录第一次失败的原因;如果站点对同一 URL 的重试有频率限制,还要把退避时间和域名级并发一起纳入设计。

异步模式和回调错误别混在一起

启用 colly.Async() 后,Visit 返回成功更多地表示任务已经提交,不能用它判断远端请求最终成功。要等 c.Wait() 让异步请求完成,并把最终结果交给 OnResponse、OnError 和 OnScraped 等回调。

c := colly.NewCollector(colly.Async())

c.OnError(func(resp *colly.Response, err error) {
	// 异步场景的最终错误在回调中处理,不依赖 Visit 的即时返回值。
	log.Printf("async error status=%d err=%v", statusCode(resp), err)
})

if err := c.Visit(targetURL); err != nil {
	// 这里主要处理任务提交阶段的错误,例如 URL 或访问规则不合法。
	log.Printf("submit visit failed: %v", err)
}

// 等待已经提交的网络任务完成,再结束本轮采集。
c.Wait()

最后可以用下面的速查表收束判断:

观察到的信号优先归类处理建议
Response 存在且状态码为 404/429/500HTTP 错误保留状态码;按业务决定记录、限流等待或有限重试
Response 为空,error 能展开为 *url.Error传输错误继续检查超时、取消、连接拒绝等原因
状态码为 2xx,但解析回调报错处理阶段错误单独统计解析问题,不算成网络断线
启用 ParseHTTPErrorResponse 后收到非 2xx可解析的 HTTP 响应在响应回调里检查状态码,不依赖 OnError

常见问题

OnError 的 Response 为 nil 就一定是网络错误吗?

不能绝对这样说。它只能说明当前没有可用的 HTTP 响应对象,仍需结合错误类型判断请求检查、取消或其他阶段错误。

为什么返回 404 时没有进入 OnResponse?

默认配置只按成功响应继续处理,非 2xx 会在错误处理路径中回调。若确实需要读取错误响应体,可以使用 colly.ParseHTTPErrorResponse(),但仍要在业务层检查状态码。

HTTP 500 是否应该自动 Retry?

不应该无条件自动重试。先确认接口是否幂等、服务端是否暂时故障、站点是否有频率限制,再设置次数和退避;4xx 通常更需要修正请求,而不是重复发送。

异步模式下如何知道所有请求结束?

调用 c.Wait() 等待已提交的异步任务,并在 OnError、OnResponse 等回调中收集最终结果。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>