Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 13:30 UTC
Wire up Cause for most context cancels (#7538)
Francis Lavoie committed
Mar 4, 2026 at 19:14 UTC
6e5e08cf582649b4eb46fa201ea05e6131870363
6 files changed
+42
-23
admin.go
+5
-1
@@ -749,10 +749,14 @@ func stopAdminServer(srv *http.Server) error {
749
if srv == nil {
750
return fmt.Errorf("no admin server")
751
}
752
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
752
+ timeout := 10 * time.Second
753
+ ctx, cancel := context.WithTimeoutCause(context.Background(), timeout, fmt.Errorf("stopping admin server: %ds timeout", int(timeout.Seconds())))
754
defer cancel()
755
err := srv.Shutdown(ctx)
756
if err != nil {
757
+ if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
758
+ err = cause
759
+ }
760
return fmt.Errorf("shutting down admin server: %v", err)
761
}
762
Log().Named("admin").Info("stopped previous server", zap.String("address", srv.Addr))
caddy.go
+7
-7
@@ -88,7 +88,7 @@ type Config struct {
88
storage certmagic.Storage
89
eventEmitter eventEmitter
90
91
- cancelFunc context.CancelFunc
91
+ cancelFunc context.CancelCauseFunc
92
93
// fileSystems is a dict of fileSystems that will later be loaded from and added to.
94
fileSystems FileSystems
@@ -433,7 +433,7 @@ func run(newCfg *Config, start bool) (Context, error) {
433
// partially copied from provisionContext
434
if err != nil {
435
globalMetrics.configSuccess.Set(0)
436
- ctx.cfg.cancelFunc()
436
+ ctx.cfg.cancelFunc(fmt.Errorf("configuration start error: %w", err))
437
438
if currentCtx.cfg != nil {
439
certmagic.Default.Storage = currentCtx.cfg.storage
@@ -509,7 +509,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
509
// cleanup occurs when we return if there
510
// was an error; if no error, it will get
511
// cleaned up on next config cycle
512
- ctx, cancel := NewContext(Context{Context: context.Background(), cfg: newCfg})
512
+ ctx, cancelCause := NewContextWithCause(Context{Context: context.Background(), cfg: newCfg})
513
defer func() {
514
if err != nil {
515
globalMetrics.configSuccess.Set(0)
@@ -518,7 +518,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
518
// since the associated config won't be used;
519
// this will cause all modules that were newly
520
// provisioned to clean themselves up
521
- cancel()
521
+ cancelCause(fmt.Errorf("configuration error: %w", err))
522
523
// also undo any other state changes we made
524
if currentCtx.cfg != nil {
@@ -526,7 +526,7 @@ func provisionContext(newCfg *Config, replaceAdminServer bool) (Context, error)
526
}
527
}
528
}()
529
- newCfg.cancelFunc = cancel // clean up later
529
+ newCfg.cancelFunc = cancelCause // clean up later
530
531
// set up logging before anything bad happens
532
if newCfg.Logging == nil {
@@ -746,7 +746,7 @@ func unsyncedStop(ctx Context) {
746
}
747
748
// clean up all modules
749
- ctx.cfg.cancelFunc()
749
+ ctx.cfg.cancelFunc(fmt.Errorf("stopping apps"))
750
}
751
752
// Validate loads, provisions, and validates
@@ -754,7 +754,7 @@ func unsyncedStop(ctx Context) {
754
func Validate(cfg *Config) error {
755
_, err := run(cfg, false)
756
if err == nil {
757
- cfg.cancelFunc() // call Cleanup on all modules
757
+ cfg.cancelFunc(fmt.Errorf("validation complete")) // call Cleanup on all modules
758
}
759
return err
760
}
context.go
+10
-3
@@ -63,10 +63,17 @@ type Context struct {
63
// modules which are loaded will be properly unloaded.
64
// See standard library context package's documentation.
65
func NewContext(ctx Context) (Context, context.CancelFunc) {
66
+ newCtx, cancelCause := NewContextWithCause(ctx)
67
+ return newCtx, func() { cancelCause(nil) }
68
+}
69
+
70
+// NewContextWithCause is like NewContext but returns a context.CancelCauseFunc.
71
+// EXPERIMENTAL: This API is subject to change.
72
+func NewContextWithCause(ctx Context) (Context, context.CancelCauseFunc) {
73
newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg, metricsRegistry: prometheus.NewPedanticRegistry()}
67
- c, cancel := context.WithCancel(ctx.Context)
68
- wrappedCancel := func() {
69
- cancel()
74
+ c, cancel := context.WithCancelCause(ctx.Context)
75
+ wrappedCancel := func(cause error) {
76
+ cancel(cause)
77
78
for _, f := range ctx.cleanupFuncs {
79
f()
listeners.go
+9
-9
@@ -512,7 +512,7 @@ func ListenerUsage(network, addr string) int {
512
// contextAndCancelFunc groups context and its cancelFunc
513
type contextAndCancelFunc struct {
514
context.Context
515
- context.CancelFunc
515
+ context.CancelCauseFunc
516
}
517
518
// sharedQUICState manages GetConfigForClient
@@ -542,17 +542,17 @@ func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Co
542
543
// addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc
544
// so that when cancelled, the active tls.Config will change
545
-func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelFunc) {
545
+func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) {
546
sqs.rmu.Lock()
547
defer sqs.rmu.Unlock()
548
549
if cacc, ok := sqs.tlsConfs[tlsConfig]; ok {
550
- return cacc.Context, cacc.CancelFunc
550
+ return cacc.Context, cacc.CancelCauseFunc
551
}
552
553
- ctx, cancel := context.WithCancel(context.Background())
554
- wrappedCancel := func() {
555
- cancel()
553
+ ctx, cancel := context.WithCancelCause(context.Background())
554
+ wrappedCancel := func(cause error) {
555
+ cancel(cause)
556
557
sqs.rmu.Lock()
558
defer sqs.rmu.Unlock()
@@ -608,13 +608,13 @@ func fakeClosedErr(l interface{ Addr() net.Addr }) error {
608
// indicating that it is pretending to be closed so that the
609
// server using it can terminate, while the underlying
610
// socket is actually left open.
611
-var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
611
+var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉")
612
613
type fakeCloseQuicListener struct {
614
closed int32 // accessed atomically; belongs to this struct only
615
*sharedQuicListener // embedded, so we also become a quic.EarlyListener
616
context context.Context
617
- contextCancel context.CancelFunc
617
+ contextCancel context.CancelCauseFunc
618
}
619
620
// Currently Accept ignores the passed context, however a situation where
@@ -637,7 +637,7 @@ func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error)
637
638
func (fcql *fakeCloseQuicListener) Close() error {
639
if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
640
- fcql.contextCancel()
640
+ fcql.contextCancel(errFakeClosed)
641
} else if atomic.CompareAndSwapInt32(&fcql.closed, 1, 2) {
642
_, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
643
}
modules/caddyhttp/app.go
+10
-2
@@ -18,6 +18,7 @@ import (
18
"cmp"
19
"context"
20
"crypto/tls"
21
+ "errors"
22
"fmt"
23
"maps"
24
"net"
@@ -711,9 +712,10 @@ func (app *App) Stop() error {
712
// enforce grace period if configured
713
if app.GracePeriod > 0 {
714
var cancel context.CancelFunc
714
- ctx, cancel = context.WithTimeout(ctx, time.Duration(app.GracePeriod))
715
+ timeout := time.Duration(app.GracePeriod)
716
+ ctx, cancel = context.WithTimeoutCause(ctx, timeout, fmt.Errorf("server graceful shutdown %ds timeout", int(timeout.Seconds())))
717
defer cancel()
716
- app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", time.Duration(app.GracePeriod)))
718
+ app.logger.Info("servers shutting down; grace period initiated", zap.Duration("duration", timeout))
719
} else {
720
app.logger.Info("servers shutting down with eternal grace period")
721
}
@@ -739,6 +741,9 @@ func (app *App) Stop() error {
741
}
742
743
if err := server.server.Shutdown(ctx); err != nil {
744
+ if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
745
+ err = cause
746
+ }
747
app.logger.Error("server shutdown",
748
zap.Error(err),
749
zap.Strings("addresses", server.Listen))
@@ -762,6 +767,9 @@ func (app *App) Stop() error {
767
}
768
769
if err := server.h3server.Shutdown(ctx); err != nil {
770
+ if cause := context.Cause(ctx); cause != nil && errors.Is(err, context.DeadlineExceeded) {
771
+ err = cause
772
+ }
773
app.logger.Error("HTTP/3 server shutdown",
774
zap.Error(err),
775
zap.Strings("addresses", server.Listen))
modules/caddyhttp/reverseproxy/httptransport.go
+1
-1
@@ -448,7 +448,7 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
448
// complete the handshake before returning the connection
449
if rt.TLSHandshakeTimeout != 0 {
450
var cancel context.CancelFunc
451
- ctx, cancel = context.WithTimeout(ctx, rt.TLSHandshakeTimeout)
451
+ ctx, cancel = context.WithTimeoutCause(ctx, rt.TLSHandshakeTimeout, fmt.Errorf("HTTP transport TLS handshake %ds timeout", int(rt.TLSHandshakeTimeout.Seconds())))
452
defer cancel()
453
}
454
err = tlsConn.HandshakeContext(ctx)