package server import ( "context" "errors" "net" "net/http" "net/netip" "strconv" "time" ) const ( // httpReadTimeout is the maximum duration for reading the // entire request, including the body. httpReadTimeout = 10 * time.Second // httpWriteTimeout is the maximum duration before timing out // writes of the response. It must stay above the router's // requestTimeout (60s, in routes.go) so the middleware timeout // fires first and returns a clean 503, rather than the transport // cutting the connection at the socket write deadline. httpWriteTimeout = 65 * time.Second // httpMaxHeaderBytes is the maximum number of bytes the // server will read parsing the request headers. httpMaxHeaderBytes = 1 << 20 ) // listenAddr renders the address the HTTP listener binds. // // The host half is always present: an empty host would be the // wildcard, and the whole point of BIND_ADDRESS is that binding every // interface is a choice the operator makes rather than one the // process makes for them. Config guarantees a literal, so // JoinHostPort's bracketing is enough to make IPv6 well formed. func (s *Server) listenAddr() string { return net.JoinHostPort( s.params.Config.BindAddress, strconv.Itoa(s.params.Config.Port), ) } // listenNetwork returns the network the HTTP listener binds on, "tcp4" // for an IPv4 BindAddress and "tcp" for any other. // // On "tcp", Go binds 0.0.0.0 with a socket that answers IPv6 as well, // exactly as it binds ::. "tcp4" keeps 0.0.0.0 to IPv4, which is what // the README says it means. :: stays on "tcp" and answers both. An // IPv4-mapped address such as ::ffff:0.0.0.0 is the IPv4 address it // carries to Go's listener, so it gets "tcp4" too. func (s *Server) listenNetwork() string { addr, err := netip.ParseAddr(s.params.Config.BindAddress) if err == nil && addr.Unmap().Is4() { return "tcp4" } return "tcp" } // newHTTPServer builds the HTTP server for this Server's // configuration. // // It is called from New, on the constructing goroutine, rather than // from the serving goroutine that used to assign s.httpServer // directly. Two goroutines reach that field — the serving goroutine // and the fx stop hook, which calls Shutdown on it — with nothing // ordering them. Constructing it during New puts the write before // every hook fx will later run, which both removes the race and rules // out the nil dereference a stop that arrived before the serving // goroutine had run would have caused. func (s *Server) newHTTPServer() *http.Server { return &http.Server{ Addr: s.listenAddr(), ReadTimeout: httpReadTimeout, WriteTimeout: httpWriteTimeout, MaxHeaderBytes: httpMaxHeaderBytes, Handler: s, } } func (s *Server) serveUntilShutdown() { // add routes // this does any necessary setup in each handler s.SetupRoutes() s.log.Info("http begin listen", "listenaddr", s.httpServer.Addr) // ListenAndServe always listens on "tcp", which would undo // listenNetwork. var listenCfg net.ListenConfig listener, err := listenCfg.Listen( context.Background(), s.listenNetwork(), s.httpServer.Addr, ) if err == nil { err = s.httpServer.Serve(listener) } if err != nil && !errors.Is(err, http.ErrServerClosed) { s.log.Error("listen error", "error", err) s.shutdownWithFailure() } } // ServeHTTP delegates to the router. func (s *Server) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { s.router.ServeHTTP(w, r) }