@marcel91200 / caddy Mirror · read-only / commits / 6e5e08cf
Fork 0

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)