2 Commits

Author SHA1 Message Date
1801754a9b chore(release): v0.9.2 2026-03-25 00:38:43 +00:00
5ab0120597 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>
2026-03-25 00:26:15 +00:00
2 changed files with 27 additions and 4 deletions

View File

@@ -5,6 +5,17 @@ All notable changes to this module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.9.2] - 2026-03-25
### Fixed
- `OnStart` now binds the TCP listener synchronously via `net.Listen` before
launching the serve goroutine. A port-in-use (or any other bind) error is
returned immediately from `OnStart`, allowing the launcher to treat it as a
fatal startup failure and trigger a clean shutdown. Previously the error only
appeared in a log line while the application continued running without an
HTTP server.
## [0.9.1] - 2026-03-21
### Fixed
@@ -33,5 +44,6 @@ and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
- No middleware is installed by default; the full middleware stack is composed explicitly via `WithMiddleware` at construction time, keeping the stack visible and ordering unambiguous in the application source
- chi was chosen as the underlying router because it uses stdlib `http.Handler` throughout, making it fully compatible with `httpmw` middleware and `httputil` handler adapters without any wrapper code at the boundary
[0.9.2]: https://code.nochebuena.dev/go/httpserver/releases/tag/v0.9.2
[0.9.1]: https://code.nochebuena.dev/go/httpserver/releases/tag/v0.9.1
[0.9.0]: https://code.nochebuena.dev/go/httpserver/releases/tag/v0.9.0

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)
}
}()