github.com/haraldrudell/parl@v0.4.176/phttp/http.go (about) 1 /* 2 © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package phttp 7 8 import ( 9 "context" 10 "net" 11 "net/http" 12 "net/netip" 13 "sync" 14 "sync/atomic" 15 "time" 16 17 "github.com/haraldrudell/parl" 18 "github.com/haraldrudell/parl/perrors" 19 "github.com/haraldrudell/parl/pnet" 20 ) 21 22 const ( 23 // default port for http: 80, or address for localhost IPv4 or IPv6 port 80 24 HttpAddr = ":http" 25 ) 26 27 // Http is an http server instance 28 // - based on [http.Server] 29 // - has listener thread 30 // - all errors sent on channel 31 // - idempotent deferrable panic-free shutdown 32 // - awaitable, observable 33 type Http struct { 34 // used for [http.Server.Listen] invocation 35 // - "tcp", "tcp4", "tcp6", "unix" or "unixpacket" 36 Network pnet.Network 37 // Close() ListenAndServe() ListenAndServeTLS() RegisterOnShutdown() 38 // Serve() ServeTLS() SetKeepAlivesEnabled() Shutdown() 39 http.Server 40 // real-time server error stream, unbound non-blocking 41 // - [Http.SendErr] invocations 42 // - shutdown error 43 ErrCh parl.NBChan[error] 44 // near socket address, protocol is tcp 45 Near netip.AddrPort 46 // allows to wait for listen 47 // - when triggered, [Http.Near] is present 48 ListenAwaitable parl.Awaitable 49 // allows to wait for end of listen 50 // - end of thread launched by [Http.Listen] 51 EndListenAwaitable parl.Awaitable 52 // the URL router 53 serveMux *http.ServeMux 54 // whether Listen invocation is allowed 55 NoListen atomic.Bool 56 // Cancel of listening set-up 57 Cancel atomic.Pointer[context.CancelFunc] 58 shutdownOnce sync.Once 59 } 60 61 // NewHttp creates http server for default “localhost:80” 62 // - if nearSocket.Addr is invalid, all interfaces for IPv6 if allowed, IPv4 otherwise is used 63 // - if nearSocket.Port is zero: 64 // - — if network is NetworkDefault: ephemeral port 65 // - — otherwise port 80 “:http” is used 66 // - for NetworkDefault, NetworkTCP is used 67 // - panic for bad Network 68 // 69 // Usage: 70 // 71 // var s = NewHttp(netip.AdddrPort{}, pnet.NetworkTCP) 72 // s.HandleFunc("/", myHandler) 73 // defer s.Shutdown() 74 // for err := range s.Listen() { 75 func NewHttp(nearSocket netip.AddrPort, network pnet.Network) (hp *Http) { 76 var hostPort string 77 if a := nearSocket.Addr(); a.IsValid() { 78 if nearSocket.Port() != 0 || network == pnet.NetworkDefault { 79 hostPort = nearSocket.String() 80 } else { 81 hostPort = a.String() + HttpAddr 82 } 83 } else { 84 hostPort = HttpAddr // default “:http” meaning IPv4 or IPv6 localhost port 80 85 } 86 switch network { 87 case pnet.NetworkDefault: 88 network = pnet.NetworkTCP 89 case pnet.NetworkTCP, pnet.NetworkTCP4, pnet.NetworkTCP6, 90 pnet.NetworkUnix, pnet.NetworkUnixPacket: 91 default: 92 panic(perrors.ErrorfPF("Bad network: %s allowed: tcp tcp4 tcp6 unix unixpacket", network)) 93 } 94 var serveMux = http.NewServeMux() 95 // there is no new-function for [http.Server] 96 return &Http{ 97 Network: network, 98 serveMux: serveMux, 99 Server: http.Server{ 100 // ServeMux matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL. 101 // - http.Handler is interface { ServeHTTP(ResponseWriter, *Request) } 102 Handler: serveMux, // struct 103 Addr: hostPort, 104 }, 105 } 106 } 107 108 const ( 109 httpShutdownTimeout = 5 * time.Second 110 ) 111 112 // HandlerFunc is the signature for URL handlers 113 type HandlerFunc func(http.ResponseWriter, *http.Request) 114 115 // HandleFunc registers a URL-handler for the server 116 func (s *Http) HandleFunc(pattern string, handler HandlerFunc) { 117 s.serveMux.HandleFunc(pattern, handler) 118 } 119 120 // Listen initiates listening and returns the error channel 121 // - can only be invoked once or panic 122 // - errCh closes on server shutdown 123 // - non-blocking, all errors are sent on the error channel 124 func (s *Http) Listen() (errCh <-chan error) { 125 if !s.NoListen.CompareAndSwap(false, true) { 126 panic(perrors.NewPF("multiple invocations")) 127 } 128 errCh = s.ErrCh.Ch() 129 // listen is deferred so just launch the thread 130 go s.httpListenerThread() 131 return 132 } 133 134 // httpListenerThread is gorouitn starting listen and 135 // waiting for server to terminate 136 func (s *Http) httpListenerThread() { 137 defer s.EndListenAwaitable.Close() 138 var err error 139 defer parl.Recover(func() parl.DA { return parl.A() }, &err, s.SendErr) 140 141 // get near tcp socket listener 142 var listener net.Listener 143 if listener, err = pnet.Listen(s.Network, s.Server.Addr, &s.Cancel); err != nil { 144 return 145 } 146 defer s.maybeClose(&listener, &err) 147 148 // set Near socket address 149 if s.Near, err = pnet.AddrPortFromAddr(listener.Addr()); err != nil { 150 return 151 } 152 s.ListenAwaitable.Close() 153 154 // blocks here until Shutdown or Close 155 err = s.Server.Serve(listener) 156 listener = nil 157 158 // on regular close, http.ErrServerClosed 159 if err == http.ErrServerClosed { 160 err = nil // ignore error 161 return // successful return 162 } 163 err = perrors.Errorf("hp.Server.Serve: ‘%w’", err) 164 } 165 166 // SendErr sends errors on the server’s error channel 167 func (s *Http) SendErr(err error) { s.ErrCh.Send(err) } 168 169 // idempotent panic-free shutdown that does not return prior to server shut down 170 func (s *Http) Shutdown() { 171 s.shutdownOnce.Do(s.shutdown) 172 } 173 174 // closes listener if non-nil 175 func (s *Http) maybeClose(listenerp *net.Listener, errp *error) { 176 var listener = *listenerp 177 if listener == nil { 178 return 179 } 180 parl.Close(listener, errp) 181 } 182 183 // 5-second shutdown 184 func (s *Http) shutdown() { 185 var wasListen = s.NoListen.Load() 186 if !wasListen { 187 // prevent further listen 188 wasListen = s.NoListen.Swap(true) 189 } else { 190 if cancelFuncp := s.Cancel.Load(); cancelFuncp != nil { 191 (*cancelFuncp)() 192 } 193 } 194 var ctx, cancel = context.WithTimeout(context.Background(), httpShutdownTimeout) 195 defer cancel() 196 if err := s.Server.Shutdown(ctx); perrors.IsPF(&err, "hp.Server.Shutdown: '%w'", err) { 197 s.SendErr(err) 198 } 199 if wasListen { 200 // wait for thread to exit 201 <-s.EndListenAwaitable.Ch() 202 } 203 s.ErrCh.Close() 204 }