GopeedLab/gopeed · error

cannot start in current state: %v

Error message

cannot start in current state: %v

What it means

Fetcher.Start() (internal/protocol/http/fetcher.go:574) is a state-machine entry point. It is legal from stateResolving, stateResolved, statePaused, stateSlowStart, stateSteady, and stateError, but the default branch rejects every other state. In practice that means stateIdle (Resolve was never called) or stateDone (the download already finished).

Source

Thrown at internal/protocol/http/fetcher.go:596

	case stateResolved, statePaused:
		// Normal case: resolved or resuming from pause
		return f.doStart()

	case stateResolving:
		// Early start: mark pending and return immediately
		f.startPending.Store(true)
		return nil

	case stateSlowStart, stateSteady:
		// Already downloading, this is a resume from pause
		return f.doStart()

	case stateError:
		// Retry after error: reset and restart
		return f.doStart()

	default:
		return fmt.Errorf("cannot start in current state: %v", state)
	}
}

func (f *Fetcher) doStart() error {
	// Wait for resolve to complete
	<-f.resolvedCh

	state := f.getState()
	if state == stateDone {
		return nil
	}

	// If retrying after error, reset connection states for retry
	if state == stateError {
		// Drain any pending error from doneCh before retry
		select {
		case <-f.doneCh:
		default:

View on GitHub (pinned to 7b7327ffb3)

Solutions

  1. Call Resolve(req, opts) first and only then Start() — the normal lifecycle is Resolve -> Start -> Wait
  2. Never restart a finished fetcher: create a new Fetcher instance for each download attempt
  3. If you see 'cannot start in current state: 0', the fetcher was never resolved; if the value is the done state, the task already completed
  4. In retry loops, reconstruct the fetcher (or use Pause/Start for pause-resume flows) instead of re-Start

Example fix

// before
fetcher.Start() // stateIdle: resolve never ran -> "cannot start in current state"

// after
if err := fetcher.Resolve(req, opts); err != nil {
    return err
}
if err := fetcher.Start(); err != nil {
    return err
}
Defensive patterns

Strategy: validation

Validate before calling

// Enforce the lifecycle in the caller: Resolve before Start, never restart a done fetcher
type dlTask struct {
    started bool
    done    bool
}

func (t *dlTask) Start(f *http.Fetcher, req *base.Request, opts *base.Options) error {
    if t.done || f == nil {
        return errors.New("task finished: build a new fetcher")
    }
    if !t.started {
        if err := f.Resolve(req, opts); err != nil {
            return err
        }
        t.started = true
    }
    return f.Start()
}

Try / catch

if err := fetcher.Start(); err != nil {
    if strings.Contains(err.Error(), "cannot start in current state") {
        // idle: resolve first; done: make a new fetcher
        if err := fetcher.Resolve(req, opts); err != nil {
            return err
        }
        return fetcher.Start()
    }
    return err
}

Prevention

When it happens

Trigger: Calling Start() on a freshly constructed Fetcher without calling Resolve(req, opts) first (state prints as 0 / stateIdle); calling Start() a second time after Wait() returned and the fetcher reached stateDone.

Common situations: Retry wrappers that blindly re-Start a completed fetcher instead of building a new one; race conditions where the caller starts before the resolve goroutine is kicked off; porting code from an older version where Start implied resolve.

Related errors


AI-assisted analysis of GopeedLab/gopeed@7b7327ffb3 (2026-08-16). Data as JSON: /api/errors/db7f252045465d7a. Report an issue: GitHub.