GopeedLab/gopeed · error
blob source unavailable
Error message
blob source unavailable
What it means
HTTP 410 Gone returned when the source already has a recorded read error (Source.readErr, set by recordSourceError). The registry remembers the first failed open or failed body read — for range-enabled sources after 2 ranged failures (rangeSourceFailureLimit) — and every subsequent GET on that URL returns 410 without invoking the opener again. The check runs before Range parsing, so even a plain GET without a Range header gets 410. A URL in this state never recovers; a new source must be created.
Source
Thrown at internal/blob/registry.go:333
if id == "" {
http.NotFound(w, req)
return
}
src, err := r.getByID(id)
if err != nil {
http.NotFound(w, req)
return
}
meta, open, session := src.acquireOpen()
if open == nil {
http.NotFound(w, req)
return
}
if session != nil {
defer session.Release()
}
if src.sourceError() != nil {
http.Error(w, "blob source unavailable", http.StatusGone)
return
}
start, end, ranged, err := parseRange(req.Header.Get("Range"), meta.Size, meta.Range)
if err != nil {
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
return
}
if !meta.Range {
start, end, ranged = 0, -1, false
}
reader, err := open(req.Context(), OpenRequest{
Offset: start,
End: end,
})
if err != nil {
if req.Context().Err() == nil || !errors.Is(err, req.Context().Err()) {
src.recordSourceError(err, meta.Range)
}View on GitHub (pinned to 7b7327ffb3)
Solutions
- Get the recorded cause via Registry.SourceError(blobURL) — it returns the first recorded error
- Create a fresh source with CreateBlob/CreateOpener and download from the new URL; the old URL is terminally dead
- For session-backed sources, call Registry.Acquire(blobURL) before downloading so the session cannot expire mid-download (unclaimedSourceTTL is 10 minutes)
- Fix or reopen the underlying resource before recreating the source, or the new source will fail the same way
Example fix
// before
for i := 0; i < 3; i++ {
resp, _ := http.Get(blobURL) // every attempt: 410 blob source unavailable
}
// after
if err := reg.SourceError(blobURL); err != nil {
blobURL, err = reg.CreateBlob(data, contentType) // fresh, healthy source
if err != nil {
return err
}
}
resp, err := http.Get(blobURL) Defensive patterns
Strategy: validation
Validate before calling
// pre-check the sticky source error before spending a request
if err := reg.SourceError(blobURL); err != nil {
// this URL will keep answering 410; recreate the source
blobURL, err = reg.CreateBlob(data, contentType)
if err != nil {
return err
}
} Type guard
func sourceHealthy(reg *blob.Registry, raw string) bool {
return reg.SourceError(raw) == nil && reg.IsURL(raw)
} Try / catch
resp, err := client.Get(blobURL)
if err == nil && resp.StatusCode == http.StatusGone { // 410
cause := reg.SourceError(blobURL)
// terminal state: do NOT retry this URL; create a new source instead
_ = cause
} Prevention
- Call Registry.Acquire(blobURL) before long downloads to pin session-backed sources past the 10-minute TTL
- After any 410, consult Registry.SourceError before deciding to retry
- Range-enabled sources stick only after 2 failures — keep ranged reads healthy so successes reset the counter
- Recreate sources from fresh data rather than resurrecting dead URLs
When it happens
Trigger: A previous GET on the same blob URL failed inside open() or during the response body copy (backing file gone, session/engine closed, upstream read error) and the error was recorded; you then retry the same URL. Or, for a Range:true source, two ranged requests fail with no successful ranged read in between.
Common situations: Retry loops hammering a URL whose backing temp file was cleaned up; resuming a download after the owning session was Released, Registry.Close was called, or the 10-minute unclaimedSourceTTL expired; flaky ranged sources that fail twice before a success resets the failure counter.
Related errors
AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16).
Data as JSON: /api/errors/8b1d626107827aca.
Report an issue: GitHub.