🔍 Duplicate Code Pattern: HTTP Server Lifecycle in cmd/root.go and cmd/proxy.go
Part of duplicate code analysis: #7078
Summary
Both run() in internal/cmd/root.go and runProxy() in internal/cmd/proxy.go contain the same HTTP server serve/wait/shutdown sequence: launch the server in a goroutine, wait for context cancellation, then perform a graceful shutdown with a 5-second timeout. The pattern is ~12 lines duplicated across two command handlers.
Duplication Details
Pattern: Inline HTTP server serve-and-graceful-shutdown sequence
- Severity: Medium
- Occurrences: 2 (
run in root.go, runProxy in proxy.go)
- Locations:
internal/cmd/root.go (lines 439–460)
internal/cmd/proxy.go (lines 312–329)
root.go (lines 439–460):
go func() {
if err := httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server error: %v", err)
cancel()
}
}()
// ...
<-ctx.Done()
debugLog.Print("Shutdown signal received, initiating graceful shutdown")
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
}
proxy.go (lines 312–329):
if err := httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server error: %v", err)
cancel()
}
// ...
<-ctx.Done()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), proxyShutdownTimeout)
defer shutdownCancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
logger.LogError("shutdown", "HTTP server shutdown error: %v", err)
return err
}
Note: root.go hard-codes 5*time.Second while proxy.go uses the named constant proxyShutdownTimeout = 5 * time.Second — they are the same value.
Impact Analysis
- Maintainability: Changing the shutdown timeout, adding structured logging to the error path, or switching to a different graceful shutdown strategy requires editing two separate files.
- Bug Risk:
root.go swallows the httpServer.Shutdown error silently; proxy.go logs it and returns it. Any future decision to unify error handling must be done in both places.
- Code Bloat: ~12 lines × 2 locations = ~24 lines of nearly identical code.
Refactoring Recommendations
- Extract a
serveAndWait helper in internal/cmd/ (e.g., internal/cmd/serve.go)
// serveAndWait launches httpServer.Serve(listener) in a goroutine,
// waits for ctx cancellation, then gracefully shuts the server down
// within timeout. The cancel func is called if Serve returns an
// unexpected error.
func serveAndWait(ctx context.Context, cancel context.CancelFunc,
httpServer *http.Server, listener net.Listener,
timeout time.Duration) error {
go func() {
if err := httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server error: %v", err)
cancel()
}
}()
<-ctx.Done()
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), timeout)
defer shutdownCancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
return err
}
return nil
}
- Extract to: new file
internal/cmd/serve.go
- Estimated effort: 60–90 minutes
- Benefits: Single place to update timeout, logging, and error handling; consistent behaviour between gateway and proxy subcommands
Implementation Checklist
Parent Issue
See parent analysis report: #7078
Related to #7078
Generated by Duplicate Code Detector · sonnet46 6.1M · ◷
🔍 Duplicate Code Pattern: HTTP Server Lifecycle in cmd/root.go and cmd/proxy.go
Part of duplicate code analysis: #7078
Summary
Both
run()ininternal/cmd/root.goandrunProxy()ininternal/cmd/proxy.gocontain the same HTTP server serve/wait/shutdown sequence: launch the server in a goroutine, wait for context cancellation, then perform a graceful shutdown with a 5-second timeout. The pattern is ~12 lines duplicated across two command handlers.Duplication Details
Pattern: Inline HTTP server serve-and-graceful-shutdown sequence
runinroot.go,runProxyinproxy.go)internal/cmd/root.go(lines 439–460)internal/cmd/proxy.go(lines 312–329)root.go (lines 439–460):
proxy.go (lines 312–329):
Note:
root.gohard-codes5*time.Secondwhileproxy.gouses the named constantproxyShutdownTimeout = 5 * time.Second— they are the same value.Impact Analysis
root.goswallows thehttpServer.Shutdownerror silently;proxy.gologs it and returns it. Any future decision to unify error handling must be done in both places.Refactoring Recommendations
serveAndWaithelper ininternal/cmd/(e.g.,internal/cmd/serve.go)internal/cmd/serve.goImplementation Checklist
internal/cmd/serve.gowithserveAndWaithelperrun()inroot.goto use the helperrunProxy()inproxy.goto use the helper5*time.Secondliteral inroot.govsproxyShutdownTimeoutconstant inproxy.go)make test-allto verify no regressionParent Issue
See parent analysis report: #7078
Related to #7078