GoogleContainerTools/skaffold · error

Forward() called before kubectl forwarder was started

Error message

Forward() called before kubectl forwarder was started

What it means

KubectlForwarder.forward checks an atomic `started` flag before starting a port-forward loop; if Forward() was called before the forwarder's Start() routine was launched, the kubectl process plumbing (base command, kill channel) is not initialized, so it pushes this error onto errChan instead of panicking. It is a lifecycle/ordering guard for the forwarder's internal state machine.

Source

Thrown at pkg/skaffold/kubernetes/portforward/kubectl_forwarder.go:102

	l := log.Entry(parentCtx)
	resourceName := ""
	if pfe != nil {
		resourceName = pfe.resource.Name
	}
	l.Tracef("KubectlForwarder.Forward(%s): waiting on errChan", resourceName)
	select {
	case <-parentCtx.Done():
		l.Tracef("KubectlForwarder.Forward(%s): parentCtx canceled, returning nil error", resourceName)
		return nil
	case err := <-errChan:
		l.Tracef("KubectlForwarder.Forward(%s): got error on errChan, returning: %+v", resourceName, err)
		return err
	}
}

func (k *KubectlForwarder) forward(ctx context.Context, pfe *portForwardEntry, errChan chan error) {
	if atomic.LoadInt32(&k.started) == 0 {
		errChan <- fmt.Errorf("Forward() called before kubectl forwarder was started")
		return
	}
	var notifiedUser bool
	defer deferFunc()

	for {
		pfe.terminationLock.Lock()
		if pfe.terminated {
			log.Entry(ctx).Debugf("port forwarding %v was cancelled...", pfe)
			pfe.terminationLock.Unlock()
			errChan <- nil
			return
		}
		pfe.terminationLock.Unlock()

		if !isPortFree(util.Loopback, pfe.localPort) {
			// Assuming that Skaffold brokered ports don't overlap, this has to be an external process that started
			// since the dev loop kicked off. We are notifying the user in the hope that they can fix it

View on GitHub (pinned to a1189de023)

Solutions

  1. Call the forwarder's Start()/Run method before invoking Forward()
  2. Ensure port-forward entries are only submitted after the forwarder signals readiness
  3. In tests, construct the forwarder the same way production code does, including the Start step

Example fix

// before
fwd := NewKubectlForwarder(out, entrypoints)
fwd.Forward(ctx, entry) // panics/errors: not started
// after
fwd := NewKubectlForwarder(out, entrypoints)
go fwd.Run(ctx, kubectl)
// wait for readiness, then:
fwd.Forward(ctx, entry)
Defensive patterns

Strategy: validation

Validate before calling

// only submit port-forward entries after the forwarder is running
select {
case <-forwarderReady: // channel closed once Run/Start has begun
	forwarder.Forward(ctx, entry)
case <-time.After(5 * time.Second):
	log.Fatal("kubectl forwarder did not start in time")
}

Try / catch

errChan := make(chan error, 1)
go func() { forwarder.Forward(ctx, entry) }()
select {
case err := <-errChan:
	if strings.Contains(err.Error(), "called before kubectl forwarder was started") {
		// re-initialize: start forwarder, then re-Forward
	}
case <-ctx.Done():
}

Prevention

When it happens

Trigger: Calling Forward() on a KubectlForwarder instance without having called its Start() method first (or before the started goroutine has set the started flag), then supplying a portForwardEntry.

Common situations: Embedding KubectlForwarder in custom tooling and forgetting to call Start(); calling Forward() immediately after construction in tests; race where port-forward entries are queued before the forwarder's Run loop starts.

Related errors


AI-assisted analysis of GoogleContainerTools/skaffold@a1189de023 (2026-09-05). Data as JSON: /api/errors/26e091696c9c93ac. Report an issue: GitHub.