From b865468c9147dfcf81f5abdd95705706a5da62ce Mon Sep 17 00:00:00 2001 From: Mausam Yadav Date: Sat, 4 Oct 2025 00:32:07 +0530 Subject: [PATCH 1/3] Enable timeout support for each service handler Signed-off-by: Mausam Yadav --- connect.go | 3 +++ handler.go | 11 +++++++++++ option.go | 21 +++++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/connect.go b/connect.go index 274a41ee..2bf3cede 100644 --- a/connect.go +++ b/connect.go @@ -30,6 +30,7 @@ import ( "io" "net/http" "net/url" + "time" ) // Version is the semantic version of the connect module. @@ -319,6 +320,8 @@ type Spec struct { Procedure string // for example, "/acme.foo.v1.FooService/Bar" IsClient bool // otherwise we're in a handler IdempotencyLevel IdempotencyLevel + ReadTimeout time.Duration + WriteTimeout time.Duration } // Peer describes the other party to an RPC. diff --git a/handler.go b/handler.go index 8f7967c0..2a7c6e06 100644 --- a/handler.go +++ b/handler.go @@ -17,6 +17,7 @@ package connect import ( "context" "net/http" + "time" ) // A Handler is the server-side implementation of a single RPC defined by a @@ -255,6 +256,12 @@ func NewBidiStreamHandler[Req, Res any]( // ServeHTTP implements [http.Handler]. func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { + if h.spec.ReadTimeout != 0 { + rc := http.NewResponseController(responseWriter) + rc.SetReadDeadline(time.Now().Add(h.spec.ReadTimeout)) + rc.SetWriteDeadline(time.Now().Add(h.spec.ReadTimeout)) + } + // We don't need to defer functions to close the request body or read to // EOF: the stream we construct later on already does that, and we only // return early when dealing with misbehaving clients. In those cases, it's @@ -350,6 +357,8 @@ type handlerConfig struct { ReadMaxBytes int SendMaxBytes int StreamType StreamType + ReadTimeout time.Duration + WriteTimeout time.Duration } func newHandlerConfig(procedure string, streamType StreamType, options []HandlerOption) *handlerConfig { @@ -376,6 +385,8 @@ func (c *handlerConfig) newSpec() Spec { Schema: c.Schema, StreamType: c.StreamType, IdempotencyLevel: c.IdempotencyLevel, + ReadTimeout: c.ReadTimeout, + WriteTimeout: c.WriteTimeout, } } diff --git a/option.go b/option.go index fe0a2cd9..0e4194a7 100644 --- a/option.go +++ b/option.go @@ -19,6 +19,7 @@ import ( "context" "io" "net/http" + "time" ) // A ClientOption configures a [Client]. @@ -351,6 +352,14 @@ func WithInterceptors(interceptors ...Interceptor) Option { return &interceptorsOption{interceptors} } +func WithReadTimeout(value time.Duration) HandlerOption { + return &readTimeoutOption{value: value} +} + +func WithWriteTimeout(value time.Duration) HandlerOption { + return &writeTimeoutOption{value: value} +} + // WithOptions composes multiple Options into one. func WithOptions(options ...Option) Option { return &optionsOption{options} @@ -645,3 +654,15 @@ func (o *conditionalHandlerOptions) applyToHandler(config *handlerConfig) { option.applyToHandler(config) } } + +type readTimeoutOption struct{ value time.Duration } + +func (o *readTimeoutOption) applyToHandler(config *handlerConfig) { + config.ReadTimeout = o.value +} + +type writeTimeoutOption struct{ value time.Duration } + +func (o *writeTimeoutOption) applyToHandler(config *handlerConfig) { + config.WriteTimeout = o.value +} From ab7fdf87f2cca6b2bc3fb4e027ab6e02fec6b3f0 Mon Sep 17 00:00:00 2001 From: R4R3D1FF <118979975+R4R3D1FF@users.noreply.github.com> Date: Tue, 7 Oct 2025 00:40:18 +0530 Subject: [PATCH 2/3] Fix ReadTimeout incorrectly used in Write Deadline Co-authored-by: Edward McFarlane <3036610+emcfarlane@users.noreply.github.com> Signed-off-by: R4R3D1FF <118979975+R4R3D1FF@users.noreply.github.com> --- handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler.go b/handler.go index 2a7c6e06..f33fc9ba 100644 --- a/handler.go +++ b/handler.go @@ -259,7 +259,7 @@ func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Re if h.spec.ReadTimeout != 0 { rc := http.NewResponseController(responseWriter) rc.SetReadDeadline(time.Now().Add(h.spec.ReadTimeout)) - rc.SetWriteDeadline(time.Now().Add(h.spec.ReadTimeout)) + rc.SetWriteDeadline(time.Now().Add(h.spec.WriteTimeout)) } // We don't need to defer functions to close the request body or read to From cc784c64509d2464cba115c0b9b536de1f3533ac Mon Sep 17 00:00:00 2001 From: Mausam Yadav Date: Sun, 7 Dec 2025 02:32:08 +0530 Subject: [PATCH 3/3] add comments Added documentation comments in the new exported options Signed-off-by: R4R3D1FF <118979975+R4R3D1FF@users.noreply.github.com> Signed-off-by: Mausam Yadav --- option.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/option.go b/option.go index 0e4194a7..d4831bc1 100644 --- a/option.go +++ b/option.go @@ -352,10 +352,30 @@ func WithInterceptors(interceptors ...Interceptor) Option { return &interceptorsOption{interceptors} } +// WithReadTimeout option specifies the maximum amount of time that a service +// handler is allowed to take when reading a message in a stream. +// If the total time exceeds WithReadTimeout, then that particular stream is +// closed. +// This enables the user to close only that particular stream instead of the +// entire connection. +// This prevents malicious or slow clients from using up resources. +// This option is passed to the handler config and then to the spec. +// Finally, ServeHTTP function of the handler reads the timeout values from +// the spec and enforces them using ResponseController. func WithReadTimeout(value time.Duration) HandlerOption { return &readTimeoutOption{value: value} } +// WithWriteTimeout option specifies the maximum amount of time that a service +// handler is allowed to take when writing a message to a stream. +// If the total time exceeds WithReadTimeout, then that particular stream is +// closed. +// This enables the user to close only that particular stream instead of the +// entire connection. +// This prevents malicious or slow clients from using up resources. +// This option is passed to the handler config and then to the spec. +// Finally, ServeHTTP function of the handler reads the timeout values from +// the spec and enforces them using ResponseController. func WithWriteTimeout(value time.Duration) HandlerOption { return &writeTimeoutOption{value: value} }