From 15519bfb416423fc2b1c0603edeb5d18253861e9 Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Wed, 8 Apr 2026 14:11:42 +0800 Subject: [PATCH 1/3] feat: add DefaultTimeoutInterceptor for server-side request timeout Adds a unary server interceptor that applies a configurable default deadline (60s) to incoming gRPC requests that arrive without one. Requests with an existing deadline are left unchanged. The interceptor is inserted as the first ColdBrew interceptor in the default chain, before ResponseTimeLoggingInterceptor. - SetDefaultTimeout(d) setter for programmatic config - 0 disables the interceptor (pass-through) - 4 new tests covering: applies timeout, existing deadline preserved, disabled mode, chain inclusion --- README.md | 96 +++++++++++++++++++++++--------------- interceptors.go | 28 +++++++++++ interceptors_test.go | 108 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index bd0da6d..4332fd6 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Interceptor configuration functions \(AddUnaryServerInterceptor, SetFilterFunc, - [func DefaultClientStreamInterceptors\(defaultOpts ...any\) \[\]grpc.StreamClientInterceptor](<#DefaultClientStreamInterceptors>) - [func DefaultInterceptors\(\) \[\]grpc.UnaryServerInterceptor](<#DefaultInterceptors>) - [func DefaultStreamInterceptors\(\) \[\]grpc.StreamServerInterceptor](<#DefaultStreamInterceptors>) +- [func DefaultTimeoutInterceptor\(\) grpc.UnaryServerInterceptor](<#DefaultTimeoutInterceptor>) - [func DoHTTPtoGRPC\(ctx context.Context, svr any, handler func\(ctx context.Context, req any\) \(any, error\), in any\) \(any, error\)](<#DoHTTPtoGRPC>) - [func FilterMethodsFunc\(ctx context.Context, fullMethodName string\) bool](<#FilterMethodsFunc>) - [func GRPCClientInterceptor\(\_ ...any\) grpc.UnaryClientInterceptor](<#GRPCClientInterceptor>) @@ -48,6 +49,7 @@ Interceptor configuration functions \(AddUnaryServerInterceptor, SetFilterFunc, - [func ServerErrorInterceptor\(\) grpc.UnaryServerInterceptor](<#ServerErrorInterceptor>) - [func ServerErrorStreamInterceptor\(\) grpc.StreamServerInterceptor](<#ServerErrorStreamInterceptor>) - [func SetClientMetricsOptions\(opts ...grpcprom.ClientMetricsOption\)](<#SetClientMetricsOptions>) +- [func SetDefaultTimeout\(d time.Duration\)](<#SetDefaultTimeout>) - [func SetDisableProtoValidate\(disable bool\)](<#SetDisableProtoValidate>) - [func SetFilterFunc\(ctx context.Context, ff FilterFunc\)](<#SetFilterFunc>) - [func SetFilterMethods\(ctx context.Context, methods \[\]string\)](<#SetFilterMethods>) @@ -84,7 +86,7 @@ var ( ``` -## func [AddStreamClientInterceptor]() +## func [AddStreamClientInterceptor]() ```go func AddStreamClientInterceptor(ctx context.Context, i ...grpc.StreamClientInterceptor) @@ -93,7 +95,7 @@ func AddStreamClientInterceptor(ctx context.Context, i ...grpc.StreamClientInter AddStreamClientInterceptor adds a client stream interceptor to default client stream interceptors. Must be called during initialization, before any RPCs are made. Not safe for concurrent use. -## func [AddStreamServerInterceptor]() +## func [AddStreamServerInterceptor]() ```go func AddStreamServerInterceptor(ctx context.Context, i ...grpc.StreamServerInterceptor) @@ -102,7 +104,7 @@ func AddStreamServerInterceptor(ctx context.Context, i ...grpc.StreamServerInter AddStreamServerInterceptor adds a server interceptor to default server interceptors. Must be called during initialization, before the server starts. Not safe for concurrent use. -## func [AddUnaryClientInterceptor]() +## func [AddUnaryClientInterceptor]() ```go func AddUnaryClientInterceptor(ctx context.Context, i ...grpc.UnaryClientInterceptor) @@ -111,7 +113,7 @@ func AddUnaryClientInterceptor(ctx context.Context, i ...grpc.UnaryClientInterce AddUnaryClientInterceptor adds a client interceptor to default client interceptors. Must be called during initialization, before any RPCs are made. Not safe for concurrent use. -## func [AddUnaryServerInterceptor]() +## func [AddUnaryServerInterceptor]() ```go func AddUnaryServerInterceptor(ctx context.Context, i ...grpc.UnaryServerInterceptor) @@ -120,7 +122,7 @@ func AddUnaryServerInterceptor(ctx context.Context, i ...grpc.UnaryServerInterce AddUnaryServerInterceptor adds a server interceptor to default server interceptors. Must be called during initialization, before the server starts. Not safe for concurrent use. -## func [DebugLoggingInterceptor]() +## func [DebugLoggingInterceptor]() ```go func DebugLoggingInterceptor() grpc.UnaryServerInterceptor @@ -129,7 +131,7 @@ func DebugLoggingInterceptor() grpc.UnaryServerInterceptor DebugLoggingInterceptor is the interceptor that logs all request/response from a handler -## func [DefaultClientInterceptor]() +## func [DefaultClientInterceptor]() ```go func DefaultClientInterceptor(defaultOpts ...any) grpc.UnaryClientInterceptor @@ -138,7 +140,7 @@ func DefaultClientInterceptor(defaultOpts ...any) grpc.UnaryClientInterceptor DefaultClientInterceptor are the set of default interceptors that should be applied to all client calls -## func [DefaultClientInterceptors]() +## func [DefaultClientInterceptors]() ```go func DefaultClientInterceptors(defaultOpts ...any) []grpc.UnaryClientInterceptor @@ -147,7 +149,7 @@ func DefaultClientInterceptors(defaultOpts ...any) []grpc.UnaryClientInterceptor DefaultClientInterceptors are the set of default interceptors that should be applied to all client calls -## func [DefaultClientStreamInterceptor]() +## func [DefaultClientStreamInterceptor]() ```go func DefaultClientStreamInterceptor(defaultOpts ...any) grpc.StreamClientInterceptor @@ -156,7 +158,7 @@ func DefaultClientStreamInterceptor(defaultOpts ...any) grpc.StreamClientInterce DefaultClientStreamInterceptor are the set of default interceptors that should be applied to all stream client calls -## func [DefaultClientStreamInterceptors]() +## func [DefaultClientStreamInterceptors]() ```go func DefaultClientStreamInterceptors(defaultOpts ...any) []grpc.StreamClientInterceptor @@ -165,7 +167,7 @@ func DefaultClientStreamInterceptors(defaultOpts ...any) []grpc.StreamClientInte DefaultClientStreamInterceptors are the set of default interceptors that should be applied to all stream client calls -## func [DefaultInterceptors]() +## func [DefaultInterceptors]() ```go func DefaultInterceptors() []grpc.UnaryServerInterceptor @@ -174,7 +176,7 @@ func DefaultInterceptors() []grpc.UnaryServerInterceptor DefaultInterceptors are the set of default interceptors that are applied to all coldbrew methods -## func [DefaultStreamInterceptors]() +## func [DefaultStreamInterceptors]() ```go func DefaultStreamInterceptors() []grpc.StreamServerInterceptor @@ -182,8 +184,17 @@ func DefaultStreamInterceptors() []grpc.StreamServerInterceptor DefaultStreamInterceptors are the set of default interceptors that should be applied to all coldbrew streams + +## func [DefaultTimeoutInterceptor]() + +```go +func DefaultTimeoutInterceptor() grpc.UnaryServerInterceptor +``` + +DefaultTimeoutInterceptor returns a unary server interceptor that applies a default deadline to incoming requests that have no deadline set. If the incoming context already has a deadline \(regardless of duration\), it is left unchanged. When defaultTimeout is 0, the interceptor is a no\-op pass\-through. + -## func [DoHTTPtoGRPC]() +## func [DoHTTPtoGRPC]() ```go func DoHTTPtoGRPC(ctx context.Context, svr any, handler func(ctx context.Context, req any) (any, error), in any) (any, error) @@ -209,7 +220,7 @@ func (s *svc) echo(ctx context.Context, req *proto.EchoRequest) (*proto.EchoResp ``` -## func [FilterMethodsFunc]() +## func [FilterMethodsFunc]() ```go func FilterMethodsFunc(ctx context.Context, fullMethodName string) bool @@ -218,7 +229,7 @@ func FilterMethodsFunc(ctx context.Context, fullMethodName string) bool FilterMethodsFunc is the default implementation of Filter function -## func [GRPCClientInterceptor]() +## func [GRPCClientInterceptor]() ```go func GRPCClientInterceptor(_ ...any) grpc.UnaryClientInterceptor @@ -227,7 +238,7 @@ func GRPCClientInterceptor(_ ...any) grpc.UnaryClientInterceptor Deprecated: GRPCClientInterceptor is no longer needed. gRPC tracing is now handled by google.golang.org/grpc/stats/opentelemetry, configured via opentelemetry.DialOption\(\) at the client level. This function is retained for backwards compatibility but returns a no\-op interceptor. -## func [HystrixClientInterceptor]() +## func [HystrixClientInterceptor]() ```go func HystrixClientInterceptor(defaultOpts ...grpc.CallOption) grpc.UnaryClientInterceptor @@ -240,7 +251,7 @@ Note: This interceptor wraps github.com/afex/hystrix\-go which has been unmainta The interceptor applies provided default and per\-call client options to configure Hystrix behavior \(for example the command name, disabled flag, excluded errors, and excluded gRPC status codes\). If Hystrix is disabled via options, the RPC is invoked directly. If the underlying RPC returns an error that matches any configured excluded error or whose gRPC status code matches any configured excluded code, Hystrix fallback is skipped and the RPC error is returned. Panics raised during the RPC invocation are captured and reported to the notifier before being converted into an error. If the RPC itself returns an error, that error is returned; otherwise any error produced by Hystrix is returned. -## func [NRHttpTracer]() +## func [NRHttpTracer]() ```go func NRHttpTracer(pattern string, h http.HandlerFunc) (string, http.HandlerFunc) @@ -249,7 +260,7 @@ func NRHttpTracer(pattern string, h http.HandlerFunc) (string, http.HandlerFunc) NRHttpTracer adds newrelic tracing to this http function -## func [NewRelicClientInterceptor]() +## func [NewRelicClientInterceptor]() ```go func NewRelicClientInterceptor() grpc.UnaryClientInterceptor @@ -258,7 +269,7 @@ func NewRelicClientInterceptor() grpc.UnaryClientInterceptor NewRelicClientInterceptor intercepts all client actions and reports them to newrelic. When NewRelic app is nil \(no license key configured\), returns a pass\-through interceptor to avoid overhead. -## func [NewRelicInterceptor]() +## func [NewRelicInterceptor]() ```go func NewRelicInterceptor() grpc.UnaryServerInterceptor @@ -267,7 +278,7 @@ func NewRelicInterceptor() grpc.UnaryServerInterceptor NewRelicInterceptor intercepts all server actions and reports them to newrelic. When NewRelic app is nil \(no license key configured\), returns a pass\-through interceptor to avoid overhead. -## func [OptionsInterceptor]() +## func [OptionsInterceptor]() ```go func OptionsInterceptor() grpc.UnaryServerInterceptor @@ -276,7 +287,7 @@ func OptionsInterceptor() grpc.UnaryServerInterceptor -## func [PanicRecoveryInterceptor]() +## func [PanicRecoveryInterceptor]() ```go func PanicRecoveryInterceptor() grpc.UnaryServerInterceptor @@ -285,7 +296,7 @@ func PanicRecoveryInterceptor() grpc.UnaryServerInterceptor -## func [ProtoValidateInterceptor]() +## func [ProtoValidateInterceptor]() ```go func ProtoValidateInterceptor() grpc.UnaryServerInterceptor @@ -294,7 +305,7 @@ func ProtoValidateInterceptor() grpc.UnaryServerInterceptor ProtoValidateInterceptor returns a unary server interceptor that validates incoming messages using protovalidate annotations. Returns InvalidArgument on validation failure. Uses GlobalValidator by default; if custom options are set via SetProtoValidateOptions, creates a new validator with those options. -## func [ProtoValidateStreamInterceptor]() +## func [ProtoValidateStreamInterceptor]() ```go func ProtoValidateStreamInterceptor() grpc.StreamServerInterceptor @@ -303,7 +314,7 @@ func ProtoValidateStreamInterceptor() grpc.StreamServerInterceptor ProtoValidateStreamInterceptor returns a stream server interceptor that validates incoming messages using protovalidate annotations. -## func [ResponseTimeLoggingInterceptor]() +## func [ResponseTimeLoggingInterceptor]() ```go func ResponseTimeLoggingInterceptor(ff FilterFunc) grpc.UnaryServerInterceptor @@ -312,7 +323,7 @@ func ResponseTimeLoggingInterceptor(ff FilterFunc) grpc.UnaryServerInterceptor ResponseTimeLoggingInterceptor logs response time for each request on server -## func [ResponseTimeLoggingStreamInterceptor]() +## func [ResponseTimeLoggingStreamInterceptor]() ```go func ResponseTimeLoggingStreamInterceptor() grpc.StreamServerInterceptor @@ -321,7 +332,7 @@ func ResponseTimeLoggingStreamInterceptor() grpc.StreamServerInterceptor ResponseTimeLoggingStreamInterceptor logs response time for stream RPCs. -## func [ServerErrorInterceptor]() +## func [ServerErrorInterceptor]() ```go func ServerErrorInterceptor() grpc.UnaryServerInterceptor @@ -330,7 +341,7 @@ func ServerErrorInterceptor() grpc.UnaryServerInterceptor ServerErrorInterceptor intercepts all server actions and reports them to error notifier -## func [ServerErrorStreamInterceptor]() +## func [ServerErrorStreamInterceptor]() ```go func ServerErrorStreamInterceptor() grpc.StreamServerInterceptor @@ -339,7 +350,7 @@ func ServerErrorStreamInterceptor() grpc.StreamServerInterceptor ServerErrorStreamInterceptor intercepts server errors for stream RPCs and reports them to the error notifier. -## func [SetClientMetricsOptions]() +## func [SetClientMetricsOptions]() ```go func SetClientMetricsOptions(opts ...grpcprom.ClientMetricsOption) @@ -347,8 +358,17 @@ func SetClientMetricsOptions(opts ...grpcprom.ClientMetricsOption) SetClientMetricsOptions appends gRPC client metrics options. Must be called during initialization, before the server starts. Not safe for concurrent use. + +## func [SetDefaultTimeout]() + +```go +func SetDefaultTimeout(d time.Duration) +``` + +SetDefaultTimeout sets the default timeout applied to incoming unary RPCs that arrive without a deadline. When set to 0, the timeout interceptor is disabled \(pass\-through\). Default is 60s. Must be called during initialization, before the server starts. Not safe for concurrent use. + -## func [SetDisableProtoValidate]() +## func [SetDisableProtoValidate]() ```go func SetDisableProtoValidate(disable bool) @@ -357,7 +377,7 @@ func SetDisableProtoValidate(disable bool) SetDisableProtoValidate disables the protovalidate interceptor in the default chain. Must be called during init\(\) — not safe for concurrent use. -## func [SetFilterFunc]() +## func [SetFilterFunc]() ```go func SetFilterFunc(ctx context.Context, ff FilterFunc) @@ -366,7 +386,7 @@ func SetFilterFunc(ctx context.Context, ff FilterFunc) SetFilterFunc sets the default filter function to be used by interceptors. Must be called during initialization, before the server starts. Not safe for concurrent use. -## func [SetFilterMethods]() +## func [SetFilterMethods]() ```go func SetFilterMethods(ctx context.Context, methods []string) @@ -375,7 +395,7 @@ func SetFilterMethods(ctx context.Context, methods []string) SetFilterMethods sets the list of method substrings to exclude from tracing/logging. It rebuilds the internal cache. Must be called during initialization, before the server starts. Not safe for concurrent use. -## func [SetProtoValidateOptions]() +## func [SetProtoValidateOptions]() ```go func SetProtoValidateOptions(opts ...protovalidate.ValidatorOption) @@ -384,7 +404,7 @@ func SetProtoValidateOptions(opts ...protovalidate.ValidatorOption) SetProtoValidateOptions configures custom protovalidate options \(e.g., custom constraints\). Must be called during init\(\) — not safe for concurrent use. Follows ColdBrew's init\-only config pattern. -## func [SetResponseTimeLogErrorOnly]() +## func [SetResponseTimeLogErrorOnly]() ```go func SetResponseTimeLogErrorOnly(errorOnly bool) @@ -393,7 +413,7 @@ func SetResponseTimeLogErrorOnly(errorOnly bool) SetResponseTimeLogErrorOnly when set to true, only logs response time when the request returns an error. Successful requests are not logged. Must be called during initialization, before the server starts. Not safe for concurrent use. -## func [SetResponseTimeLogLevel]() +## func [SetResponseTimeLogLevel]() ```go func SetResponseTimeLogLevel(ctx context.Context, level loggers.Level) @@ -402,7 +422,7 @@ func SetResponseTimeLogLevel(ctx context.Context, level loggers.Level) SetResponseTimeLogLevel sets the log level for response time logging. Default is InfoLevel. Must be called during initialization, before the server starts. Not safe for concurrent use. -## func [SetServerMetricsOptions]() +## func [SetServerMetricsOptions]() ```go func SetServerMetricsOptions(opts ...grpcprom.ServerMetricsOption) @@ -411,7 +431,7 @@ func SetServerMetricsOptions(opts ...grpcprom.ServerMetricsOption) SetServerMetricsOptions appends gRPC server metrics options \(histogram, labels, namespace, etc.\). Must be called during initialization, before the server starts. Not safe for concurrent use. -## func [TraceIdInterceptor]() +## func [TraceIdInterceptor]() ```go func TraceIdInterceptor() grpc.UnaryServerInterceptor @@ -420,7 +440,7 @@ func TraceIdInterceptor() grpc.UnaryServerInterceptor TraceIdInterceptor allows injecting trace id from request objects -## func [UseColdBrewClientInterceptors]() +## func [UseColdBrewClientInterceptors]() ```go func UseColdBrewClientInterceptors(ctx context.Context, flag bool) @@ -429,7 +449,7 @@ func UseColdBrewClientInterceptors(ctx context.Context, flag bool) UseColdBrewClientInterceptors allows enabling/disabling coldbrew client interceptors. When set to false, the coldbrew client interceptors will not be used. Must be called during initialization, before any RPCs are made. Not safe for concurrent use. -## func [UseColdBrewServerInterceptors]() +## func [UseColdBrewServerInterceptors]() ```go func UseColdBrewServerInterceptors(ctx context.Context, flag bool) @@ -438,7 +458,7 @@ func UseColdBrewServerInterceptors(ctx context.Context, flag bool) UseColdBrewServerInterceptors allows enabling/disabling coldbrew server interceptors. When set to false, the coldbrew server interceptors will not be used. Must be called during initialization, before the server starts. Not safe for concurrent use. -## type [FilterFunc]() +## type [FilterFunc]() If it returns false, the given request will not be traced. diff --git a/interceptors.go b/interceptors.go index b6a4cd8..5add7db 100644 --- a/interceptors.go +++ b/interceptors.go @@ -60,6 +60,7 @@ var ( useCBClientInterceptors = true responseTimeLogLevel loggers.Level = loggers.InfoLevel responseTimeLogErrorOnly bool + defaultTimeout time.Duration = 60 * time.Second // 0 disables protoValidateOpts []protovalidate.ValidatorOption disableProtoValidate bool srvMetricsOpts []grpcprom.ServerMetricsOption @@ -83,6 +84,14 @@ func SetResponseTimeLogErrorOnly(errorOnly bool) { responseTimeLogErrorOnly = errorOnly } +// SetDefaultTimeout sets the default timeout applied to incoming unary RPCs +// that arrive without a deadline. When set to 0, the timeout interceptor is +// disabled (pass-through). Default is 60s. +// Must be called during initialization, before the server starts. Not safe for concurrent use. +func SetDefaultTimeout(d time.Duration) { + defaultTimeout = d +} + // If it returns false, the given request will not be traced. type FilterFunc func(ctx context.Context, fullMethodName string) bool @@ -417,6 +426,7 @@ func DefaultInterceptors() []grpc.UnaryServerInterceptor { } if useCBServerInterceptors { ints = append(ints, + DefaultTimeoutInterceptor(), ResponseTimeLoggingInterceptor(defaultFilterFunc), TraceIdInterceptor(), ) @@ -515,6 +525,24 @@ func DebugLoggingInterceptor() grpc.UnaryServerInterceptor { } } +// DefaultTimeoutInterceptor returns a unary server interceptor that applies a +// default deadline to incoming requests that have no deadline set. If the +// incoming context already has a deadline (regardless of duration), it is left +// unchanged. When defaultTimeout is 0, the interceptor is a no-op pass-through. +func DefaultTimeoutInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + if defaultTimeout <= 0 { + return handler(ctx, req) + } + if _, ok := ctx.Deadline(); ok { + return handler(ctx, req) + } + ctx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + return handler(ctx, req) + } +} + // ResponseTimeLoggingInterceptor logs response time for each request on server func ResponseTimeLoggingInterceptor(ff FilterFunc) grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) { diff --git a/interceptors_test.go b/interceptors_test.go index 77ef410..2f74afc 100644 --- a/interceptors_test.go +++ b/interceptors_test.go @@ -8,6 +8,7 @@ import ( "sync" "sync/atomic" "testing" + "time" "github.com/go-coldbrew/log/loggers" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -42,6 +43,7 @@ func resetGlobals() { useCBClientInterceptors = true responseTimeLogErrorOnly = false responseTimeLogLevel = loggers.InfoLevel + defaultTimeout = 60 * time.Second httpToGRPCOnce = sync.Once{} httpToGRPCInterceptor = nil } @@ -1029,3 +1031,109 @@ func TestDoHTTPtoGRPC_InterceptorCaching(t *testing.T) { t.Error("interceptor added after first DoHTTPtoGRPC call should not be in the cached chain") } } + +func TestDefaultTimeoutInterceptor_AppliesTimeout(t *testing.T) { + defer resetGlobals() + + interceptor := DefaultTimeoutInterceptor() + info := &grpc.UnaryServerInfo{FullMethod: "/test.Service/Method"} + ctx := context.Background() + + handler := func(ctx context.Context, req any) (any, error) { + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("expected deadline to be set") + } + remaining := time.Until(deadline) + if remaining < 59*time.Second || remaining > 61*time.Second { + t.Fatalf("expected ~60s deadline, got %v", remaining) + } + return "ok", nil + } + + resp, err := interceptor(ctx, nil, info, handler) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp != "ok" { + t.Fatalf("expected 'ok', got %v", resp) + } +} + +func TestDefaultTimeoutInterceptor_ExistingDeadline(t *testing.T) { + defer resetGlobals() + + interceptor := DefaultTimeoutInterceptor() + info := &grpc.UnaryServerInfo{FullMethod: "/test.Service/Method"} + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + handler := func(ctx context.Context, req any) (any, error) { + deadline, ok := ctx.Deadline() + if !ok { + t.Fatal("expected deadline to be set") + } + remaining := time.Until(deadline) + if remaining > 6*time.Second { + t.Fatalf("deadline should be ~5s from caller, got %v", remaining) + } + return "ok", nil + } + + _, err := interceptor(ctx, nil, info, handler) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestDefaultTimeoutInterceptor_Disabled(t *testing.T) { + defer resetGlobals() + + SetDefaultTimeout(0) + interceptor := DefaultTimeoutInterceptor() + info := &grpc.UnaryServerInfo{FullMethod: "/test.Service/Method"} + ctx := context.Background() + + handler := func(ctx context.Context, req any) (any, error) { + if _, ok := ctx.Deadline(); ok { + t.Fatal("expected no deadline when timeout is disabled") + } + return "ok", nil + } + + _, err := interceptor(ctx, nil, info, handler) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestDefaultInterceptors_IncludesTimeout(t *testing.T) { + defer resetGlobals() + + // Call the DefaultTimeoutInterceptor directly from the chain to verify it's wired in. + // The first CB interceptor (index 0 when no user interceptors) should be the timeout. + ints := DefaultInterceptors() + if len(ints) == 0 { + t.Fatal("expected at least one interceptor") + } + + // The first interceptor should set a deadline on a bare context. + ctx := context.Background() + info := &grpc.UnaryServerInfo{FullMethod: "/test.Service/Method"} + deadlineSet := false + + handler := func(ctx context.Context, req any) (any, error) { + if _, ok := ctx.Deadline(); ok { + deadlineSet = true + } + return "ok", nil + } + + _, err := ints[0](ctx, nil, info, handler) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !deadlineSet { + t.Error("expected first CB interceptor (DefaultTimeoutInterceptor) to set a deadline") + } +} From cc305ad1f66edf9a4cc817cdc1b715e9241c52ab Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Wed, 8 Apr 2026 15:03:56 +0800 Subject: [PATCH 2/3] chore: bump Go to 1.25.9 to fix stdlib CVEs --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 02fa4fe..14f0d89 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/go-coldbrew/interceptors -go 1.25.8 +go 1.25.9 require ( buf.build/go/protovalidate v1.1.3 From 0b5965c62723488be1c8e5b8b12935f5d9803118 Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Wed, 8 Apr 2026 15:18:21 +0800 Subject: [PATCH 3/3] fix: update doc comments to match <= 0 disables behavior --- README.md | 4 ++-- interceptors.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4332fd6..df3668b 100644 --- a/README.md +++ b/README.md @@ -191,7 +191,7 @@ DefaultStreamInterceptors are the set of default interceptors that should be app func DefaultTimeoutInterceptor() grpc.UnaryServerInterceptor ``` -DefaultTimeoutInterceptor returns a unary server interceptor that applies a default deadline to incoming requests that have no deadline set. If the incoming context already has a deadline \(regardless of duration\), it is left unchanged. When defaultTimeout is 0, the interceptor is a no\-op pass\-through. +DefaultTimeoutInterceptor returns a unary server interceptor that applies a default deadline to incoming requests that have no deadline set. If the incoming context already has a deadline \(regardless of duration\), it is left unchanged. When defaultTimeout is \<= 0, the interceptor is a no\-op pass\-through. ## func [DoHTTPtoGRPC]() @@ -365,7 +365,7 @@ SetClientMetricsOptions appends gRPC client metrics options. Must be called duri func SetDefaultTimeout(d time.Duration) ``` -SetDefaultTimeout sets the default timeout applied to incoming unary RPCs that arrive without a deadline. When set to 0, the timeout interceptor is disabled \(pass\-through\). Default is 60s. Must be called during initialization, before the server starts. Not safe for concurrent use. +SetDefaultTimeout sets the default timeout applied to incoming unary RPCs that arrive without a deadline. When set to \<= 0, the timeout interceptor is disabled \(pass\-through\). Default is 60s. Must be called during initialization, before the server starts. Not safe for concurrent use. ## func [SetDisableProtoValidate]() diff --git a/interceptors.go b/interceptors.go index 5add7db..a6b3da1 100644 --- a/interceptors.go +++ b/interceptors.go @@ -85,7 +85,7 @@ func SetResponseTimeLogErrorOnly(errorOnly bool) { } // SetDefaultTimeout sets the default timeout applied to incoming unary RPCs -// that arrive without a deadline. When set to 0, the timeout interceptor is +// that arrive without a deadline. When set to <= 0, the timeout interceptor is // disabled (pass-through). Default is 60s. // Must be called during initialization, before the server starts. Not safe for concurrent use. func SetDefaultTimeout(d time.Duration) { @@ -528,7 +528,7 @@ func DebugLoggingInterceptor() grpc.UnaryServerInterceptor { // DefaultTimeoutInterceptor returns a unary server interceptor that applies a // default deadline to incoming requests that have no deadline set. If the // incoming context already has a deadline (regardless of duration), it is left -// unchanged. When defaultTimeout is 0, the interceptor is a no-op pass-through. +// unchanged. When defaultTimeout is <= 0, the interceptor is a no-op pass-through. func DefaultTimeoutInterceptor() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if defaultTimeout <= 0 {