{"record":{"id":"6044ea3df79d5da5","repo":"GopeedLab/gopeed","slug":"blob-source-revoked","errorCode":null,"errorMessage":"blob source revoked","messagePattern":"blob source revoked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/blob/registry.go","lineNumber":33,"sourceCode":"\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst urlPathPrefix = \"/__blob/\"\n\nconst rangeSourceFailureLimit = 2\n\n// unclaimedSourceTTL bounds how long a session-backed source may keep its\n// engine alive without ever being claimed by a download task.\nvar unclaimedSourceTTL = 10 * time.Minute\n\nvar (\n\tErrInvalidURL      = errors.New(\"invalid blob url\")\n\tErrInvalidOptions  = errors.New(\"invalid blob options\")\n\tErrSourceNotFound  = errors.New(\"blob source not found\")\n\tErrSourceRevoked   = errors.New(\"blob source revoked\")\n\tErrSourceClosed    = errors.New(\"blob source closed\")\n\tErrRangeNotAllowed = errors.New(\"blob range not allowed\")\n)\n\ntype SessionRef interface {\n\tRetain()\n\tRelease()\n}\n\ntype OpenRequest struct {\n\tOffset int64\n\tEnd    int64\n}\n\ntype OpenFunc func(ctx context.Context, req OpenRequest) (io.ReadCloser, error)\n\ntype CreateOptions struct {\n\tContentType string","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/GopeedLab/gopeed/blob/7b7327ffb30816273a74b142cccc0bc10c5a4c67/internal/blob/registry.go#L15-L51","documentation":"Returned when a blob source still exists in the map but is marked revoked. Registry.get checks src.revoked (registry.go:626-631), and Source.acquireTask/releaseTask return it directly (registry.go:403-405,420-422). Revocation is how the registry retires a source: explicit Revoke, the final task Release, expiry of an unclaimed session-backed source after unclaimedSourceTTL (10 minutes), or Registry.Close.","triggerScenarios":"Calling Acquire/Release after Revoke(blobURL); calling Release for the last task reference and then any further blob operation on the same URL; creating a session-backed source and Acquiring it more than 10 minutes after creation; any blob call racing with Registry.Close (Close marks every source revoked).","commonSituations":"Extension/UI code holding onto a blob URL after the download finished (final Release already revoked it); revoking a source on cancel but a queued re-try path still referencing the old URL; slow test suites exceeding the 10-minute TTL between CreateOpener and Acquire.","solutions":["Consider the source dead: create a new one and rebind the task's URL","Acquire the source before starting the download task, not after, so the unclaimed TTL timer is cancelled (acquireTask stops it)","Avoid re-using a blob URL after you have called Release for its last reference or Revoke","If seen right after shutdown starts, it is benign racing with Registry.Close — skip the operation instead of retrying"],"exampleFix":"// before\n_ = registry.Release(blobURL)\nerr := registry.Acquire(blobURL) // ErrSourceRevoked\n\n// after\n_ = registry.Release(blobURL)\nblobURL, err = registry.CreateOpener(open, opts) // fresh source, fresh URL\nif err != nil { return err }\nerr = registry.Acquire(blobURL)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := registry.Acquire(blobURL); err != nil {\n    if errors.Is(err, blob.ErrSourceRevoked) {\n        // source was revoked (Revoke/final Release/TTL/Close): recreate it\n        blobURL, err = registry.CreateOpener(open, opts)\n        if err != nil { return err }\n    }\n}","preventionTips":["Cancel the unclaimed TTL by Acquiring before the download starts, not after","Treat Revoke and final Release as terminal events; drop all cached references to the URL","During shutdown, ignore blob errors after calling Registry.Close"],"tags":["blob","lifecycle","revoked","registry"],"backgroundTag":null,"analyzedSha":"7b7327ffb30816273a74b142cccc0bc10c5a4c67","analyzedAt":"2026-08-16T02:51:03.250Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}