@@ -6,29 +6,65 @@ import (
66 "fmt"
77 "net"
88 "net/http"
9- "sync/atomic"
9+ "sync"
10+ "time"
1011)
1112
1213// HTTPServer wraps a http.Server, while providing conveniences
1314// like exposing the running state and address.
15+ //
16+ // It can be started with HTTPServer.Start and closed with
17+ // HTTPServer.Stop, HTTPServer.Close and HTTPServer.Shutdown (convenience functions for different gracefulness).
18+ //
19+ // The addr contains both host and port. A 0 port may be used to make the system bind to an available one.
20+ // The resulting address can be retrieved with HTTPServer.Addr or HTTPServer.HTTPEndpoint.
21+ //
22+ // The server may be started, stopped and started back up.
1423type HTTPServer struct {
24+ // mu is the lock used for bringing the server online/offline, and accessing the address of the server.
25+ mu sync.RWMutex
26+
27+ // listener that the server is bound to. Nil if online.
1528 listener net.Listener
16- srv * http.Server
17- closed atomic.Bool
29+
30+ srv * http.Server
31+
32+ // used as BaseContext of the http.Server
33+ srvCtx context.Context
34+ srvCancel context.CancelFunc
35+
36+ config * config
1837}
1938
20- // HTTPOption applies a change to an HTTP server
21- type HTTPOption func (srv * HTTPServer ) error
39+ // NewHTTPServer creates an HTTPServer that serves the given HTTP handler.
40+ // The server is inactive and has to be started explicitly.
41+ func NewHTTPServer (addr string , handler http.Handler , opts ... Option ) * HTTPServer {
42+ cfg := & config {
43+ listenAddr : addr ,
44+ tls : nil ,
45+ handler : handler ,
46+ httpOpts : nil ,
47+ }
48+ cfg .ApplyOptions (opts ... )
49+ return & HTTPServer {config : cfg }
50+ }
2251
23- func StartHTTPServer (addr string , handler http.Handler , opts ... HTTPOption ) (* HTTPServer , error ) {
24- listener , err := net .Listen ("tcp" , addr )
25- if err != nil {
26- return nil , fmt .Errorf ("failed to bind to address %q: %w" , addr , err )
52+ func StartHTTPServer (addr string , handler http.Handler , opts ... Option ) (* HTTPServer , error ) {
53+ out := NewHTTPServer (addr , handler , opts ... )
54+ return out , out .Start ()
55+ }
56+
57+ // Start starts the server, and checks if it comes online fully.
58+ func (s * HTTPServer ) Start () error {
59+ s .mu .Lock ()
60+ defer s .mu .Unlock ()
61+ if s .srv != nil {
62+ return errors .New ("already have existing server" )
2763 }
2864
2965 srvCtx , srvCancel := context .WithCancel (context .Background ())
3066 srv := & http.Server {
31- Handler : handler ,
67+ Handler : s . config . handler ,
3268 ReadTimeout : DefaultTimeouts .ReadTimeout ,
3369 ReadHeaderTimeout : DefaultTimeouts .ReadHeaderTimeout ,
3470 WriteTimeout : DefaultTimeouts .WriteTimeout ,
@@ -37,28 +73,56 @@ func StartHTTPServer(addr string, handler http.Handler, opts ...HTTPOption) (*HT
3773 return srvCtx
3874 },
3975 }
40- out := & HTTPServer {listener : listener , srv : srv }
41- for _ , opt := range opts {
42- if err := opt (out ); err != nil {
76+
77+ if s .config .tls != nil {
78+ srv .TLSConfig = s .config .tls .Config
79+ }
80+
81+ for _ , opt := range s .config .httpOpts {
82+ if err := opt (srv ); err != nil {
4383 srvCancel ()
44- return nil , errors . Join ( fmt .Errorf ("failed to apply HTTP option: %w" , err ), listener . Close () )
84+ return fmt .Errorf ("failed to apply HTTP option: %w" , err )
4585 }
4686 }
47- go func () {
48- err := out .srv .Serve (listener )
87+
88+ listener , err := net .Listen ("tcp" , s .config .listenAddr )
89+ if err != nil {
4990 srvCancel ()
50- // no error, unless ErrServerClosed (or unused base context closes, or unused http2 config error)
51- if errors .Is (err , http .ErrServerClosed ) {
52- out .closed .Store (true )
91+ return fmt .Errorf ("failed to bind to address %q: %w" , s .config .listenAddr , err )
92+ }
93+ s .listener = listener
94+
95+ s .srv = srv
96+ s .srvCtx = srvCtx
97+ s .srvCancel = srvCancel
98+
99+ // cap of 1, to not block on non-immediate shutdown
100+ errCh := make (chan error , 1 )
101+ go func () {
102+ if s .config .tls != nil {
103+ errCh <- s .srv .ServeTLS (s .listener , "" , "" )
53104 } else {
54- panic ( fmt . Errorf ( "unexpected serve error: %w" , err ) )
105+ errCh <- s . srv . Serve ( s . listener )
55106 }
56107 }()
57- return out , nil
108+
109+ // verify that the server comes up
110+ standupTimer := time .NewTimer (10 * time .Millisecond )
111+ defer standupTimer .Stop ()
112+
113+ select {
114+ case err := <- errCh :
115+ s .cleanup ()
116+ return fmt .Errorf ("http server failed: %w" , err )
117+ case <- standupTimer .C :
118+ return nil
119+ }
58120}
59121
60122func (s * HTTPServer ) Closed () bool {
61- return s .closed .Load ()
123+ s .mu .RLock ()
124+ defer s .mu .RUnlock ()
125+ return s .srv == nil
62126}
63127
64128// Stop is a convenience method to gracefully shut down the server, but force-close if the ctx is cancelled.
@@ -73,28 +137,73 @@ func (s *HTTPServer) Stop(ctx context.Context) error {
73137 return nil
74138}
75139
140+ func (s * HTTPServer ) cleanup () {
141+ s .srv = nil
142+ s .listener = nil
143+ s .srvCtx = nil
144+ s .srvCancel = nil
145+ }
146+
76147// Shutdown shuts down the HTTP server and its listener,
77148// but allows active connections to close gracefully.
78149// If the function exits due to a ctx cancellation the listener is closed but active connections may remain,
79150// a call to Close() can force-close any remaining active connections.
80151func (s * HTTPServer ) Shutdown (ctx context.Context ) error {
152+ s .mu .Lock ()
153+ defer s .mu .Unlock ()
154+ if s .srv == nil {
155+ return nil
156+ }
157+ s .srvCancel ()
81158 // closes the underlying listener too.
82- return s .srv .Shutdown (ctx )
159+ err := s .srv .Shutdown (ctx )
160+ if err != nil {
161+ return err
162+ }
163+ s .cleanup ()
164+ return nil
83165}
84166
85167// Close force-closes the HTTPServer, its listener, and all its active connections.
86168func (s * HTTPServer ) Close () error {
169+ s .mu .Lock ()
170+ defer s .mu .Unlock ()
171+ if s .srv == nil {
172+ return nil
173+ }
174+ s .srvCancel ()
87175 // closes the underlying listener too
88- return s .srv .Close ()
176+ err := s .srv .Close ()
177+ if err != nil {
178+ return err
179+ }
180+ s .cleanup ()
181+ return nil
89182}
90183
184+ // Addr returns the address that the server is listening on.
185+ // It returns nil if the server is not online.
91186func (s * HTTPServer ) Addr () net.Addr {
187+ s .mu .RLock ()
188+ defer s .mu .RUnlock ()
189+ if s .listener == nil {
190+ return nil
191+ }
92192 return s .listener .Addr ()
93193}
94194
95- func WithMaxHeaderBytes (max int ) HTTPOption {
96- return func (srv * HTTPServer ) error {
97- srv .srv .MaxHeaderBytes = max
98- return nil
195+ // HTTPEndpoint returns the http(s) endpoint the server is serving.
196+ // It returns an empty string if the server is not online.
197+ func (s * HTTPServer ) HTTPEndpoint () string {
198+ s .mu .RLock ()
199+ defer s .mu .RUnlock ()
200+ if s .listener == nil {
201+ return ""
202+ }
203+ addr := s .listener .Addr ().String ()
204+ if s .config .tls != nil {
205+ return "https://" + addr
206+ } else {
207+ return "http://" + addr
99208 }
100209}
0 commit comments