Skip to content

auth login: panic on repeated invocation due to http.DefaultServeMux registration #51

@yuyan0629

Description

@yuyan0629

Summary

InitiateBrowserAuth in internal/utils/http_request/common/token.go has three issues that can break the auth login flow on the Web Experimentation side:

  1. Uses the global http.DefaultServeMux via http.HandleFunc, which panics with http: multiple registrations for /auth/callback when the function is invoked more than once in the same process (e.g. re-login, tests, library use).
  2. The HTTP server is never proactively shut down after the callback is received; it keeps running until the 5-minute timeout goroutine fires, leaking a goroutine and a port binding.
  3. log.Fatalf is called from goroutines for non-fatal conditions (normal shutdown returns http.ErrServerClosed; server.Shutdown may return a non-fatal error), which can kill the whole process unexpectedly.

Location

internal/utils/http_request/common/token.go:126-159

func InitiateBrowserAuth(username, clientID, clientSecret string) (models.TokenResponse, error) {
    ...
    server := &http.Server{
        Addr: "127.0.0.1:8010",
    }
    ...
    go func() {
        http.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
            handleCallback(w, r, codeChan)
        })

        if err := server.ListenAndServe(); err != nil {
            log.Fatalf("Error starting callback server: %s", err)
        }
    }()

    go func() {
        select {
        case <-time.After(5 * time.Minute):
            if err := server.Shutdown(context.Background()); err != nil {
                log.Fatalf("Server forced to shutdown: %s", err)
            }
        }
    }()

    code := <-codeChan
    ...

Expected behavior

Calling abtasty-cli auth login multiple times in the same session, or using InitiateBrowserAuth programmatically, should work reliably.

Actual behavior

  • Second and subsequent calls panic with http: multiple registrations for /auth/callback because http.HandleFunc registers on the global DefaultServeMux, which is shared and persistent across calls.
  • Even on a single successful call, the bound port 127.0.0.1:8010 remains occupied until the 5-minute timer goroutine fires, preventing a quick re-login.
  • When server.Shutdown is eventually called, ListenAndServe returns http.ErrServerClosed. The current code treats this as fatal and calls log.Fatalf, terminating the process.

Suggested fix

Use a dedicated http.ServeMux per call, and stop treating normal shutdown as fatal:

mux := http.NewServeMux()
mux.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
    handleCallback(w, r, codeChan)
})

server := &http.Server{
    Addr:    "127.0.0.1:8010",
    Handler: mux,
}

serverErr := make(chan error, 1)
go func() {
    if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        serverErr <- err
    }
}()

// ... wait for code or timeout ...

// Always shut down the server before returning, not just on timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = server.Shutdown(ctx)

Additionally, replacing log.Fatalf calls inside goroutines with error returns (via a channel) would make the function safer to use programmatically.

Environment

  • Version: v1.6.0
  • Platform: macOS (Darwin)
  • Context: observed while automating AB Tasty management operations via the public API; encountered the panic when retrying auth login in the same session.

Reported by Yuya Suzuki from GAPRISE Inc. (official AB Tasty reseller in Japan).
Happy to provide more detail or test a fix if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions