2026-08-19 20:50:03 +01:00
|
|
|
package httpserver
|
2022-02-10 18:19:34 +01:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
2025-09-10 10:19:04 +01:00
|
|
|
"errors"
|
2022-02-10 18:19:34 +01:00
|
|
|
"net"
|
|
|
|
|
"net/http"
|
2025-09-10 10:19:04 +01:00
|
|
|
"net/netip"
|
2022-06-15 15:49:31 +01:00
|
|
|
"net/url"
|
2025-09-10 10:19:04 +01:00
|
|
|
"strconv"
|
2026-04-08 13:59:41 +03:00
|
|
|
"sync"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"syscall"
|
2026-04-23 13:34:32 +03:00
|
|
|
"time"
|
2022-02-10 18:19:34 +01:00
|
|
|
|
|
|
|
|
"go.uber.org/zap"
|
2024-09-27 06:37:32 +08:00
|
|
|
|
2026-08-19 20:49:00 +01:00
|
|
|
"github.com/status-im/status-go/internal/panics"
|
2022-02-10 18:19:34 +01:00
|
|
|
)
|
|
|
|
|
|
2026-04-23 13:34:32 +03:00
|
|
|
// backgroundShutdownTimeout bounds how long Server.ToBackground waits for
|
|
|
|
|
// active HTTP handlers to drain before force-closing remaining connections.
|
|
|
|
|
const backgroundShutdownTimeout = 300 * time.Millisecond
|
|
|
|
|
|
2026-07-31 15:32:28 +03:00
|
|
|
// teardownShutdownTimeout bounds the same drain at teardown. It is generous
|
|
|
|
|
// enough for normal in-flight requests to finish, but a handler that never
|
|
|
|
|
// returns must not be able to wedge Stop — and with it the logout path that
|
|
|
|
|
// holds the backend mutex.
|
|
|
|
|
const teardownShutdownTimeout = 3 * time.Second
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
// tcpListenConfig sets SO_REUSEADDR so that after the previous listener is fully
|
|
|
|
|
// closed, the kernel can allow binding the same cached ephemeral port again (e.g.
|
|
|
|
|
// TIME_WAIT) without a long delay. It does not help if Stop returns before that
|
|
|
|
|
// socket is closed — Stop waits on serveWg for that ordering.
|
|
|
|
|
var tcpListenConfig = net.ListenConfig{
|
|
|
|
|
Control: func(network, address string, c syscall.RawConn) error {
|
|
|
|
|
var optErr error
|
|
|
|
|
if err := c.Control(func(fd uintptr) {
|
|
|
|
|
optErr = setReuseAddr(fd)
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return optErr
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
type Config struct {
|
|
|
|
|
Cert *tls.Certificate
|
|
|
|
|
AddrPort netip.AddrPort
|
|
|
|
|
|
|
|
|
|
// AdvertizeHost and AdvertizePort define the a different host/port to be advertized in media URLs than the one server listen on.
|
|
|
|
|
// Can be used when status-go is running behind a NAT/PAT. If empty/zero, the actual listening address is used.
|
|
|
|
|
AdvertizeHost string
|
|
|
|
|
AdvertizePort int
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-09 09:07:57 -04:00
|
|
|
type Server struct {
|
2026-04-08 13:59:41 +03:00
|
|
|
mu sync.Mutex
|
|
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
listener net.Listener
|
|
|
|
|
server *http.Server
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
handlers HandlerPatternMap
|
|
|
|
|
|
|
|
|
|
config *Config
|
|
|
|
|
|
|
|
|
|
// address is the host and port the server is listening on
|
|
|
|
|
address *net.TCPAddr
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
// running is true if the server was started and Serve is active; read via IsRunning().
|
|
|
|
|
running atomic.Bool
|
|
|
|
|
|
|
|
|
|
// cachedPort stores the port from the first successful bind when AddrPort used port 0 (ephemeral).
|
|
|
|
|
// Reused on pause/resume so URLs remain valid across ToBackground/ToForeground cycles.
|
|
|
|
|
cachedPort int
|
|
|
|
|
|
|
|
|
|
// serveWg waits for the serve goroutine to finish all defers after http.Server.Serve returns,
|
|
|
|
|
// so Stop does not return while the previous listener may still be tearing down (avoids EADDRINUSE
|
|
|
|
|
// when rebinding the same cached ephemeral port).
|
|
|
|
|
serveWg sync.WaitGroup
|
2023-02-15 15:50:30 +00:00
|
|
|
|
2026-07-28 14:48:49 +03:00
|
|
|
// serveDone is closed when the current serve goroutine has fully exited.
|
|
|
|
|
// The dead-listener rebind path waits on the instance it captured — never
|
|
|
|
|
// on serveWg, which a concurrent Start could grow while the mutex is
|
|
|
|
|
// released, making the wait block on an unrelated healthy server.
|
|
|
|
|
serveDone chan struct{}
|
|
|
|
|
|
2023-02-15 15:50:30 +00:00
|
|
|
*timeoutManager
|
2022-05-09 09:07:57 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
func NewServer(logger *zap.Logger, config *Config) *Server {
|
|
|
|
|
return &Server{
|
2023-02-15 15:50:30 +00:00
|
|
|
logger: logger,
|
2025-09-10 10:19:04 +01:00
|
|
|
config: config,
|
2023-02-15 15:50:30 +00:00
|
|
|
timeoutManager: newTimeoutManager(),
|
2022-10-12 11:00:14 +01:00
|
|
|
}
|
2022-06-15 15:49:31 +01:00
|
|
|
}
|
2022-02-10 18:19:34 +01:00
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
func (s *Server) GetAddrPort() string {
|
2026-06-03 16:40:14 -04:00
|
|
|
host := ""
|
|
|
|
|
if s.address != nil {
|
|
|
|
|
host = s.address.IP.String()
|
|
|
|
|
} else if s.config != nil {
|
|
|
|
|
host = s.config.AddrPort.Addr().String()
|
2025-09-10 10:19:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.config != nil && s.config.Cert != nil && len(s.config.Cert.Leaf.DNSNames) > 0 {
|
|
|
|
|
host = s.config.Cert.Leaf.DNSNames[0]
|
|
|
|
|
}
|
|
|
|
|
if s.config != nil && s.config.AdvertizeHost != "" {
|
|
|
|
|
host = s.config.AdvertizeHost
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:40:14 -04:00
|
|
|
port := s.GetPort()
|
|
|
|
|
if host == "" || port == 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return net.JoinHostPort(host, strconv.Itoa(port))
|
2022-10-12 13:51:13 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
func (s *Server) GetPort() int {
|
|
|
|
|
if s.config != nil && s.config.AdvertizePort != 0 {
|
|
|
|
|
return s.config.AdvertizePort
|
|
|
|
|
}
|
2026-06-03 16:40:14 -04:00
|
|
|
|
|
|
|
|
if s.address != nil {
|
|
|
|
|
return s.address.Port
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.cachedPort != 0 {
|
|
|
|
|
return s.cachedPort
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.config != nil && s.config.AddrPort.Port() != 0 {
|
|
|
|
|
return int(s.config.AddrPort.Port())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0
|
2023-01-06 20:21:14 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-05 00:13:38 +01:00
|
|
|
func (s *Server) GetListeningAddrPort() string {
|
|
|
|
|
if s.address == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return s.address.String()
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-06 20:21:14 +08:00
|
|
|
func (s *Server) GetCert() *tls.Certificate {
|
2025-09-10 10:19:04 +01:00
|
|
|
return s.config.Cert
|
2023-01-06 20:21:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) GetLogger() *zap.Logger {
|
|
|
|
|
return s.logger
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
// getBindAddrPort returns the address to bind to. When config used port 0 (ephemeral)
|
|
|
|
|
// and we have a cached port from a previous run, reuse it so URLs stay stable across pause/resume.
|
|
|
|
|
func (s *Server) getBindAddrPort() netip.AddrPort {
|
|
|
|
|
if s.cachedPort != 0 && s.config.AddrPort.Port() == 0 {
|
|
|
|
|
return netip.AddrPortFrom(s.config.AddrPort.Addr(), uint16(s.cachedPort))
|
|
|
|
|
}
|
|
|
|
|
return s.config.AddrPort
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-22 10:23:17 +08:00
|
|
|
func (s *Server) createListener() (net.Listener, error) {
|
2026-04-08 13:59:41 +03:00
|
|
|
addr := s.getBindAddrPort()
|
|
|
|
|
addrStr := addr.String()
|
|
|
|
|
|
|
|
|
|
ln, err := tcpListenConfig.Listen(context.Background(), "tcp", addrStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
2024-11-22 10:23:17 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
if s.config.Cert == nil {
|
|
|
|
|
return ln, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serverName := addr.Addr().String()
|
2025-09-10 10:19:04 +01:00
|
|
|
if len(s.config.Cert.Leaf.DNSNames) > 0 {
|
|
|
|
|
serverName = s.config.Cert.Leaf.DNSNames[0]
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-22 10:23:17 +08:00
|
|
|
cfg := &tls.Config{
|
2025-09-10 10:19:04 +01:00
|
|
|
Certificates: []tls.Certificate{*s.config.Cert},
|
|
|
|
|
ServerName: serverName,
|
2024-11-22 10:23:17 +08:00
|
|
|
MinVersion: tls.VersionTLS12,
|
|
|
|
|
}
|
2026-04-08 13:59:41 +03:00
|
|
|
return tls.NewListener(ln, cfg), nil
|
2024-11-22 10:23:17 +08:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
func (s *Server) listen() error {
|
|
|
|
|
s.address = nil
|
2022-02-10 18:19:34 +01:00
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
var err error
|
|
|
|
|
s.listener, err = s.createListener()
|
2022-02-10 18:19:34 +01:00
|
|
|
if err != nil {
|
2025-09-10 10:19:04 +01:00
|
|
|
s.logger.Error("failed to start server", zap.Error(err))
|
|
|
|
|
return err
|
2022-02-10 18:19:34 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
s.address = s.listener.Addr().(*net.TCPAddr)
|
2026-04-08 13:59:41 +03:00
|
|
|
if s.config.AddrPort.Port() == 0 {
|
|
|
|
|
s.cachedPort = s.address.Port
|
|
|
|
|
}
|
2022-06-15 15:49:31 +01:00
|
|
|
|
2023-02-15 15:50:30 +00:00
|
|
|
s.StartTimeout(func() {
|
|
|
|
|
err := s.Stop()
|
|
|
|
|
if err != nil {
|
2023-03-21 12:36:34 +00:00
|
|
|
s.logger.Error("server termination fail", zap.Error(err))
|
2023-02-15 15:50:30 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2025-09-10 10:19:04 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
func (s *Server) serve(currentServer *http.Server, currentListener net.Listener) {
|
2026-08-19 20:49:00 +01:00
|
|
|
defer panics.LogOnPanic()
|
2025-09-10 10:19:04 +01:00
|
|
|
|
|
|
|
|
defer func() {
|
2026-04-08 13:59:41 +03:00
|
|
|
s.mu.Lock()
|
|
|
|
|
defer s.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// If a newer Start() already replaced server/listener, do not clobber
|
|
|
|
|
// the latest running instance state.
|
|
|
|
|
if s.server == currentServer && s.listener == currentListener {
|
|
|
|
|
s.running.Store(false)
|
|
|
|
|
s.address = nil
|
|
|
|
|
}
|
2025-09-10 10:19:04 +01:00
|
|
|
}()
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
err := currentServer.Serve(currentListener)
|
2025-09-10 10:19:04 +01:00
|
|
|
if errors.Is(err, http.ErrServerClosed) {
|
2022-02-10 18:19:34 +01:00
|
|
|
return
|
|
|
|
|
}
|
2025-09-10 10:19:04 +01:00
|
|
|
|
|
|
|
|
s.logger.Error("server failed unexpectedly, restarting", zap.Error(err))
|
|
|
|
|
return
|
|
|
|
|
|
2022-02-10 18:19:34 +01:00
|
|
|
}
|
|
|
|
|
|
2022-06-15 15:49:31 +01:00
|
|
|
func (s *Server) resetServer() {
|
2023-02-15 15:50:30 +00:00
|
|
|
s.StopTimeout()
|
2022-06-15 15:49:31 +01:00
|
|
|
s.server = new(http.Server)
|
2025-09-10 10:19:04 +01:00
|
|
|
s.address = nil
|
2022-06-15 15:49:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) applyHandlers() {
|
|
|
|
|
if s.server == nil {
|
|
|
|
|
s.server = new(http.Server)
|
|
|
|
|
}
|
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
|
|
|
|
|
|
for p, h := range s.handlers {
|
|
|
|
|
mux.HandleFunc(p, h)
|
|
|
|
|
}
|
|
|
|
|
s.server.Handler = mux
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 14:21:39 +03:00
|
|
|
// listenerAlive verifies the bound listener actually accepts connections.
|
|
|
|
|
// On iOS a process suspension can kill the listening socket without the accept
|
|
|
|
|
// loop ever returning an error, leaving `running` true while every client
|
|
|
|
|
// connect is refused (observed when the app is suspended on the login screen,
|
|
|
|
|
// before the pausable-services bridge has anything to drive).
|
2026-07-28 14:48:49 +03:00
|
|
|
//
|
|
|
|
|
// A raw TCP dial is sufficient and safe with a TLS listener: crypto/tls only
|
|
|
|
|
// handshakes on the first read/write of an accepted conn, and net/http runs
|
|
|
|
|
// that in a per-connection goroutine — an immediate close surfaces (at most) a
|
|
|
|
|
// per-conn handshake error, never an Accept error, so Serve keeps running.
|
2026-07-28 14:21:39 +03:00
|
|
|
func (s *Server) listenerAlive() bool {
|
|
|
|
|
addr := s.GetListeningAddrPort()
|
|
|
|
|
if addr == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
_ = conn.Close()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-10 18:19:34 +01:00
|
|
|
func (s *Server) Start() error {
|
2026-04-08 13:59:41 +03:00
|
|
|
s.mu.Lock()
|
|
|
|
|
defer s.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if s.running.Load() {
|
2026-07-28 14:21:39 +03:00
|
|
|
if s.listenerAlive() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
s.logger.Warn("server marked running but listener is dead; rebinding")
|
|
|
|
|
currentServer := s.server
|
2026-07-28 14:48:49 +03:00
|
|
|
done := s.serveDone
|
2026-07-28 14:21:39 +03:00
|
|
|
s.running.Store(false)
|
|
|
|
|
_ = currentServer.Close()
|
2026-07-28 14:48:49 +03:00
|
|
|
// The serve goroutine's deferred cleanup takes s.mu — release it while
|
|
|
|
|
// waiting, and wait only on the captured instance, never on serveWg
|
|
|
|
|
// (a concurrent Start could grow it with a healthy new server).
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
if done != nil {
|
|
|
|
|
<-done
|
|
|
|
|
}
|
2026-07-28 14:21:39 +03:00
|
|
|
s.mu.Lock()
|
2026-07-28 14:48:49 +03:00
|
|
|
// Re-validate after the unlocked window: a concurrent Start may have
|
|
|
|
|
// already rebound — in that case this call's work is done.
|
|
|
|
|
if s.running.Load() {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-09-10 10:19:04 +01:00
|
|
|
}
|
|
|
|
|
|
2022-06-15 15:49:31 +01:00
|
|
|
// Once Shutdown has been called on a server, it may not be reused;
|
|
|
|
|
s.resetServer()
|
|
|
|
|
s.applyHandlers()
|
2025-09-10 10:19:04 +01:00
|
|
|
|
|
|
|
|
err := s.listen()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
// Mark running synchronously to avoid pause/play races where ToBackground
|
|
|
|
|
// can run before serve() goroutine has a chance to set the state.
|
|
|
|
|
s.running.Store(true)
|
2026-07-28 14:48:49 +03:00
|
|
|
done := make(chan struct{})
|
|
|
|
|
s.serveDone = done
|
2026-04-08 13:59:41 +03:00
|
|
|
s.serveWg.Add(1)
|
|
|
|
|
go func() {
|
2026-08-19 20:49:00 +01:00
|
|
|
defer panics.LogOnPanic()
|
2026-07-28 14:48:49 +03:00
|
|
|
defer close(done)
|
2026-04-08 13:59:41 +03:00
|
|
|
defer s.serveWg.Done()
|
|
|
|
|
s.serve(s.server, s.listener)
|
|
|
|
|
}()
|
2022-02-10 18:19:34 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) Stop() error {
|
2026-07-31 15:32:28 +03:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), teardownShutdownTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
err := s.stopWith(ctx)
|
|
|
|
|
if shouldForceCloseOnShutdownError(err) {
|
|
|
|
|
s.logger.Warn("server graceful shutdown did not complete in time; forced close",
|
|
|
|
|
zap.Error(err), zap.Duration("timeout", teardownShutdownTimeout))
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return err
|
2026-04-23 13:34:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stopWith performs the graceful shutdown sequence bounded by ctx. If ctx
|
|
|
|
|
// expires or is canceled before active connections drain, the server is
|
|
|
|
|
// force-closed via Close so the caller isn't blocked by a slow or stuck
|
2026-07-31 15:32:28 +03:00
|
|
|
// handler. Callers always pass a deadline: a generous one at teardown, a short
|
|
|
|
|
// one for lifecycle events like pause/ToBackground.
|
2026-04-23 13:34:32 +03:00
|
|
|
func (s *Server) stopWith(ctx context.Context) error {
|
2026-04-08 13:59:41 +03:00
|
|
|
s.mu.Lock()
|
2023-02-15 15:50:30 +00:00
|
|
|
s.StopTimeout()
|
2026-04-08 13:59:41 +03:00
|
|
|
if !s.running.Load() || s.server == nil {
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
return nil
|
2022-02-10 18:19:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 13:59:41 +03:00
|
|
|
// Capture the current instance and release the lock before Shutdown.
|
|
|
|
|
// Shutdown waits for Serve() to return, and Serve() may update state in
|
|
|
|
|
// its defer path under the same mutex.
|
|
|
|
|
currentServer := s.server
|
|
|
|
|
s.running.Store(false)
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
|
2026-04-23 13:34:32 +03:00
|
|
|
err := currentServer.Shutdown(ctx)
|
|
|
|
|
if shouldForceCloseOnShutdownError(err) {
|
|
|
|
|
_ = currentServer.Close()
|
|
|
|
|
}
|
2026-04-08 13:59:41 +03:00
|
|
|
s.serveWg.Wait()
|
|
|
|
|
return err
|
2022-02-10 18:19:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 13:34:32 +03:00
|
|
|
func shouldForceCloseOnShutdownError(err error) bool {
|
|
|
|
|
return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled)
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-15 15:50:30 +00:00
|
|
|
func (s *Server) IsRunning() bool {
|
2026-04-08 13:59:41 +03:00
|
|
|
return s.running.Load()
|
2023-02-15 15:50:30 +00:00
|
|
|
}
|
|
|
|
|
|
2022-02-10 18:19:34 +01:00
|
|
|
func (s *Server) ToForeground() {
|
2026-04-08 13:59:41 +03:00
|
|
|
if err := s.Start(); err != nil {
|
|
|
|
|
s.logger.Error("server start failed during foreground transition", zap.Error(err))
|
2022-02-10 18:19:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) ToBackground() {
|
2026-04-23 13:34:32 +03:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), backgroundShutdownTimeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
if err := s.stopWith(ctx); err != nil {
|
|
|
|
|
if shouldForceCloseOnShutdownError(err) {
|
|
|
|
|
s.logger.Warn("server graceful shutdown did not complete in time; forced close",
|
|
|
|
|
zap.Error(err), zap.Duration("timeout", backgroundShutdownTimeout))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.logger.Error("server shutdown failed during background transition", zap.Error(err))
|
2022-02-10 18:19:34 +01:00
|
|
|
}
|
|
|
|
|
}
|
2022-06-15 15:49:31 +01:00
|
|
|
|
|
|
|
|
func (s *Server) SetHandlers(handlers HandlerPatternMap) {
|
|
|
|
|
s.handlers = handlers
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-06 14:26:16 +01:00
|
|
|
func (s *Server) AddHandlers(handlers HandlerPatternMap) {
|
|
|
|
|
if s.handlers == nil {
|
|
|
|
|
s.handlers = make(HandlerPatternMap)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for name := range handlers {
|
|
|
|
|
s.handlers[name] = handlers[name]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-15 15:49:31 +01:00
|
|
|
func (s *Server) MakeBaseURL() *url.URL {
|
2026-06-03 16:40:14 -04:00
|
|
|
hostPort := s.GetAddrPort()
|
|
|
|
|
if hostPort == "" {
|
2025-09-10 10:19:04 +01:00
|
|
|
return &url.URL{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scheme := "http"
|
|
|
|
|
if s.config != nil && s.config.Cert != nil {
|
|
|
|
|
scheme = "https"
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-15 15:49:31 +01:00
|
|
|
return &url.URL{
|
2025-09-10 10:19:04 +01:00
|
|
|
Scheme: scheme,
|
2026-06-03 16:40:14 -04:00
|
|
|
Host: hostPort,
|
2022-06-15 15:49:31 +01:00
|
|
|
}
|
|
|
|
|
}
|