Documentation
¶
Overview ¶
Package timeout gives each request a deadline and answers 503 request_timeout when the handler hasn't started its response by then (ADR-0085):
handler := httpx.Chain(mux, httpx.Recover(logger), httpx.RequestID(), timeout.New(30*time.Second), )
gorbital.New installs it as the Timeout step of the default middleware stack, configured by APP_REQUEST_TIMEOUT, and gorbital.Timeout shortens it for one route.
It is a package of its own, not part of httpx, because apps generated by orb v0.1 record every problem code of the gorbital.dev packages they link in api/surface.json: a new code in httpx would fail their tests (docs/guides/stability.md).
Stability: experimental until v0.2.0 (ADR-0015, ADR-0081).
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func New ¶
func New(d time.Duration) httpx.Middleware
New returns middleware that gives each request a context deadline d from now. When the deadline passes before the handler has started its response, the client gets a 503 problem with code "request_timeout", and whatever the handler writes afterwards is discarded. Late writes report success, so frameworks that treat a failed write as a bug (Huma panics) don't turn every timeout into a logged panic; the handler learns of the timeout from its cancelled context. Flushing, hijacking and deadline changes after a timeout return http.ErrHandlerTimeout. A d of zero or less turns it off.
Once the handler has started its response (written the status or body, flushed, or hijacked the connection), the deadline only cancels the context: nothing is buffered, so streaming and http.ResponseController work as without the middleware. Streams meant to outlive d, such as server-sent events, belong on routes without this middleware.
The handler runs on the request's goroutine, so the request ends when the handler returns: pass r.Context() to everything that waits, so it stops at the deadline. The 503 is written and flushed at the deadline with a Content-Length, so an HTTP/1.1 client has the whole response even while a handler that ignores its context keeps the connection. Install it after httpx.Recover and httpx.RequestID.
Example ¶
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"time"
"gorbital.dev/httpx"
"gorbital.dev/httpx/timeout"
)
func main() {
slowReport := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(time.Second): // a query that takes too long
_, _ = w.Write([]byte("report"))
case <-r.Context().Done():
// The query stops with the request's context.
}
})
h := httpx.Chain(slowReport, httpx.RequestID(), timeout.New(10*time.Millisecond))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/reports/1", nil))
fmt.Println(rec.Code, problemCode(rec.Body.Bytes()))
}
// problemCode returns the code of a problem response body.
func problemCode(body []byte) string {
var p httpx.Problem
_ = json.Unmarshal(body, &p)
return p.Code
}
Output: 503 request_timeout
Example (Deadline) ¶
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
"gorbital.dev/httpx/timeout"
)
func main() {
// Handlers read the deadline from the request's context and pass the
// context on, so queries stop in time.
h := timeout.New(30 * time.Second)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
deadline, ok := r.Context().Deadline()
fmt.Println(ok, time.Until(deadline) > 29*time.Second)
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) // a shorter step inside
defer cancel()
_ = ctx
}))
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
}
Output: true true
Types ¶
This section is empty.