gocolly/colly · error

ErrRetryBodyUnseekable

ErrRetryBodyUnseekable

Error message

Retry Body Unseekable

What it means

ErrRetryBodyUnseekable is returned by Request.Retry when the request has a non-nil Body that does not implement io.ReadSeeker (request.go:160). Retrying a POST requires rewinding the body to its start; if the body cannot seek, colly cannot safely resend it. Declared at colly.go:252.

Source

Thrown at colly.go:252

	ErrNoURLFiltersMatch = errors.New("No URLFilters match")
	// ErrRobotsTxtBlocked is the error type for robots.txt errors
	ErrRobotsTxtBlocked = errors.New("URL blocked by robots.txt")
	// ErrNoCookieJar is the error type for missing cookie jar
	ErrNoCookieJar = errors.New("Cookie jar is not available")
	// ErrNoPattern is the error type for LimitRules without patterns
	ErrNoPattern = errors.New("No pattern defined in LimitRule")
	// ErrEmptyProxyURL is the error type for empty Proxy URL list
	ErrEmptyProxyURL = errors.New("Proxy URL list is empty")
	// ErrAbortedAfterHeaders is the error returned when OnResponseHeaders aborts the transfer.
	ErrAbortedAfterHeaders = errors.New("Aborted after receiving response headers")
	// ErrAbortedBeforeRequest is the error returned when OnResponseHeaders aborts the transfer.
	ErrAbortedBeforeRequest = errors.New("Aborted before Do Request")
	// ErrQueueFull is the error returned when the queue is full
	ErrQueueFull = errors.New("Queue MaxSize reached")
	// ErrMaxRequests is the error returned when exceeding max requests
	ErrMaxRequests = errors.New("Max Requests limit reached")
	// ErrRetryBodyUnseekable is the error when retry with not seekable body
	ErrRetryBodyUnseekable = errors.New("Retry Body Unseekable")
)

var envMap = map[string]func(*Collector, string){
	"ALLOWED_DOMAINS": func(c *Collector, val string) {
		c.AllowedDomains = strings.Split(val, ",")
	},
	"CACHE_DIR": func(c *Collector, val string) {
		c.CacheDir = val
	},
	"DETECT_CHARSET": func(c *Collector, val string) {
		c.DetectCharset = isYesString(val)
	},
	"DISABLE_COOKIES": func(c *Collector, _ string) {
		c.backend.Client.Jar = nil
	},
	"DISALLOWED_DOMAINS": func(c *Collector, val string) {
		c.DisallowedDomains = strings.Split(val, ",")
	},

View on GitHub (pinned to 17d1d6ca92)

Solutions

  1. Build the request body from a seekable type such as bytes.NewReader(payload) or bytes.Buffer
  2. If the body cannot be seekable, re-issue the request with c.Post instead of calling Retry
  3. Only call Retry for GET/HEAD requests (nil body), which never hit this error
  4. Use errors.Is(err, colly.ErrRetryBodyUnseekable) to fall back to manual re-posting

Example fix

// before
r.Body = ioutil.NopCloser(strings.NewReader(form)) // not a ReadSeeker
// after
r.Body = bytes.NewReader([]byte(form)) // implements io.ReadSeeker
Defensive patterns

Strategy: validation

Validate before calling

if r.Body != nil {
    if _, ok := r.Body.(io.ReadSeeker); !ok {
        // rebuild body as *bytes.Reader before enabling Retry
    }
}

Type guard

func bodySeekable(r *colly.Request) bool {
    if r.Body == nil { return true }
    _, ok := r.Body.(io.ReadSeeker)
    return ok
}

Try / catch

if err := r.Request.Retry(); err != nil {
    if errors.Is(err, colly.ErrRetryBodyUnseekable) {
        // fall back to re-issuing via c.Post with a fresh bytes.Reader body
        return
    }
    log.Println(err)
}

Prevention

When it happens

Trigger: Calling r.Request.Retry() from OnError/OnResponse for a request whose Body was set to a non-seekable reader (e.g. an io.Reader from a pipe or streaming source) instead of bytes.Reader/bytes.Buffer.

Common situations: POST requests with bodies built from os.Stdin, network streams, or multipart readers; retries triggered by 429/5xx handling on such requests.

Related errors


AI-assisted analysis of gocolly/colly@17d1d6ca92 (2026-08-30). Data as JSON: /api/errors/6f442afdbde21c76. Report an issue: GitHub.