timeout

package
v0.3.4 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: Apache-2.0 Imports: 12 Imported by: 1

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

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.

Jump to

Keyboard shortcuts

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