router-for-me/CLIProxyAPI · error

server failed to start: %w

Error message

server failed to start: %w

What it means

Start() launches ListenAndServe in a goroutine and forwards any non-ErrServerClosed failure into errorChan, which WaitForCallback picks up. This means the server can pass the port pre-check but still fail asynchronously moments later (a race where another process grabs the port, or a bind permission error). The error reaches the caller as a wrapped listen failure during WaitForCallback, not from Start itself.

Source

Thrown at internal/auth/claude/oauth_server.go:101

	}

	mux := http.NewServeMux()
	mux.HandleFunc("/callback", s.handleCallback)
	mux.HandleFunc("/success", s.handleSuccess)

	s.server = &http.Server{
		Addr:         fmt.Sprintf(":%d", s.port),
		Handler:      mux,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	s.running = true

	// Start server in goroutine
	go func() {
		if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			s.errorChan <- fmt.Errorf("server failed to start: %w", err)
		}
	}()

	// Give server a moment to start
	time.Sleep(100 * time.Millisecond)

	return nil
}

// Stop gracefully stops the OAuth callback server.
// It performs a graceful shutdown of the HTTP server with a timeout.
//
// Parameters:
//   - ctx: The context for controlling the shutdown process
//
// Returns:
//   - error: An error if the server fails to stop gracefully
func (s *OAuthServer) Stop(ctx context.Context) error {

View on GitHub (pinned to 78f0c4079e)

Solutions

  1. Read the wrapped error text — EADDRINUSE means a port race: retry login, ideally with a fresh --oauth-callback-port.
  2. For permission/FD errors, run on an unprivileged port (>1024) and check ulimit -n.
  3. Serialize login flows so two processes never race for the same callback port.
  4. If it persists, reproduce with `curl http://127.0.0.1:<port>` during login to see what actually answered.
Defensive patterns

Strategy: try-catch

Try / catch

go func() {
    if err := server.Start(); err != nil { log.Fatal(err) }
}()
result, err := server.WaitForCallback(timeout)
if err != nil && strings.Contains(err.Error(), "server failed to start") {
    // async bind failure: port raced — retry with a new port
}

Prevention

When it happens

Trigger: Another process binds the port between the isPortAvailable check and ListenAndServe; binding fails for OS-level reasons (permission on privileged port, FD limits); the goroutine's ListenAndServe returns immediately with EADDRINUSE.

Common situations: High-churn environments where many processes race for ephemeral callback ports; running inside containers with restricted network capabilities; concurrent logins from multiple CI jobs on the same host.

Related errors


AI-assisted analysis of router-for-me/CLIProxyAPI@78f0c4079e (2026-08-15). Data as JSON: /api/errors/9a3ae0d19188fdc8. Report an issue: GitHub.