{"record":{"id":"8b1d626107827aca","repo":"GopeedLab/gopeed","slug":"blob-source-unavailable","errorCode":null,"errorMessage":"blob source unavailable","messagePattern":"blob source unavailable","errorType":"http","errorClass":null,"httpStatus":410,"severity":"error","filePath":"internal/blob/registry.go","lineNumber":333,"sourceCode":"\tif id == \"\" {\n\t\thttp.NotFound(w, req)\n\t\treturn\n\t}\n\tsrc, err := r.getByID(id)\n\tif err != nil {\n\t\thttp.NotFound(w, req)\n\t\treturn\n\t}\n\tmeta, open, session := src.acquireOpen()\n\tif open == nil {\n\t\thttp.NotFound(w, req)\n\t\treturn\n\t}\n\tif session != nil {\n\t\tdefer session.Release()\n\t}\n\tif src.sourceError() != nil {\n\t\thttp.Error(w, \"blob source unavailable\", http.StatusGone)\n\t\treturn\n\t}\n\tstart, end, ranged, err := parseRange(req.Header.Get(\"Range\"), meta.Size, meta.Range)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)\n\t\treturn\n\t}\n\tif !meta.Range {\n\t\tstart, end, ranged = 0, -1, false\n\t}\n\treader, err := open(req.Context(), OpenRequest{\n\t\tOffset: start,\n\t\tEnd:    end,\n\t})\n\tif err != nil {\n\t\tif req.Context().Err() == nil || !errors.Is(err, req.Context().Err()) {\n\t\t\tsrc.recordSourceError(err, meta.Range)\n\t\t}","sourceCodeStart":315,"sourceCodeEnd":351,"githubUrl":"https://github.com/GopeedLab/gopeed/blob/7b7327ffb30816273a74b142cccc0bc10c5a4c67/internal/blob/registry.go#L315-L351","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nfor i := 0; i < 3; i++ {\n\tresp, _ := http.Get(blobURL) // every attempt: 410 blob source unavailable\n}\n\n// after\nif err := reg.SourceError(blobURL); err != nil {\n\tblobURL, err = reg.CreateBlob(data, contentType) // fresh, healthy source\n\tif err != nil {\n\t\treturn err\n\t}\n}\nresp, err := http.Get(blobURL)","handlingStrategy":"validation","validationCode":"// pre-check the sticky source error before spending a request\nif err := reg.SourceError(blobURL); err != nil {\n\t// this URL will keep answering 410; recreate the source\n\tblobURL, err = reg.CreateBlob(data, contentType)\n\tif err != nil {\n\t\treturn err\n\t}\n}","typeGuard":"func sourceHealthy(reg *blob.Registry, raw string) bool {\n\treturn reg.SourceError(raw) == nil && reg.IsURL(raw)\n}","tryCatchPattern":"resp, err := client.Get(blobURL)\nif err == nil && resp.StatusCode == http.StatusGone { // 410\n\tcause := reg.SourceError(blobURL)\n\t// terminal state: do NOT retry this URL; create a new source instead\n\t_ = cause\n}","preventionTips":["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"],"tags":["http-410","blob-registry","sticky-error","state"],"backgroundTag":null,"analyzedSha":"7b7327ffb30816273a74b142cccc0bc10c5a4c67","analyzedAt":"2026-08-16T02:51:03.250Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}