• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

alexferl / zerohttp / 23167705173

16 Mar 2026 09:53PM UTC coverage: 93.667% (+0.01%) from 93.657%
23167705173

Pull #117

github

web-flow
Merge acb6989a6 into 349ae394b
Pull Request #117: Readme

9170 of 9790 relevant lines covered (93.67%)

382.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.3
/server.go
1
package zerohttp
2

3
import (
4
        "context"
5
        "crypto/tls"
6
        "errors"
7
        "fmt"
8
        "net"
9
        "net/http"
10
        "sync"
11
        "time"
12

13
        "github.com/alexferl/zerohttp/config"
14
        zconfig "github.com/alexferl/zerohttp/internal/config"
15
        "github.com/alexferl/zerohttp/log"
16
        "github.com/alexferl/zerohttp/metrics"
17
        "github.com/alexferl/zerohttp/middleware"
18
)
19

20
// Default server timeout constants
21
const (
22
        DefaultReadTimeout       = 10 * time.Second
23
        DefaultReadHeaderTimeout = 5 * time.Second
24
        DefaultWriteTimeout      = 15 * time.Second
25
        DefaultIdleTimeout       = 60 * time.Second
26
)
27

28
// Server represents a zerohttp server instance that wraps Go's standard HTTP server
29
// with additional functionality including middleware support, TLS configuration,
30
// automatic certificate management, and structured logging.
31
//
32
// The Server embeds a Router interface, providing direct access to HTTP routing
33
// methods (GET, POST, PUT, DELETE, etc.) and middleware management.
34
type Server struct {
35
        // Router provides HTTP routing functionality including method-specific
36
        // route registration, middleware support, and request handling.
37
        Router
38

39
        // mu protects concurrent access to server fields during startup,
40
        // shutdown, and configuration operations.
41
        mu sync.RWMutex
42

43
        // baseCtx is the root context for all requests.
44
        // It is cancelled when Shutdown is called to signal request cancellation.
45
        baseCtx context.Context
46

47
        // cancelBaseCtx cancels the base context.
48
        cancelBaseCtx context.CancelFunc
49

50
        // server is the HTTP server instance for handling plain HTTP traffic.
51
        // If nil, HTTP server will not be started.
52
        server *http.Server
53

54
        // listener is the network listener for HTTP traffic. If nil, a default
55
        // listener will be created using the server's configured address.
56
        listener net.Listener
57

58
        // tlsServer is the HTTPS server instance for handling encrypted traffic.
59
        // If nil, HTTPS server will not be started.
60
        tlsServer *http.Server
61

62
        // tlsListener is the network listener for HTTPS traffic. If nil, a default
63
        // TLS listener will be created using the tlsServer's configured address.
64
        tlsListener net.Listener
65

66
        // certFile is the file path to the TLS certificate in PEM format.
67
        // Used when serving HTTPS traffic with certificate files.
68
        certFile string
69

70
        // keyFile is the file path to the TLS private key in PEM format.
71
        // Used when serving HTTPS traffic with certificate files.
72
        keyFile string
73

74
        // logger is the structured logger used by the server and its middleware
75
        // for recording HTTP requests, errors, and server lifecycle events.
76
        logger log.Logger
77

78
        // preStartupHooks execute sequentially before any startup hooks.
79
        preStartupHooks []config.StartupHookConfig
80

81
        // startupHooks execute sequentially before the server starts accepting connections.
82
        // If any startup hook returns an error, the server will not start.
83
        startupHooks []config.StartupHookConfig
84

85
        // postStartupHooks execute sequentially after the server has started.
86
        postStartupHooks []config.StartupHookConfig
87

88
        // preShutdownHooks execute sequentially before server shutdown begins.
89
        preShutdownHooks []config.ShutdownHookConfig
90

91
        // shutdownHooks execute concurrently with server shutdown.
92
        shutdownHooks []config.ShutdownHookConfig
93

94
        // postShutdownHooks execute sequentially after all servers are shut down.
95
        postShutdownHooks []config.ShutdownHookConfig
96

97
        // validator is an optional struct validator for validating request data.
98
        // Users can inject their own implementation (e.g., go-playground/validator/v10).
99
        // If nil, the default built-in validator will be used.
100
        validator config.Validator
101

102
        // metricsRegistry holds the metrics registry for collecting and exposing metrics.
103
        // If nil, metrics collection is disabled.
104
        metricsRegistry metrics.Registry
105

106
        // metricsServer is a dedicated HTTP server for serving metrics.
107
        // When Metrics.ServerAddr is set, metrics are served on this separate server
108
        // bound to the specified address (typically localhost for security).
109
        metricsServer *http.Server
110

111
        // metricsListener is the network listener for the metrics server.
112
        metricsListener net.Listener
113

114
        // metricsServerAddr is the configured address for the metrics server.
115
        metricsServerAddr string
116

117
        // autocertManager handles automatic certificate provisioning and renewal
118
        // using Let's Encrypt ACME protocol. If set, enables automatic TLS.
119
        // Users must provide their own implementation (e.g., golang.org/x/crypto/acme/autocert.Manager).
120
        autocertManager config.AutocertManager
121

122
        // http3Server is an optional HTTP/3 server for handling HTTP/3 traffic over QUIC.
123
        // Users can inject their own implementation (e.g., quic-go/http3) to enable HTTP/3.
124
        // If nil, HTTP/3 server will not be started.
125
        http3Server config.HTTP3Server
126

127
        // sseProvider is an optional SSE provider for handling Server-Sent Events connections.
128
        // Users can inject their own implementation or use the built-in stdlib provider.
129
        // If nil, SSE is not available but users can still handle SSE manually in their handlers.
130
        sseProvider config.SSEProvider
131

132
        // webSocketUpgrader is an optional WebSocket upgrader for handling WebSocket connections.
133
        // Users provide their own implementation using their preferred WebSocket library.
134
        // If nil, WebSocket is not available but users can still handle upgrades manually.
135
        webSocketUpgrader config.WebSocketUpgrader
136

137
        // webTransportServer is an optional WebTransport server for handling WebTransport sessions.
138
        // Users can inject their own implementation (e.g., quic-go/webtransport-go) to enable WebTransport.
139
        // If nil, WebTransport support will not be enabled.
140
        // The server will be started automatically when ListenAndServeTLS or Start is called.
141
        webTransportServer config.WebTransportServer
142
}
143

144
// New creates and configures a new Server instance with the provided config.
145
// It initializes the server with sensible defaults that can be overridden
146
// using the provided config.
147
//
148
// The server includes:
149
//   - HTTP and HTTPS support
150
//   - Middleware integration
151
//   - Structured logging
152
//   - Automatic metrics collection (enabled by default)
153
//   - Request binding and validation
154
//
155
// Example - Basic usage with defaults:
156
//
157
//        app := zh.New()
158
//        app.GET("/", handler)
159
//        log.Fatal(app.Start())
160
//
161
// Example - With custom configuration:
162
//
163
//        app := zh.New(config.Config{
164
//            Addr:         ":8080",
165
//            ReadTimeout:  10 * time.Second,
166
//            WriteTimeout: 15 * time.Second,
167
//            Logger:       myLogger,
168
//            Metrics: config.MetricsConfig{
169
//                Enabled: false, // Disable metrics
170
//            },
171
//        })
172
//
173
// Example - With pluggable validator:
174
//
175
//        app := zh.New(config.Config{
176
//            Validator: myCustomValidator,
177
//        })
178
func New(cfg ...config.Config) *Server {
95✔
179
        c := mergeConfig(cfg...)
95✔
180
        router := NewRouter()
95✔
181
        logger := createLogger(c)
95✔
182

95✔
183
        router.SetLogger(logger)
95✔
184
        router.SetConfig(c)
95✔
185

95✔
186
        server := createHTTPServer(c)
95✔
187
        tlsServer := createTLSServer(c)
95✔
188
        registry := createMetricsRegistry(c)
95✔
189
        metricsServer := createMetricsServer(c, registry)
95✔
190

95✔
191
        baseCtx, cancelBaseCtx := context.WithCancel(context.Background())
95✔
192

95✔
193
        s := &Server{
95✔
194
                Router:             router,
95✔
195
                server:             server,
95✔
196
                listener:           c.Listener,
95✔
197
                tlsServer:          tlsServer,
95✔
198
                tlsListener:        c.TLS.Listener,
95✔
199
                certFile:           c.TLS.CertFile,
95✔
200
                keyFile:            c.TLS.KeyFile,
95✔
201
                autocertManager:    c.Extensions.AutocertManager,
95✔
202
                http3Server:        c.Extensions.HTTP3Server,
95✔
203
                webTransportServer: c.Extensions.WebTransportServer,
95✔
204
                webSocketUpgrader:  c.Extensions.WebSocketUpgrader,
95✔
205
                sseProvider:        c.Extensions.SSEProvider,
95✔
206
                metricsRegistry:    registry,
95✔
207
                metricsServer:      metricsServer,
95✔
208
                metricsServerAddr:  c.Metrics.ServerAddr,
95✔
209
                validator:          c.Validator,
95✔
210
                logger:             logger,
95✔
211
                preStartupHooks:    c.Lifecycle.PreStartupHooks,
95✔
212
                startupHooks:       c.Lifecycle.StartupHooks,
95✔
213
                postStartupHooks:   c.Lifecycle.PostStartupHooks,
95✔
214
                preShutdownHooks:   c.Lifecycle.PreShutdownHooks,
95✔
215
                shutdownHooks:      c.Lifecycle.ShutdownHooks,
95✔
216
                postShutdownHooks:  c.Lifecycle.PostShutdownHooks,
95✔
217
                baseCtx:            baseCtx,
95✔
218
                cancelBaseCtx:      cancelBaseCtx,
95✔
219
        }
95✔
220

95✔
221
        setupMiddleware(s, c, registry)
95✔
222
        setupServerHandlers(s, router)
95✔
223
        registerMetricsEndpoint(s, c, registry)
95✔
224

95✔
225
        // Finalize router to register catch-all handler with middleware
95✔
226
        if r, ok := router.(interface{ finalize() }); ok {
95✔
227
                r.finalize()
×
228
        }
×
229

230
        return s
95✔
231
}
232

233
// ListenAndServe starts the HTTP server and begins accepting connections.
234
// It creates a listener if one is not already configured and serves HTTP
235
// traffic on the configured address. If the server is not configured,
236
// this method logs a debug message and returns nil without error.
237
//
238
// This method blocks until the server encounters an error or is shut down.
239
// Returns any error encountered while starting or running the server.
240
func (s *Server) ListenAndServe() error {
4✔
241
        s.mu.Lock()
4✔
242

4✔
243
        if s.server == nil {
5✔
244
                s.mu.Unlock()
1✔
245
                s.logger.Debug("HTTP server not configured, skipping")
1✔
246
                return nil
1✔
247
        }
1✔
248

249
        var err error
3✔
250
        if s.listener == nil {
4✔
251
                s.logger.Debug("Creating HTTP listener", log.F("addr", s.server.Addr))
1✔
252
                s.listener, err = net.Listen("tcp", s.server.Addr)
1✔
253
                if err != nil {
1✔
254
                        s.mu.Unlock()
×
255
                        return err
×
256
                }
×
257
        }
258

259
        s.mu.Unlock()
3✔
260

3✔
261
        s.logger.Info("Starting HTTP server", log.F("addr", fmtHTTPAddr(s.listener.Addr().String())))
3✔
262

3✔
263
        return s.server.Serve(s.listener)
3✔
264
}
265

266
// Start begins serving HTTP, HTTPS, and metrics traffic concurrently.
267
// It starts all configured servers (HTTP, HTTPS, metrics, HTTP/3, WebTransport)
268
// in separate goroutines and blocks until all servers exit.
269
//
270
// For HTTPS, the server will start if:
271
//   - TLS server is configured AND
272
//   - Either certificates are loaded in TLS config OR certificate files are specified
273
//
274
// Start blocks until all servers exit. If any server encounters an unexpected
275
// error (i.e. not ErrServerClosed), that error is returned immediately.
276
// Returns nil when all servers shut down cleanly (e.g. via Shutdown()).
277
func (s *Server) Start() error {
20✔
278
        s.logger.Info("Starting server...")
20✔
279

20✔
280
        // Run pre-startup hooks first
20✔
281
        if err := s.runPreStartupHooks(s.baseCtx); err != nil {
21✔
282
                s.logger.Error("Pre-startup hook failed, server not starting", log.E(err))
1✔
283
                return err
1✔
284
        }
1✔
285

286
        handler := s.Router
19✔
287

19✔
288
        // Determine if we should start HTTPS server
19✔
289
        shouldStartTLS := s.tlsServer != nil &&
19✔
290
                ((s.tlsServer.TLSConfig != nil &&
19✔
291
                        (len(s.tlsServer.TLSConfig.Certificates) > 0 || s.tlsServer.TLSConfig.GetCertificate != nil)) ||
19✔
292
                        (s.certFile != "" && s.keyFile != ""))
19✔
293

19✔
294
        // Validate and load certificates before starting any goroutines.
19✔
295
        // This avoids orphaning the HTTP/metrics server goroutines on cert failure.
19✔
296
        if shouldStartTLS {
21✔
297
                if s.tlsServer.TLSConfig == nil {
4✔
298
                        s.tlsServer.TLSConfig = &tls.Config{
2✔
299
                                MinVersion: tls.VersionTLS12,
2✔
300
                        }
2✔
301
                }
2✔
302
                s.tlsServer.Handler = handler
2✔
303

2✔
304
                // Load cert/key if paths are specified
2✔
305
                if s.certFile != "" && s.keyFile != "" &&
2✔
306
                        (len(s.tlsServer.TLSConfig.Certificates) == 0 && s.tlsServer.TLSConfig.GetCertificate == nil) {
4✔
307
                        cert, err := tls.LoadX509KeyPair(s.certFile, s.keyFile)
2✔
308
                        if err != nil {
2✔
309
                                s.logger.Error("Failed to load TLS certificate", log.E(err))
×
310
                                return fmt.Errorf("failed to load certificate files: %w", err)
×
311
                        }
×
312
                        s.tlsServer.TLSConfig.Certificates = []tls.Certificate{cert}
2✔
313
                }
314
        }
315

316
        var wg sync.WaitGroup
19✔
317

19✔
318
        // Calculate actual number of servers that will start for error channel capacity
19✔
319
        serverCount := 0
19✔
320
        if s.metricsServer != nil {
22✔
321
                serverCount++
3✔
322
        }
3✔
323
        if s.server != nil {
33✔
324
                serverCount++
14✔
325
        }
14✔
326
        if shouldStartTLS {
21✔
327
                serverCount++ // HTTPS server
2✔
328
        }
2✔
329
        if s.http3Server != nil && shouldStartTLS {
20✔
330
                serverCount++
1✔
331
        }
1✔
332
        if s.webTransportServer != nil && shouldStartTLS {
20✔
333
                serverCount++
1✔
334
        }
1✔
335

336
        errCh := make(chan error, serverCount)
19✔
337

19✔
338
        // Start metrics server if configured
19✔
339
        if s.metricsServer != nil {
22✔
340
                wg.Add(1)
3✔
341
                go func() {
6✔
342
                        defer wg.Done()
3✔
343
                        s.logger.Info("Starting metrics server...", log.F("addr", fmtHTTPAddr(s.metricsServer.Addr)))
3✔
344
                        if err := s.startMetricsServer(); err != nil && !errors.Is(err, http.ErrServerClosed) {
3✔
345
                                errCh <- fmt.Errorf("metrics server error: %w", err)
×
346
                        }
×
347
                }()
348
        }
349

350
        // Start HTTP server
351
        if s.server != nil {
33✔
352
                s.server.Handler = handler
14✔
353
                wg.Add(1)
14✔
354
                go func() {
28✔
355
                        defer wg.Done()
14✔
356
                        s.logger.Info("Starting HTTP server...", log.F("addr", fmtHTTPAddr(s.server.Addr)))
14✔
357
                        if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
21✔
358
                                errCh <- fmt.Errorf("HTTP server error: %w", err)
7✔
359
                        }
7✔
360
                }()
361
        }
362

363
        // Start HTTPS server
364
        if shouldStartTLS {
21✔
365
                wg.Add(1)
2✔
366
                go func() {
4✔
367
                        defer wg.Done()
2✔
368
                        s.logger.Info("Starting HTTPS server...",
2✔
369
                                log.F("addr", fmtHTTPSAddr(s.tlsServer.Addr)),
2✔
370
                                log.F("cert_file", s.certFile),
2✔
371
                                log.F("key_file", s.keyFile))
2✔
372
                        // Pass empty strings - certs are already loaded in TLSConfig.Certificates
2✔
373
                        if err := s.tlsServer.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
4✔
374
                                errCh <- fmt.Errorf("HTTPS server error: %w", err)
2✔
375
                        }
2✔
376
                }()
377
        }
378

379
        // Start HTTP/3 server if configured and we have TLS
380
        if s.http3Server != nil && shouldStartTLS {
20✔
381
                go func() {
2✔
382
                        s.logger.Info("Starting HTTP/3 server...",
1✔
383
                                log.F("cert_file", s.certFile),
1✔
384
                                log.F("key_file", s.keyFile))
1✔
385
                        // Pass empty strings - certs are already loaded in TLSConfig.Certificates
1✔
386
                        if err := s.http3Server.ListenAndServeTLS("", ""); err != nil {
1✔
387
                                s.logger.Error("HTTP/3 server error", log.E(err))
×
388
                        }
×
389
                }()
390
        }
391

392
        // Start WebTransport server if configured and we have TLS
393
        if s.webTransportServer != nil && shouldStartTLS {
20✔
394
                go func() {
2✔
395
                        s.logger.Info("Starting WebTransport server...",
1✔
396
                                log.F("cert_file", s.certFile),
1✔
397
                                log.F("key_file", s.keyFile))
1✔
398
                        // Pass empty strings - certs are already loaded in TLSConfig.Certificates
1✔
399
                        if err := s.webTransportServer.ListenAndServeTLS("", ""); err != nil {
1✔
400
                                s.logger.Error("WebTransport server error", log.E(err))
×
401
                        }
×
402
                }()
403
        }
404

405
        // Guard against hanging if no servers were started
406
        started := 0
19✔
407
        if s.metricsServer != nil {
22✔
408
                started++
3✔
409
        }
3✔
410
        if s.server != nil {
33✔
411
                started++
14✔
412
        }
14✔
413
        if shouldStartTLS {
21✔
414
                started++
2✔
415
        }
2✔
416
        if started == 0 {
22✔
417
                s.logger.Warn("No servers configured, Start() returning immediately")
3✔
418
                return nil
3✔
419
        }
3✔
420

421
        // Run startup hooks concurrently with servers
422
        startupHookErrCh := make(chan error, 1)
16✔
423
        go func() {
32✔
424
                if err := s.runStartupHooks(s.baseCtx); err != nil {
20✔
425
                        s.logger.Error("Startup hook failed, initiating shutdown", log.E(err))
4✔
426
                        startupHookErrCh <- err
4✔
427
                        // Trigger shutdown to stop the servers
4✔
428
                        shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
4✔
429
                        defer cancel()
4✔
430
                        _ = s.Shutdown(shutdownCtx)
4✔
431
                        return
4✔
432
                }
4✔
433
                close(startupHookErrCh)
12✔
434

12✔
435
                // Run post-startup hooks after startup hooks complete successfully
12✔
436
                if err := s.runPostStartupHooks(s.baseCtx); err != nil {
12✔
437
                        s.logger.Error("Post-startup hook failed", log.E(err))
×
438
                        // Non-fatal - continue running
×
439
                }
×
440
        }()
441

442
        // Close errCh when all goroutines complete, then range to collect any errors
443
        go func() {
32✔
444
                wg.Wait()
16✔
445
                close(errCh)
16✔
446
        }()
16✔
447

448
        // Check for server errors or startup hook errors
449
        for {
605,976✔
450
                select {
605,960✔
451
                case err := <-errCh:
15✔
452
                        if err != nil {
24✔
453
                                return err
9✔
454
                        }
9✔
455
                        // errCh closed without error, check startup hook
456
                        if hookErr := <-startupHookErrCh; hookErr != nil {
6✔
457
                                return hookErr
×
458
                        }
×
459
                        return nil
6✔
460
                case hookErr := <-startupHookErrCh:
605,945✔
461
                        // Startup hook failed, wait for servers to shut down
605,945✔
462
                        if hookErr != nil {
605,946✔
463
                                // Drain errCh
1✔
464
                                go func() {
2✔
465
                                        for range errCh {
1✔
466
                                        }
×
467
                                }()
468
                                return hookErr
1✔
469
                        }
470
                }
471
        }
472
}
473

474
// ListenerAddr returns the network address that the HTTP server is listening on.
475
// If a listener is configured, it returns the listener's actual address.
476
// If no listener is configured but a server is configured, it returns the server's configured address.
477
// If neither is configured, it returns an empty string.
478
//
479
// This method is thread-safe and can be called concurrently.
480
// The returned address includes both host and port (e.g., "127.0.0.1:8080").
481
func (s *Server) ListenerAddr() string {
15✔
482
        s.mu.RLock()
15✔
483
        defer s.mu.RUnlock()
15✔
484

15✔
485
        if s.listener != nil {
17✔
486
                return s.listener.Addr().String()
2✔
487
        }
2✔
488

489
        if s.server != nil {
25✔
490
                return s.server.Addr
12✔
491
        }
12✔
492

493
        return ""
1✔
494
}
495

496
// Logger returns the structured logger instance used by the server.
497
// This logger is used for recording HTTP requests, errors, server lifecycle events,
498
// and can be used by application code for consistent logging.
499
//
500
// The returned logger implements the log.Logger interface and provides
501
// structured logging capabilities with fields and different log levels.
502
func (s *Server) Logger() log.Logger {
1✔
503
        return s.logger
1✔
504
}
1✔
505

506
// SetValidator sets the struct validator instance. This can be used to inject
507
// a custom validation implementation (e.g., go-playground/validator/v10) after
508
// creating the server. If nil, the default built-in validator will be used.
509
//
510
// Example:
511
//
512
//        import "github.com/go-playground/validator/v10"
513
//
514
//        app := zerohttp.New()
515
//        app.SetValidator(&myValidator{v: validator.New()})
516
//
517
// Parameters:
518
//   - validator: A validator instance implementing the config.Validator interface
519
func (s *Server) SetValidator(validator config.Validator) {
1✔
520
        s.mu.Lock()
1✔
521
        defer s.mu.Unlock()
1✔
522
        s.validator = validator
1✔
523
}
1✔
524

525
// Validator returns the configured struct validator (if any).
526
// Returns nil if no custom validator has been configured - in this case,
527
// the default built-in validator (zh.V) should be used.
528
func (s *Server) Validator() config.Validator {
4✔
529
        s.mu.RLock()
4✔
530
        defer s.mu.RUnlock()
4✔
531
        return s.validator
4✔
532
}
4✔
533

534
// Shutdown gracefully shuts down both HTTP and HTTPS servers without interrupting
535
// any active connections. It waits for active connections to finish or for the
536
// provided context to be cancelled.
537
//
538
// Parameters:
539
//   - ctx: Context that controls the shutdown timeout. If the context is cancelled
540
//     before shutdown completes, the servers will be forcefully closed.
541
//
542
// The shutdown process runs concurrently for both servers. If any server
543
// encounters an error during shutdown, that error is returned.
544
// Shutdown hooks are executed during the shutdown process:
545
//   - Pre-shutdown hooks run sequentially before server shutdown begins
546
//   - Shutdown hooks run concurrently with server shutdown
547
//   - Post-shutdown hooks run sequentially after all servers are shut down
548
//
549
// Returns the first error encountered during shutdown, or nil if successful.
550
func (s *Server) Shutdown(ctx context.Context) error {
32✔
551
        s.logger.Info("Shutting down server...")
32✔
552

32✔
553
        // Cancel the base context to signal all requests to close
32✔
554
        // This happens before pre-shutdown hooks so requests can start terminating
32✔
555
        if s.cancelBaseCtx != nil {
64✔
556
                s.cancelBaseCtx()
32✔
557
        }
32✔
558

559
        // Execute pre-shutdown hooks sequentially
560
        if err := s.runPreShutdownHooks(ctx); err != nil {
33✔
561
                s.logger.Error("Pre-shutdown hook error", log.E(err))
1✔
562
                // Return context errors as they indicate shutdown was cancelled
1✔
563
                if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
2✔
564
                        return err
1✔
565
                }
1✔
566
        }
567

568
        // Start shutdown hooks concurrently and wait for them
569
        hookWg, hookErrCh := s.startShutdownHooks(ctx)
31✔
570

31✔
571
        var wg sync.WaitGroup
31✔
572
        errCh := make(chan error, 5) // 5 potential goroutines: server, tlsServer, webTransport, http3, metrics
31✔
573

31✔
574
        if s.server != nil {
61✔
575
                wg.Add(1)
30✔
576
                go func() {
60✔
577
                        defer wg.Done()
30✔
578
                        s.logger.Info("Shutting down HTTP server")
30✔
579
                        if err := s.server.Shutdown(ctx); err != nil {
30✔
580
                                s.logger.Error("Error shutting down HTTP server", log.F("error", err))
×
581
                                errCh <- err
×
582
                        } else {
30✔
583
                                s.logger.Info("HTTP server shutdown complete")
30✔
584
                        }
30✔
585
                }()
586
        }
587

588
        if s.tlsServer != nil {
35✔
589
                wg.Add(1)
4✔
590
                go func() {
8✔
591
                        defer wg.Done()
4✔
592
                        s.logger.Info("Shutting down HTTPS server")
4✔
593
                        if err := s.tlsServer.Shutdown(ctx); err != nil {
4✔
594
                                s.logger.Error("Error shutting down HTTPS server", log.F("error", err))
×
595
                                errCh <- err
×
596
                        } else {
4✔
597
                                s.logger.Info("HTTPS server shutdown complete")
4✔
598
                        }
4✔
599
                }()
600
        }
601

602
        // Shutdown WebTransport and HTTP/3 concurrently
603
        if s.webTransportServer != nil {
34✔
604
                wg.Add(1)
3✔
605
                go func() {
6✔
606
                        defer wg.Done()
3✔
607
                        s.logger.Info("Closing WebTransport server")
3✔
608
                        if err := s.webTransportServer.Close(); err != nil {
4✔
609
                                s.logger.Error("Error closing WebTransport server", log.F("error", err))
1✔
610
                                errCh <- err
1✔
611
                        } else {
3✔
612
                                s.logger.Info("WebTransport server closed")
2✔
613
                        }
2✔
614
                }()
615
        }
616

617
        if s.http3Server != nil {
35✔
618
                wg.Add(1)
4✔
619
                go func() {
8✔
620
                        defer wg.Done()
4✔
621
                        s.logger.Info("Shutting down HTTP/3 server")
4✔
622
                        if err := s.http3Server.Shutdown(ctx); err != nil {
5✔
623
                                s.logger.Error("Error shutting down HTTP/3 server", log.F("error", err))
1✔
624
                                errCh <- err
1✔
625
                        } else {
4✔
626
                                s.logger.Info("HTTP/3 server shutdown complete")
3✔
627
                        }
3✔
628
                }()
629
        }
630

631
        // Shutdown metrics server
632
        if s.metricsServer != nil {
34✔
633
                wg.Add(1)
3✔
634
                go func() {
6✔
635
                        defer wg.Done()
3✔
636
                        s.logger.Info("Shutting down metrics server")
3✔
637
                        if err := s.metricsServer.Shutdown(ctx); err != nil {
3✔
638
                                s.logger.Error("Error shutting down metrics server", log.F("error", err))
×
639
                                errCh <- err
×
640
                        } else {
3✔
641
                                s.logger.Info("Metrics server shutdown complete")
3✔
642
                        }
3✔
643
                }()
644
        }
645

646
        wg.Wait()
31✔
647
        close(errCh)
31✔
648

31✔
649
        // Collect errors from servers and return the first one
31✔
650
        var firstErr error
31✔
651
        for err := range errCh {
33✔
652
                if err != nil && firstErr == nil {
4✔
653
                        firstErr = err
2✔
654
                }
2✔
655
        }
656

657
        // Wait for shutdown hooks to complete
658
        hookWg.Wait()
31✔
659
        close(hookErrCh)
31✔
660

31✔
661
        // Drain hook errors (log them but don't fail shutdown)
31✔
662
        for err := range hookErrCh {
33✔
663
                if err != nil {
4✔
664
                        s.logger.Error("Shutdown hook error", log.E(err))
2✔
665
                }
2✔
666
        }
667

668
        // Execute post-shutdown hooks sequentially
669
        if err := s.runPostShutdownHooks(ctx); err != nil {
31✔
670
                s.logger.Error("Post-shutdown hook error", log.E(err))
×
671
        }
×
672

673
        s.logger.Info("Server shutdown complete")
31✔
674
        return firstErr
31✔
675
}
676

677
// Close immediately closes all server listeners, terminating any active connections.
678
// Unlike Shutdown, this method does not wait for connections to finish gracefully.
679
// It closes both HTTP and HTTPS listeners concurrently.
680
//
681
// This method is thread-safe and can be called multiple times safely.
682
// Returns the last error encountered while closing listeners, or nil if successful.
683
func (s *Server) Close() error {
6✔
684
        s.mu.Lock()
6✔
685
        defer s.mu.Unlock()
6✔
686

6✔
687
        s.logger.Debug("Closing server listeners...")
6✔
688
        var lastErr error
6✔
689

6✔
690
        // Close HTTP server directly - works for both ListenAndServe() and Start()
6✔
691
        if s.server != nil {
12✔
692
                s.logger.Debug("Closing HTTP server")
6✔
693
                if err := s.server.Close(); err != nil {
6✔
694
                        s.logger.Error("Error closing HTTP server", log.F("error", err))
×
695
                        lastErr = err
×
696
                }
×
697
        }
698

699
        // Close HTTPS server directly - works for both ListenAndServe() and Start()
700
        if s.tlsServer != nil {
9✔
701
                s.logger.Debug("Closing HTTPS server")
3✔
702
                if err := s.tlsServer.Close(); err != nil {
3✔
703
                        s.logger.Error("Error closing HTTPS server", log.F("error", err))
×
704
                        lastErr = err
×
705
                }
×
706
        }
707

708
        if s.http3Server != nil {
8✔
709
                s.logger.Debug("Closing HTTP/3 server")
2✔
710
                if err := s.http3Server.Close(); err != nil {
2✔
711
                        s.logger.Error("Error closing HTTP/3 server", log.F("error", err))
×
712
                        lastErr = err
×
713
                }
×
714
        }
715

716
        if s.webTransportServer != nil {
7✔
717
                s.logger.Debug("Closing WebTransport server")
1✔
718
                if err := s.webTransportServer.Close(); err != nil {
1✔
719
                        s.logger.Error("Error closing WebTransport server", log.F("error", err))
×
720
                        lastErr = err
×
721
                }
×
722
        }
723

724
        // Close metrics server directly - works for both ListenAndServe() and Start()
725
        if s.metricsServer != nil {
6✔
726
                s.logger.Debug("Closing metrics server")
×
727
                if err := s.metricsServer.Close(); err != nil {
×
728
                        s.logger.Error("Error closing metrics server", log.F("error", err))
×
729
                        lastErr = err
×
730
                }
×
731
        }
732

733
        if lastErr == nil {
12✔
734
                s.logger.Debug("All listeners closed successfully")
6✔
735
        }
6✔
736

737
        return lastErr
6✔
738
}
739

740
// mergeConfig merges user config with defaults.
741
func mergeConfig(cfg ...config.Config) config.Config {
95✔
742
        c := config.DefaultConfig
95✔
743
        if len(cfg) > 0 {
131✔
744
                userCfg := cfg[0]
36✔
745
                zconfig.Merge(&c, userCfg)
36✔
746
                // Handle fields that must always be copied (even if zero value)
36✔
747
                // ServerAddr can be set to empty string to disable separate metrics server
36✔
748
                c.Metrics.ServerAddr = userCfg.Metrics.ServerAddr
36✔
749
        }
36✔
750
        return c
95✔
751
}
752

753
// createLogger creates a logger instance from config or returns default.
754
func createLogger(c config.Config) log.Logger {
95✔
755
        if c.Logger != nil {
98✔
756
                return c.Logger
3✔
757
        }
3✔
758
        return log.NewDefaultLogger()
92✔
759
}
760

761
// createHTTPServer creates the HTTP server from config.
762
func createHTTPServer(c config.Config) *http.Server {
95✔
763
        if c.Server != nil {
95✔
764
                return c.Server
×
765
        }
×
766
        return &http.Server{
95✔
767
                Addr:              c.Addr,
95✔
768
                ReadTimeout:       DefaultReadTimeout,
95✔
769
                ReadHeaderTimeout: DefaultReadHeaderTimeout,
95✔
770
                WriteTimeout:      DefaultWriteTimeout,
95✔
771
                IdleTimeout:       DefaultIdleTimeout,
95✔
772
                MaxHeaderBytes:    1 << 20, // 1 MB
95✔
773
        }
95✔
774
}
775

776
// createTLSServer creates the TLS server from config if TLS is configured.
777
func createTLSServer(c config.Config) *http.Server {
95✔
778
        if c.TLS.Server != nil {
95✔
779
                return c.TLS.Server
×
780
        }
×
781
        if !needsTLSServer(c) {
178✔
782
                return nil
83✔
783
        }
83✔
784
        return &http.Server{
12✔
785
                Addr:              c.TLS.Addr,
12✔
786
                ReadTimeout:       DefaultReadTimeout,
12✔
787
                ReadHeaderTimeout: DefaultReadHeaderTimeout,
12✔
788
                WriteTimeout:      DefaultWriteTimeout,
12✔
789
                IdleTimeout:       DefaultIdleTimeout,
12✔
790
                MaxHeaderBytes:    1 << 20, // 1 MB
12✔
791
                TLSConfig: &tls.Config{
12✔
792
                        MinVersion: tls.VersionTLS12,
12✔
793
                        NextProtos: []string{"h2", "http/1.1"},
12✔
794
                },
12✔
795
        }
12✔
796
}
797

798
// needsTLSServer returns true if the config requires a TLS server to be created.
799
func needsTLSServer(c config.Config) bool {
95✔
800
        return c.TLS.CertFile != "" ||
95✔
801
                c.TLS.KeyFile != "" ||
95✔
802
                c.Extensions.AutocertManager != nil ||
95✔
803
                c.TLS.Listener != nil ||
95✔
804
                c.Extensions.HTTP3Server != nil
95✔
805
}
95✔
806

807
// createMetricsRegistry creates metrics registry if enabled.
808
func createMetricsRegistry(c config.Config) metrics.Registry {
95✔
809
        if c.Metrics.Enabled {
99✔
810
                return metrics.NewRegistry()
4✔
811
        }
4✔
812
        return nil
91✔
813
}
814

815
// createMetricsServer creates a separate metrics server if ServerAddr is set.
816
func createMetricsServer(c config.Config, registry metrics.Registry) *http.Server {
95✔
817
        if !c.Metrics.Enabled || registry == nil || c.Metrics.ServerAddr == "" {
188✔
818
                return nil
93✔
819
        }
93✔
820
        return &http.Server{
2✔
821
                Addr:              c.Metrics.ServerAddr,
2✔
822
                ReadTimeout:       DefaultReadTimeout,
2✔
823
                ReadHeaderTimeout: DefaultReadHeaderTimeout,
2✔
824
                WriteTimeout:      DefaultWriteTimeout,
2✔
825
                IdleTimeout:       DefaultIdleTimeout,
2✔
826
                Handler:           metrics.Handler(registry),
2✔
827
        }
2✔
828
}
829

830
// setupMiddleware configures the middleware chain on the server.
831
func setupMiddleware(s *Server, c config.Config, registry metrics.Registry) {
95✔
832
        var middlewares []func(http.Handler) http.Handler
95✔
833

95✔
834
        // Add metrics middleware first so it will be innermost after reverse,
95✔
835
        // running inside Recover and able to capture status codes written by other middleware
95✔
836
        if c.Metrics.Enabled && registry != nil {
99✔
837
                middlewares = append(middlewares, metrics.NewMiddleware(registry, c.Metrics))
4✔
838
        }
4✔
839

840
        if c.DisableDefaultMiddlewares {
96✔
841
                middlewares = append(middlewares, c.DefaultMiddlewares...)
1✔
842
        } else if c.DefaultMiddlewares == nil {
188✔
843
                middlewares = append(middlewares, middleware.DefaultMiddlewares(c, s.logger)...)
93✔
844
        } else {
94✔
845
                defaults := middleware.DefaultMiddlewares(c, s.logger)
1✔
846
                middlewares = append(middlewares, defaults...)
1✔
847
                middlewares = append(middlewares, c.DefaultMiddlewares...)
1✔
848
        }
1✔
849

850
        if len(middlewares) > 0 {
190✔
851
                s.Use(middlewares...)
95✔
852
        }
95✔
853
}
854

855
// setupServerHandlers sets the router and base context on server instances.
856
func setupServerHandlers(s *Server, router Router) {
95✔
857
        if s.server != nil {
190✔
858
                s.server.Handler = router
95✔
859
                s.server.BaseContext = func(net.Listener) context.Context {
99✔
860
                        return s.baseCtx
4✔
861
                }
4✔
862
        }
863

864
        if s.tlsServer != nil {
107✔
865
                s.tlsServer.Handler = router
12✔
866
                s.tlsServer.BaseContext = func(net.Listener) context.Context {
21✔
867
                        return s.baseCtx
9✔
868
                }
9✔
869
        }
870
}
871

872
// registerMetricsEndpoint registers the metrics endpoint on the main router if needed.
873
func registerMetricsEndpoint(s *Server, c config.Config, registry metrics.Registry) {
95✔
874
        if c.Metrics.Enabled && registry != nil && c.Metrics.ServerAddr == "" {
97✔
875
                s.logger.Warn("Metrics endpoint registered on main server (set Metrics.ServerAddr to isolate)", log.F("endpoint", c.Metrics.Endpoint))
2✔
876
                s.GET(c.Metrics.Endpoint, metrics.Handler(registry))
2✔
877
        }
2✔
878
}
879

880
func fmtHTTPAddr(addr string) string {
26✔
881
        return fmt.Sprintf("http://%s", addr)
26✔
882
}
26✔
883

884
func fmtHTTPSAddr(addr string) string {
15✔
885
        return fmt.Sprintf("https://%s", addr)
15✔
886
}
15✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc