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
- Find and stop the holder: lsof -nP -iTCP:51121 -sTCP:LISTEN (or ss -ltnp 'sport = :51121'), then retry
- Set cfg.Port to a known-free high port before calling LoginBrowser
- Set cfg.Port to 0 — the listener reports actualPort, and redirectURI is rebuilt from it, so ephemeral binding works
- 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
- Probe the port before login; fall back to Port=0 (ephemeral) on conflict
- Never run two logins with the same configured port
- Kill stale listeners (lsof -iTCP:<port> -sTCP:LISTEN) after aborted logins
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
- state mismatch
- error fetching models: %w
- manual input canceled
- could not find authorization code in input
- authentication timed out after 5 minutes
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/ef9eef1e6764b3f0.
Report an issue: GitHub.