Documentation
¶
Overview ¶
Package timeout provides middleware for enforcing request timeouts to prevent long-running requests from consuming server resources.
This middleware sets a deadline on the request context, causing handlers to be canceled if they exceed the configured timeout duration. This prevents slow or stuck handlers from consuming resources indefinitely.
Basic Usage ¶
import "rivaas.dev/middleware/timeout" r := router.MustNew() r.Use(timeout.New()) // Uses 30s default timeout
With Custom Duration ¶
r.Use(timeout.New(timeout.WithDuration(5 * time.Second)))
Configuration Options ¶
- Duration: Maximum duration for request processing (default: 30s)
- Logger: Custom slog.Logger for timeout events (default: slog.Default())
- Handler: Custom handler for timeout errors
- SkipPaths: Exact paths to exclude from timeout
- SkipPrefix: Path prefixes to exclude from timeout
- SkipSuffix: Path suffixes to exclude from timeout
- Skip: Custom function to determine if timeout should be skipped
Timeout Behavior ¶
When a timeout occurs:
- The request context is canceled
- A warning is logged (unless disabled with WithoutLogging())
- A 408 Request Timeout response is sent
- Handlers should check ctx.Done() and return early
Skip Paths ¶
// Skip exact paths
r.Use(timeout.New(
timeout.WithSkipPaths("/stream", "/webhook"),
))
// Skip by prefix (all /admin/* routes)
r.Use(timeout.New(
timeout.WithSkipPrefix("/admin", "/internal"),
))
// Skip by suffix (all streaming endpoints)
r.Use(timeout.New(
timeout.WithSkipSuffix("/stream", "/events"),
))
// Skip with custom logic
r.Use(timeout.New(
timeout.WithSkip(func(c *router.Context) bool {
return c.Request.Method == "OPTIONS"
}),
))
Custom Error Handler ¶
r.Use(timeout.New(
timeout.WithDuration(30 * time.Second),
timeout.WithHandler(func(c *router.Context, timeout time.Duration) {
c.JSON(http.StatusRequestTimeout, map[string]any{
"error": "Request timeout",
"timeout": timeout.String(),
})
}),
))
Disable Logging ¶
r.Use(timeout.New(timeout.WithoutLogging()))
Handler Implementation ¶
Handlers should respect context cancellation:
func handler(c *router.Context) {
ctx := c.Request.Context()
select {
case <-ctx.Done():
return // Timeout occurred
case result := <-longRunningOperation(ctx):
c.JSON(http.StatusOK, result)
}
}
Timeout enforcement uses context.WithTimeout, which is standard Go practice.
Package timeout provides middleware for setting request timeouts.
Index ¶
- func New(opts ...Option) router.HandlerFunc
- type Option
- func WithDuration(d time.Duration) Option
- func WithHandler(handler func(c *router.Context, timeout time.Duration)) Option
- func WithLogger(logger *slog.Logger) Option
- func WithSkip(fn func(c *router.Context) bool) Option
- func WithSkipPaths(paths ...string) Option
- func WithSkipPrefix(prefixes ...string) Option
- func WithSkipSuffix(suffixes ...string) Option
- func WithoutLogging() Option
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶
func New(opts ...Option) router.HandlerFunc
New returns a middleware that adds a timeout to requests. If a request takes longer than the specified duration, it will be canceled and an error response will be sent.
The middleware creates a new context with timeout and passes it to the handler. Handlers should respect context cancellation to properly handle timeouts.
Basic usage (uses 30s default):
r := router.MustNew() r.Use(timeout.New())
With custom duration:
r.Use(timeout.New(timeout.WithDuration(5 * time.Second)))
With custom error handler:
r.Use(timeout.New(
timeout.WithDuration(30 * time.Second),
timeout.WithHandler(func(c *router.Context, timeout time.Duration) {
c.JSON(http.StatusRequestTimeout, map[string]any{
"error": "Operation timed out",
"timeout": timeout.String(),
})
}),
))
Skip certain paths:
r.Use(timeout.New(
timeout.WithSkipPaths("/stream", "/events"),
timeout.WithSkipPrefix("/admin"),
timeout.WithSkipSuffix("/ws"),
))
Skip based on custom logic:
r.Use(timeout.New(
timeout.WithSkip(func(c *router.Context) bool {
return c.Request.Method == "OPTIONS"
}),
))
Disable logging:
r.Use(timeout.New(timeout.WithoutLogging()))
Respecting timeouts in handlers:
r.GET("/slow", func(c *router.Context) {
select {
case <-time.After(2 * time.Second):
c.JSON(http.StatusOK, map[string]string{"message": "Done"})
case <-c.Request.Context().Done():
// Request was canceled or timed out
return
}
})
Important notes:
- Handlers MUST check c.Request.Context().Done() for long operations
- Database queries should use context: db.QueryContext(c.Request.Context(), ...)
- HTTP calls should use context: req.WithContext(c.Request.Context())
- Timeouts don't interrupt running code, they cancel the context
- Goroutines spawned by handlers may continue after timeout until they complete or check context cancellation - this is a limitation of Go's timeout mechanism
Timeout checking is handled by Go's context package.
Goroutine behavior:
The timeout middleware spawns a goroutine to execute the handler chain. If a timeout occurs, the context is canceled but the goroutine continues until it naturally completes or checks c.Request.Context().Done(). This is expected behavior and handlers must be designed to respect context cancellation to avoid goroutine leaks and unnecessary work.
Panic handling:
Panics that occur within the handler goroutine are caught and re-thrown in the main goroutine. This ensures the recovery middleware (which runs in the main goroutine) can properly catch and handle panics.
Types ¶
type Option ¶
type Option func(*config)
Option defines functional options for timeout middleware configuration.
func WithDuration ¶
WithDuration sets the timeout duration. Default: 30 seconds
Example:
timeout.New(timeout.WithDuration(5 * time.Second))
func WithHandler ¶
WithHandler sets a custom handler for timeout errors. This handler is called when a request exceeds the timeout duration. The handler receives the configured timeout duration.
Example:
timeout.New(
timeout.WithHandler(func(c *router.Context, timeout time.Duration) {
c.JSON(http.StatusRequestTimeout, map[string]any{
"error": "Request took too long",
"timeout": timeout.String(),
"request_id": c.Response.Header().Get("X-Request-ID"),
})
}),
)
func WithLogger ¶
WithLogger sets a custom slog.Logger for timeout logging.
Example:
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) timeout.New(timeout.WithLogger(logger))
func WithSkip ¶
WithSkip sets a custom function to determine if timeout should be skipped. Return true to skip timeout for the request.
Example:
timeout.New(
timeout.WithSkip(func(c *router.Context) bool {
// Skip OPTIONS requests
if c.Request.Method == "OPTIONS" {
return true
}
// Skip if header present
return c.Request.Header.Get("X-No-Timeout") != ""
}),
)
func WithSkipPaths ¶
WithSkipPaths sets exact paths that should not have timeout applied. Useful for long-running endpoints like streaming or webhooks.
Example:
timeout.New(timeout.WithSkipPaths("/stream", "/webhook"))
func WithSkipPrefix ¶
WithSkipPrefix skips paths that start with any of the given prefixes. Useful for skipping entire route groups.
Example:
timeout.New(timeout.WithSkipPrefix("/admin", "/internal"))
func WithSkipSuffix ¶
WithSkipSuffix skips paths that end with any of the given suffixes. Useful for skipping specific endpoint types.
Example:
timeout.New(timeout.WithSkipSuffix("/stream", "/events"))
func WithoutLogging ¶
func WithoutLogging() Option
WithoutLogging disables timeout logging. By default, timeouts are logged using slog.Default().
Example:
timeout.New(timeout.WithoutLogging())