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

  1. Get the recorded cause via Registry.SourceError(blobURL) — it returns the first recorded error
  2. Create a fresh source with CreateBlob/CreateOpener and download from the new URL; the old URL is terminally dead
  3. For session-backed sources, call Registry.Acquire(blobURL) before downloading so the session cannot expire mid-download (unclaimedSourceTTL is 10 minutes)
  4. 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

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.