GopeedLab/gopeed · error
invalid resource id
Error message
invalid resource id
What it means
Downloader.Create(rrId) (pkg/download/downloader.go:526-539) looks rrId up in fetcherCache, which is populated only by a successful Resolve (downloader.go:458-460) and consumed destructively: Create deletes the entry (deferred delete at lines 533-537), so each resolve-result id is single-use. An unknown or already-used rrId yields 'invalid resource id'.
Source
Thrown at pkg/download/downloader.go:531
opts := ir.Opts
if opts == nil {
opts = req.Opts
}
taskId, err := d.CreateDirect(ir.Req, opts.Clone())
if err != nil {
return nil, err
}
taskIds = append(taskIds, taskId)
}
return taskIds, nil
}
func (d *Downloader) Create(rrId string) (taskId string, err error) {
d.fetcherMapLock.RLock()
fetcher, ok := d.fetcherCache[rrId]
d.fetcherMapLock.RUnlock()
if !ok {
return "", errors.New("invalid resource id")
}
defer func() {
d.fetcherMapLock.Lock()
delete(d.fetcherCache, rrId)
d.fetcherMapLock.Unlock()
}()
return d.doCreate(fetcher, nil)
}
// Patch modifies task-specific data based on the protocol.
// For HTTP protocol, it can modify Request info.
// For BT protocol, it can modify SelectFiles.
func (d *Downloader) Patch(id string, req *base.Request, opts *base.Options) error {
task := d.GetTask(id)
if task == nil {
return ErrTaskNotFound
}
if err := func() error {View on GitHub (pinned to 7b7327ffb3)
Solutions
- Call Create exactly once per Resolve, immediately after Resolve returns; treat its result as the taskId
- Make the operation idempotent on the client: if Create fails with this error, re-Resolve the request to get a fresh rrId
- Disable/serialize the confirm button while a Create is in flight
- Never persist or reuse rrIds across sessions — they are ephemeral handles
Example fix
// before
rr, _ := d.Resolve(req, nil)
_, err1 := d.Create(rr.ID)
_, err2 := d.Create(rr.ID) // "invalid resource id"
// after
rr, err := d.Resolve(req, nil)
if err != nil { return "", err }
taskId, err := d.Create(rr.ID)
if errors.Is(err, /* invalid resource id */ errInvalidRR) || err != nil {
rr, err = d.Resolve(req, nil) // re-resolve, then Create once
if err != nil { return "", err }
return d.Create(rr.ID)
} Defensive patterns
Strategy: validation
Validate before calling
rr, err := downloader.Resolve(req, opts)
if err != nil { return "", err }
// rr.ID is single-use: create immediately, exactly once
taskId, err := downloader.Create(rr.ID)
if err != nil {
// do NOT reuse rr.ID; re-resolve if needed
} Try / catch
taskId, err := downloader.Create(rrId)
if err != nil && strings.Contains(err.Error(), "invalid resource id") {
// rrId unknown or consumed: re-Resolve to get a fresh id, then Create once
rr, rerr := downloader.Resolve(req, opts)
if rerr != nil { return "", rerr }
return downloader.Create(rr.ID)
} Prevention
- Create exactly once, immediately after Resolve (the cache entry is deleted on use)
- Guard double-clicks/retries in UI so Create is not reissued with the same rrId
- Never persist rrIds; they live only in the Downloader's in-memory cache
When it happens
Trigger: Calling Create twice with the same rrId (first call succeeds and evicts the cache entry); calling Create without a prior successful Resolve; using an rrId from a different Downloader instance or from before a restart (the cache is in-memory only); racing two Creates on the same rrId — one wins, the other gets this error.
Common situations: Client retries the create call after a network blip and the first attempt actually succeeded; UI 'confirm download' button double-clicked; splitting resolve and create across processes/sessions; confusing the ResolveResult.ID with the taskId or the blob URL id.
Related errors
AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16).
Data as JSON: /api/errors/13fcc3e1da2c2b29.
Report an issue: GitHub.