// Package httpprocess provides [process.Runner] wrapper for [http.Server].
package httpprocess import ( ) // Server returns a [process.Runner] instance for the given HTTP server and // network listener. func ( *http.Server, net.Listener) process.Runner { var httptrack.ConnTracker := httptrack.Wrap(, &) := &nilCloserListener{Listener: } return process.Leaf( func( context.Context) error { := .BaseContext .BaseContext = func( net.Listener) context.Context { return } := .Serve() if errors.Is(, http.ErrServerClosed) { = nil } if == nil { = .CloseError() } // Wait until all connections are actually closed (i.e. until all // per-connection goroutines return). // // Otherwise, process.Leaf would cancel the context on return, and // the cancellation would propagate to in-flight HTTP handlers. We // avoid that because our graceful shutdown model assumes the // context only expires during forced shutdown. // // Note that this can still happen if the shutdown function returns, // but by that point, graceful shutdown would have already failed. .Wait() .BaseContext = .ConnState = return }, func( context.Context) error { // Shutdown and Close forward error from net.Listener.Close. // We already capture this error via nilCloserListener. _ = .Shutdown() _ = .Close() return nil }, ) } // nilCloserListener is a wrapper around a [net.Listener] that ensures the // underlying listener is closed exactly once and returns nil error. type nilCloserListener struct { net.Listener once sync.Once err error } // Close closes the underlying [net.Listener] exactly once. It always returns // nil error. func ( *nilCloserListener) () error { .once.Do(func() { .err = .Listener.Close() }) return nil } // CloseError returns Close error from the underlying [net.Listener]. func ( *nilCloserListener) () error { return .err }