timeout

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 19, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

Timeout

Go Reference Go Version License

Stop requests that run too long. The middleware sets a deadline on the request; if the handler exceeds it, the context is canceled and the client gets a timeout response. Helps avoid stuck handlers using resources forever.

Full docs: Middleware Guide and Middleware Reference.

Features

  • Set a max duration per request (default 30s)
  • Request context is canceled when the timeout is reached
  • Returns 408 Request Timeout by default; custom handler supported
  • Skip timeout for specific paths or prefixes (e.g. streaming, webhooks)
  • Optional logging when a timeout happens
  • Handlers can check context and return early

Installation

go get rivaas.dev/middleware/timeout

Requires Go 1.25 or later.

Quick Start

package main

import (
    "net/http"
    "time"
    "rivaas.dev/router"
    "rivaas.dev/middleware/timeout"
)

func main() {
    r := router.New()
    r.Use(timeout.New(timeout.WithDuration(10 * time.Second)))

    r.GET("/", func(c *router.Context) {
        c.String(http.StatusOK, "Hello")
    })

    http.ListenAndServe(":8080", r)
}

Configuration

Option What it does
WithDuration Max time for the request (default: 30s)
WithHandler Custom response when timeout happens (default: 408)
WithSkipPaths Exact paths to exclude from timeout
WithSkipPrefix Path prefixes to exclude (e.g. /stream)
WithSkipSuffix Path suffixes to exclude
WithSkip Custom function to skip timeout for a request
WithoutLogging Do not log timeout events

Skip timeout for some paths:

r.Use(timeout.New(
    timeout.WithDuration(30*time.Second),
    timeout.WithSkipPaths("/webhook", "/health"),
    timeout.WithSkipPrefix("/stream"),
))

Handlers should respect context cancellation:

func handler(c *router.Context) {
    ctx := c.Request.Context()
    select {
    case <-ctx.Done():
        return // Timeout or canceled
    case result := <-doWork(ctx):
        c.JSON(http.StatusOK, result)
    }
}

Examples

A runnable example is in the example/ directory:

cd example
go run main.go

Learn More

License

Apache License 2.0 – see LICENSE for details.

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

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

func WithDuration(d time.Duration) Option

WithDuration sets the timeout duration. Default: 30 seconds

Example:

timeout.New(timeout.WithDuration(5 * time.Second))

func WithHandler

func WithHandler(handler func(c *router.Context, timeout time.Duration)) Option

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

func WithLogger(logger *slog.Logger) Option

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

func WithSkip(fn func(c *router.Context) bool) Option

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

func WithSkipPaths(paths ...string) Option

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

func WithSkipPrefix(prefixes ...string) Option

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

func WithSkipSuffix(suffixes ...string) Option

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())

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL