fix(httpserver): bind port synchronously in OnStart so bind errors propagate

Previously ListenAndServe ran entirely in a goroutine, so a port-in-use
error was only logged and the launcher received nil from OnStart — leaving
the application running without an HTTP server.

Replace with net.Listen (synchronous) + srv.Serve(ln) (goroutine). A bind
failure now returns an error from OnStart, which the launcher treats as
fatal and triggers a clean shutdown immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 00:26:15 +00:00
parent 69cea64ea0
commit 5ab0120597

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
@@ -80,7 +81,10 @@ func (s *httpServer) OnInit() error {
return nil
}
// OnStart starts the HTTP server in a background goroutine.
// OnStart binds the TCP listener synchronously so that a port conflict returns
// an error immediately — allowing the launcher to trigger a clean shutdown
// instead of running silently without an HTTP server. The accepted connections
// are then served in a background goroutine.
func (s *httpServer) OnStart() error {
host := s.cfg.Host
if host == "" {
@@ -90,16 +94,23 @@ func (s *httpServer) OnStart() error {
if port == 0 {
port = 8080
}
addr := fmt.Sprintf("%s:%d", host, port)
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("httpserver: bind %s: %w", addr, err)
}
s.srv = &http.Server{
Addr: fmt.Sprintf("%s:%d", host, port),
Addr: addr,
Handler: s.Router,
ReadTimeout: s.cfg.ReadTimeout,
WriteTimeout: s.cfg.WriteTimeout,
IdleTimeout: s.cfg.IdleTimeout,
}
s.logger.Info("httpserver: starting", "addr", s.srv.Addr)
s.logger.Info("httpserver: starting", "addr", addr)
go func() {
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := s.srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("httpserver: fatal error", err)
}
}()