{"record":{"id":"ef9eef1e6764b3f0","repo":"sipeed/picoclaw","slug":"starting-callback-server-on-port-d-w","errorCode":null,"errorMessage":"starting callback server on port %d: %w","messagePattern":"starting callback server on port (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/auth/oauth.go","lineNumber":110,"sourceCode":"\tpkce, err := GeneratePKCE()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"generating PKCE: %w\", err)\n\t}\n\n\tstate, err := GenerateState()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"generating state: %w\", err)\n\t}\n\n\tredirectURI := oauthCallbackRedirectURI(cfg.Port)\n\tcallbackPort := cfg.Port\n\tvar resultCh <-chan callbackResult\n\n\tif !opts.NoBrowser {\n\t\tcallbackResultCh := make(chan callbackResult, 1)\n\t\tlistener, actualPort, err := listenOAuthCallback(cfg.Port)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"starting callback server on port %d: %w\", cfg.Port, err)\n\t\t}\n\n\t\tredirectURI = oauthCallbackRedirectURI(actualPort)\n\t\tcallbackPort = actualPort\n\t\tresultCh = callbackResultCh\n\n\t\tserver := &http.Server{Handler: oauthCallbackHandler(state, callbackResultCh)}\n\t\tgo func() {\n\t\t\t_ = server.Serve(listener)\n\t\t}()\n\t\tdefer func() {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\n\t\t\tdefer cancel()\n\t\t\t_ = server.Shutdown(ctx)\n\t\t}()\n\t}\n\n\tauthURL := buildAuthorizeURL(cfg, pkce, state, redirectURI)","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/auth/oauth.go#L92-L128","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: fixed port that may collide\ncfg := auth.OAuthProviderConfig{Port: 51121, ...}\n\n// after: probe, else fall back to ephemeral\nif ln, err := net.Listen(\"tcp\", \"127.0.0.1:51121\"); err != nil {\n    _ = ln.Close()\n    cfg.Port = 0 // ephemeral; redirect URI is rebuilt from actualPort\n} else {\n    _ = ln.Close()\n    cfg.Port = 51121\n}","handlingStrategy":"validation","validationCode":"// ensure the callback port is free (or go ephemeral) before login\nfunc preparePort(cfg *auth.OAuthProviderConfig) {\n    ln, err := net.Listen(\"tcp\", fmt.Sprintf(\"127.0.0.1:%d\", cfg.Port))\n    if err != nil {\n        cfg.Port = 0 // ephemeral; redirect URI is rebuilt from actualPort\n        return\n    }\n    _ = ln.Close()\n}","typeGuard":null,"tryCatchPattern":"cred, err := auth.LoginBrowserWithOptions(cfg, opts)\nif err != nil && strings.Contains(err.Error(), \"starting callback server\") {\n    // EADDRINUSE almost always: kill the holder or switch port\n    fmt.Fprintln(os.Stderr, \"callback port busy — free it or set Port=0\")\n    return err\n}","preventionTips":["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"],"tags":["network","oauth","ports","concurrency","login"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}