GopeedLab/gopeed · error

connection %d failed: retries=%d, status=%d

Error message

connection %d failed: retries=%d, status=%d

What it means

Produced in onDownloadComplete (internal/protocol/http/fetcher.go:1537) when the download did not finish and at least one connection ended in connFailed after exhausting its retries. This variant is chosen when the connection's last error is a RequestError carrying an HTTP status code; that code is included in the message. 403 is deliberately skipped because it usually signals a server-side per-IP connection limit, not a real failure.

Source

Thrown at internal/protocol/http/fetcher.go:1537

			allChunksComplete = false
			break
		}
	}

	// If total downloaded matches file size, consider it a success regardless of connection failures
	downloadComplete := f.meta.Res.Size > 0 && totalDownloaded >= f.meta.Res.Size

	// Check for any errors, but ignore 403 (server connection limit) errors if download completed
	var finalErr error
	if !downloadComplete && !allChunksComplete {
		for _, conn := range f.connections {
			if conn.State == connFailed && conn.failed {
				// Skip 403 errors (server connection limit) - these are expected when exceeding server's limit
				if re := extractRequestError(conn.lastErr); re != nil && re.Code == 403 {
					continue
				}
				if re := extractRequestError(conn.lastErr); re != nil {
					finalErr = fmt.Errorf("connection %d failed: retries=%d, status=%d", conn.ID, conn.retryTimes, re.Code)
				} else if conn.lastErr != nil {
					finalErr = fmt.Errorf("connection %d failed: retries=%d, err=%v", conn.ID, conn.retryTimes, conn.lastErr)
				} else {
					finalErr = fmt.Errorf("connection %d failed: retries=%d", conn.ID, conn.retryTimes)
				}
				break
			}
		}
	}
	f.connMu.Unlock()

	// Close the file before signaling completion
	// This ensures the file handle is released before Wait() returns
	f.fileMu.Lock()
	if f.file != nil {
		f.file.Close()
		f.file = nil
	}

View on GitHub (pinned to 7b7327ffb3)

Solutions

  1. Match the status: 404/410 means the URL is gone — re-resolve the resource; 401/403 with signed URLs means re-sign; 5xx means retry later
  2. Verify with a HEAD/GET that the URL still returns 200 and Accept-Ranges: bytes before retrying a multi-connection download
  3. Reduce the connection count (some servers throttle parallel ranges)
  4. Retry the whole task from a fresh Fetcher instead of re-Start

Example fix

// before
err := fetcher.Wait() // "connection 2 failed: retries=5, status=416"

// after
// preflight before a multi-connection retry
resp, err := http.Head(url)
if err == nil && resp.StatusCode == 200 && resp.Header.Get("Accept-Ranges") == "bytes" {
    // safe to retry with range connections
} else {
    // fall back to a single connection or re-resolve the URL
}
Defensive patterns

Strategy: retry

Validate before calling

// Preflight the URL before a multi-connection download
func supportsRanges(url string) bool {
    resp, err := http.Head(url)
    if err != nil || resp.StatusCode != http.StatusOK {
        return false
    }
    return resp.Header.Get("Accept-Ranges") == "bytes"
}

Try / catch

err := fetcher.Wait()
if err != nil && strings.Contains(err.Error(), "failed: retries=") && strings.Contains(err.Error(), "status=") {
    // extract the status and decide: re-resolve for 401/404, backoff-retry for 5xx
    return retryFresh(req, opts, 3) // new Fetcher per attempt
}

Prevention

When it happens

Trigger: A chunk connection repeatedly received 404 (file removed mid-download), 416 (range no longer satisfiable after the file changed), 500/502/503 (server errors), or 401 (signed URL expired) until the retry budget ran out.

Common situations: Expired pre-signed URLs on CDN hosts, servers that reject many parallel range requests, files deleted or replaced while a long download was paused, or a proxy stripping Range support so chunked workers fail.

Related errors


AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16). Data as JSON: /api/errors/545ea0aef6f24d69. Report an issue: GitHub.