The authz user command starts a local HTTP server to handle the OAuth2 authorization
code grant flow. A browser is opened, the user authenticates, and the browser redirects
back to the local server with the authorization code. The server exchanges it for a token
and sends the result through an unbuffered channel.
There are two issues in the original shutdown logic:
Shutdown()is called withcontext.Background(), which has no timeout- A deadlock can occur between the handler trying to write to a channel and
Shutdown()waiting for the handler to return
The login server creates unbuffered channels (ims-go/login/server.go:91-92):
resCh = make(chan *ims.TokenResponse)
errCh = make(chan error)An unbuffered channel blocks the writer until a reader is ready to receive.
-
The
selectinims/authz_user.go:137-145waits on three cases:select { case serr = <-server.Error(): log.Println("The IMS HTTP handler returned an error message.") case resp = <-server.Response(): log.Println("The IMS HTTP handler returned a message.") case <-time.After(time.Minute * 5): fmt.Fprintf(os.Stderr, "Timeout reached waiting for the user to finish the authentication ...\n") serr = fmt.Errorf("user timed out") }
-
Browser callback arrives. The handler writes to
resCh(ims-go/login/result.go:35-40):select { case h.resCh <- result: // Result sent. case <-r.Context().Done(): // Request cancelled. }
-
Our
selectreads fromserver.Response()(which returnsresCh). Both sides proceed. -
Shutdown()is called. No in-flight handlers, so it completes immediately.
-
The
selectinims/authz_user.go:137-145is waiting on the three cases. -
The 5-minute timeout fires first. We leave the
selectand reachShutdown()(ims/authz_user.go:147-150). -
Now nobody is reading from
resChorerrCh. -
The browser callback arrives late (the user finished auth just after the timeout). The HTTP handler chain reaches
resultHandler.ServeHTTP(ims-go/login/result.go:27), processes the token exchange successfully, and hits (ims-go/login/result.go:35-40):select { case h.resCh <- result: // Result sent. case <-r.Context().Done(): // Request cancelled. }
-
h.resCh <- resultblocks — unbuffered channel, no reader. -
r.Context().Done()— the request context is controlled byhttp.Server.Shutdowndoes not cancel request contexts while waiting for handlers to return. So this case doesn't fire either. -
The handler is stuck in this select forever (
ims-go/login/result.go:35-40). -
Shutdown()(ims-go/login/server.go:143-148) callss.server.Shutdown(ctx)which waits for in-flight handlers to return. It waits for step 7 to complete. It never does.func (s *Server) Shutdown(ctx context.Context) error { defer close(s.errCh) defer close(s.resCh) return s.server.Shutdown(ctx) }
-
defer close(s.resCh)anddefer close(s.errCh)(ims-go/login/server.go:144-145) never execute becauses.server.Shutdownnever returns. -
Circular dependency:
Shutdownwaits for the handler, the handler waits for someone to read the channel, and nobody is reading.
The deadlock doesn't prevent the CLI from exiting. With a timeout on Shutdown, the
context expires, Shutdown returns an error, and execution continues. The stuck handler
goroutine is cleaned up when the process exits. For a long-running server this would be
a real resource leak; for a CLI it's cosmetic (the user sees an error message about the
shutdown timeout instead of a clean exit).
Two changes in ims/authz_user.go:
Replace context.Background() (no deadline) with a 10-second timeout:
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err = server.Shutdown(shutdownCtx); err != nil {
return "", fmt.Errorf("error shutting down the local server: %w", err)
}The defer cancel() releases the internal timer resources if Shutdown completes
before the deadline. This is idiomatic Go — go vet warns if the cancel function
from context.WithTimeout is not called.
Start goroutines that read from both channels before calling Shutdown:
go func() {
for range server.Response() {
}
}()
go func() {
for range server.Error() {
}
}()This breaks the deadlock:
- Drainers start reading from
resChanderrCh Shutdownis called and waits for in-flight handlers- If a handler writes to
resCh(ims-go/login/result.go:36), the drainer reads it. The handler unblocks and returns. Shutdownsees all handlers are done. It callsclose(s.resCh)andclose(s.errCh)(ims-go/login/server.go:144-145).for rangeexits on a closed channel. The drainer goroutines return.
Everyone cleans up.