Documentation
¶
Overview ¶
Package timeout provides middleware that bounds the time a downstream handler chain may take before the client is answered. It is the express framework's Go analogue of Connect's connect-timeout middleware and the classic express-timeout wrappers: a drop-in express.Handler that runs the rest of the chain under a deadline and, if that deadline elapses first, answers the client with 503 Service Unavailable instead of leaving the request hanging.
Reach for this middleware when a slow or stuck handler must not be allowed to tie up a client indefinitely: routes that call flaky upstream services, database queries without their own timeout, report generation, or any place where a bounded worst-case latency is more valuable than always waiting for the "real" answer. Placing it near the front of the chain -- before the handlers you want to bound -- makes every handler downstream of it subject to the same Duration budget.
Operationally, on each request the middleware derives a context.WithTimeout from req.Raw.Context() using Options.Duration and installs it back onto the request, so a cooperative downstream handler that watches req.Raw.Context() can observe cancellation. It then swaps res.Writer for a handlerWriter that buffers headers in a private http.Header map and runs next() in a separate goroutine, while the middleware itself selects on the handler's completion versus the context deadline. If the handler finishes first, its buffered headers and body are flushed to the real writer and the request completes normally; if the deadline fires first, the middleware writes the 503.
Because two goroutines could otherwise both try to commit the response, a respGuard built on sync.Once elects exactly one winner: the handler claims id 1 on its first Write or WriteHeader, the timeout path claims id 2, and only the winner is allowed to touch the underlying writer. A handler that loses the race still runs to completion, but its writes are silently discarded (Write reports success without copying bytes) so a late response can never corrupt or duplicate the 503 already sent. On timeout the middleware also defaults the Content-Type to text/plain; charset=utf-8 when the handler had not already set one.
Two option fields tune the behavior: Duration (the deadline, defaulting to 30 seconds when <= 0) and Message (the 503 body, defaulting to "Service Unavailable"). Note the important semantic difference from a synchronous port: this middleware does not abort the handler goroutine -- Go cannot forcibly kill a goroutine -- so a handler ignoring context cancellation keeps running in the background until it returns, merely with its output suppressed. Callers must therefore ensure long-running work honors context cancellation to actually reclaim resources; the timeout guarantees a timely response to the client, not immediate termination of the work behind it.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶
New returns timeout middleware configured by opts.
Example ¶
ExampleNew demonstrates the timeout middleware converting a slow handler into a prompt 503 response. It mounts a timeout with a short 20ms Duration and a custom Message on an express.Application, then registers a "GET /" handler that deliberately sleeps well past the deadline before attempting to write. Driving the request through httptest, the deadline fires first, so the middleware wins the response race and sends 503 Service Unavailable with the configured message while the late handler's write is silently suppressed. The example sleeps briefly afterward to prove the background handler's late write never leaks into the already-sent response. Because the outcome here is governed by fixed, well-separated durations, the Output block is deterministic.
package main
import (
"fmt"
"net/http/httptest"
"time"
"github.com/malcolmston/express"
"github.com/malcolmston/express/middleware/timeout"
)
func main() {
app := express.New()
app.Use(timeout.New(timeout.Options{
Duration: 20 * time.Millisecond,
Message: "upstream too slow",
}))
app.Get("/", func(req *express.Request, res *express.Response, next express.Next) {
time.Sleep(80 * time.Millisecond)
res.Send("late answer")
})
r := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
// Let the losing handler finish; its write must not corrupt the response.
time.Sleep(90 * time.Millisecond)
fmt.Printf("%d %s\n", w.Code, w.Body.String())
}
Output: 503 upstream too slow