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

  1. Call Create exactly once per Resolve, immediately after Resolve returns; treat its result as the taskId
  2. Make the operation idempotent on the client: if Create fails with this error, re-Resolve the request to get a fresh rrId
  3. Disable/serialize the confirm button while a Create is in flight
  4. 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

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.