sipeed/picoclaw · error

starting callback server on port %d: %w

Error message

starting callback server on port %d: %w

What it means

listenOAuthCallback failed at pkg/auth/oauth.go:110 — net.Listen("tcp", "127.0.0.1:<cfg.Port>") could not bind the loopback callback listener. Default configs use port 51121. The wrapped error is almost always EADDRINUSE (another process, or a previous login still running, holds the port) or EACCES (unprivileged user asking for a port below 1024).

Source

Thrown at pkg/auth/oauth.go:110

	pkce, err := GeneratePKCE()
	if err != nil {
		return nil, fmt.Errorf("generating PKCE: %w", err)
	}

	state, err := GenerateState()
	if err != nil {
		return nil, fmt.Errorf("generating state: %w", err)
	}

	redirectURI := oauthCallbackRedirectURI(cfg.Port)
	callbackPort := cfg.Port
	var resultCh <-chan callbackResult

	if !opts.NoBrowser {
		callbackResultCh := make(chan callbackResult, 1)
		listener, actualPort, err := listenOAuthCallback(cfg.Port)
		if err != nil {
			return nil, fmt.Errorf("starting callback server on port %d: %w", cfg.Port, err)
		}

		redirectURI = oauthCallbackRedirectURI(actualPort)
		callbackPort = actualPort
		resultCh = callbackResultCh

		server := &http.Server{Handler: oauthCallbackHandler(state, callbackResultCh)}
		go func() {
			_ = server.Serve(listener)
		}()
		defer func() {
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			_ = server.Shutdown(ctx)
		}()
	}

	authURL := buildAuthorizeURL(cfg, pkce, state, redirectURI)

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Find and stop the holder: lsof -nP -iTCP:51121 -sTCP:LISTEN (or ss -ltnp 'sport = :51121'), then retry
  2. Set cfg.Port to a known-free high port before calling LoginBrowser
  3. Set cfg.Port to 0 — the listener reports actualPort, and redirectURI is rebuilt from it, so ephemeral binding works
  4. Ensure only one login flow runs at a time per machine

Example fix

// before: fixed port that may collide
cfg := auth.OAuthProviderConfig{Port: 51121, ...}

// after: probe, else fall back to ephemeral
if ln, err := net.Listen("tcp", "127.0.0.1:51121"); err != nil {
    _ = ln.Close()
    cfg.Port = 0 // ephemeral; redirect URI is rebuilt from actualPort
} else {
    _ = ln.Close()
    cfg.Port = 51121
}
Defensive patterns

Strategy: validation

Validate before calling

// ensure the callback port is free (or go ephemeral) before login
func preparePort(cfg *auth.OAuthProviderConfig) {
    ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", cfg.Port))
    if err != nil {
        cfg.Port = 0 // ephemeral; redirect URI is rebuilt from actualPort
        return
    }
    _ = ln.Close()
}

Try / catch

cred, err := auth.LoginBrowserWithOptions(cfg, opts)
if err != nil && strings.Contains(err.Error(), "starting callback server") {
    // EADDRINUSE almost always: kill the holder or switch port
    fmt.Fprintln(os.Stderr, "callback port busy — free it or set Port=0")
    return err
}

Prevention

When it happens

Trigger: Two concurrent logins with the same cfg.Port; a stale bot process still holding 51121; cfg.Port set to a privileged port (<1024) while running as non-root; port excluded by the OS (ip_local_reserved_ports / blackhole).

Common situations: Re-running login while the first attempt's callback server never shut down; port 51121 colliding with another local dev service; running the login helper inside a restricted container.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/ef9eef1e6764b3f0. Report an issue: GitHub.