Documentation
¶
Overview ¶
Package http2 provides an opinionated HTTP/2 server wrapper that implements lifecycle.Runner. It ships with built-in /livez, /readyz, and /metrics endpoints, a slog-aware recovery middleware, an opt-in otelhttp instrumentation hook, and a context-safe shutdown contract.
Typical usage:
cfg := http2.Config{Port: "8080"}
srv := http2.NewServer(cfg,
http2.WithOtel(),
http2.WithRecovery(),
http2.WithHealthcheckFrom(app),
)
srv.Mount("/api", apiHandler)
a := app.New()
a.Add(srv)
log.Fatal(a.Run())
Index ¶
- Constants
- Variables
- func NewClient(opts ...ClientOption) *http.Client
- func NewErrorBodyRoundTripper(next http.RoundTripper) http.RoundTripper
- type ClientOption
- func WithClientOtel(opts ...otelhttp.Option) ClientOption
- func WithDialTimeout(d time.Duration) ClientOption
- func WithIdleConnTimeout(d time.Duration) ClientOption
- func WithInnerRoundTripper(wrap func(http.RoundTripper) http.RoundTripper) ClientOption
- func WithKeepAlive(d time.Duration) ClientOption
- func WithMaxIdleConns(n int) ClientOption
- func WithMaxIdleConnsPerHost(n int) ClientOption
- func WithResponseHeaderTimeout(d time.Duration) ClientOption
- func WithRoundTripper(wrap func(http.RoundTripper) http.RoundTripper) ClientOption
- func WithTLSConfig(cfg *tls.Config) ClientOption
- func WithTLSHandshakeTimeout(d time.Duration) ClientOption
- func WithTransportOptions(fns ...func(*http.Transport)) ClientOption
- type Config
- type Option
- func WithHealthcheckFrom(h lifecycle.Healthchecker) Option
- func WithListener(l net.Listener) Option
- func WithLogger(l *slog.Logger) Option
- func WithMetricsHandler(h http.Handler) Option
- func WithMiddleware(mw ...func(http.Handler) http.Handler) Option
- func WithOtel(opts ...otelhttp.Option) Option
- func WithReadyzCheck(name string, fn func(ctx context.Context) error) Option
- func WithRecovery() Option
- type Server
Constants ¶
const ( // LivenessPath returns 200 OK unconditionally — a signal that the // process is up and the event loop is scheduling. Does NOT check // dependencies; that is /readyz's job. LivenessPath = "/livez" // ReadinessPath returns 200 OK if every Healthchecker wired via // WithHealthcheckFrom / WithReadyzCheck reports healthy, or 503 with // the error body otherwise. ReadinessPath = "/readyz" // MetricsPath serves the handler passed via WithMetricsHandler, or 404 // if no handler was set. Typical use: pass the promhttp handler that // scrapes the otel PrometheusExporter. MetricsPath = "/metrics" )
Built-in route paths served by every Server.
Variables ¶
var ErrServerAlreadyStarted = errors.New("http2: server already started")
ErrServerAlreadyStarted is returned by Server.Run if the server has already been started. A Server instance is single-use: the underlying http.Server cannot be reused after Shutdown.
Functions ¶
func NewClient ¶ added in v1.10.0
func NewClient(opts ...ClientOption) *http.Client
NewClient builds an *http.Client tuned for outbound service-to-service and third-party API calls: a fresh *http.Transport with connection pool and timeout fields set to sane defaults, optional TLS configuration and RoundTripper wrapping for auth/headers, and optional otel instrumentation (see WithClientOtel).
http.Client.Timeout is intentionally left unset — callers are expected to bound calls with context.WithTimeout/WithDeadline rather than a client-wide wall clock, so that deadlines propagate correctly through retries and don't clip a single slow-but-valid call inside a longer request chain.
func NewErrorBodyRoundTripper ¶ added in v1.11.0
func NewErrorBodyRoundTripper(next http.RoundTripper) http.RoundTripper
NewErrorBodyRoundTripper wraps next with baseline error visibility for an otelhttp-instrumented client: a RecordError event on transport-level failures (dial/TLS/timeout), and a capped, UTF-8-safe body snippet attribute on HTTP-level errors (status >= 400). It is meant to be passed to WithInnerRoundTripper, NOT WithRoundTripper — see that option's doc comment for why placement matters here: this wrapper must run WHILE the otelhttp client span is still open, which only holds for the inner (pre-otel) position.
Deliberately does NOT call span.SetStatus: otelhttp's own client semconv already marks any status >= 400 as codes.Error, and that call runs AFTER this RoundTripper returns control to otelhttp — a SetStatus call made here would just have its description blanked by otelhttp's later call (the SDK only refuses to downgrade FROM codes.Error, not same-code overwrites). So today every >=400 client span already shows codes.Error with an empty description; this wrapper's RecordError/body-snippet are what carry the actual diagnostic content, without duplicating (and losing) the status itself. Closing the empty-description gap would need a second, OUTER wrapper (WithRoundTripper) running after otelhttp's SetStatus — but that placement can't see transport-level errors at all (otelhttp has already called span.End() by the time an outer wrapper regains control on that path), so it is not implemented here.
Domain-level error classification (which upstream, which error class) is deliberately NOT this RoundTripper's job — it has no notion of "provider" or "model", only raw HTTP. Callers that need that (e.g. a per-provider chat-completion client) should keep building their own richer span around the call and classify the returned error there; this wrapper is the baseline every call gets for free, not a replacement for that.
Types ¶
type ClientOption ¶ added in v1.10.0
type ClientOption func(*clientOptions)
ClientOption configures NewClient.
func WithClientOtel ¶ added in v1.10.0
func WithClientOtel(opts ...otelhttp.Option) ClientOption
WithClientOtel enables OpenTelemetry instrumentation of the client's transport via otelhttp.NewTransport, using the global TracerProvider and MeterProvider — otel.Setup must have been called and registered with app.App before NewClient so the providers are non-noop. Named distinctly from NewServer's WithOtel only because both Option types live in this package and Go disallows overloading; the convention is otherwise identical to WithOtel and every other core/* package's opt-in WithOtel: disabled unless passed.
otelhttp's default instrumentation records duration and body-size histograms tagged with the full default attribute set. Pass otelhttp.WithMetricAttributesFn(...) via opts to trim cardinality; see the same concern documented on NewServer's WithOtel.
func WithDialTimeout ¶ added in v1.10.0
func WithDialTimeout(d time.Duration) ClientOption
WithDialTimeout overrides the dialer's connect timeout. Default: 5s.
func WithIdleConnTimeout ¶ added in v1.10.0
func WithIdleConnTimeout(d time.Duration) ClientOption
WithIdleConnTimeout overrides how long an idle connection is kept in the pool before being closed. Default: 90s.
func WithInnerRoundTripper ¶ added in v1.11.0
func WithInnerRoundTripper(wrap func(http.RoundTripper) http.RoundTripper) ClientOption
WithInnerRoundTripper appends a RoundTripper-wrapping function to the chain, like WithRoundTripper, but placed on the OPPOSITE side of otel instrumentation: between the raw *http.Transport and otelhttp.NewTransport, rather than above it. wrap's "next" is the raw transport (or the next inner wrapper), never the otelhttp.Transport.
This exists for exactly one class of concern: annotating the otelhttp client span from inside its own lifetime. otelhttp creates that span before calling into the RoundTripper chain and, on a transport-level error, ends it synchronously before returning control to any WithRoundTripper (outer) wrapper — so an outer wrapper sees an already-ended span on that path, and RecordError/SetAttributes on an ended span are silently dropped by the SDK. A wrapper placed here instead runs WHILE that span is still open (the span is already bound to the request's context.Context, retrievable via trace.SpanFromContext) and can safely enrich it before returning. See NewErrorBodyRoundTripper for the intended consumer.
Ordering among multiple WithInnerRoundTripper calls mirrors WithRoundTripper: the first-passed wrapper is outermost within this inner group (closest to otel, furthest from the wire).
A no-op if WithClientOtel is never passed: without otel, this chain still wraps the raw transport, but there is no span in context for the wrapper to find (trace.SpanFromContext returns a no-op span, safe to call).
func WithKeepAlive ¶ added in v1.10.0
func WithKeepAlive(d time.Duration) ClientOption
WithKeepAlive overrides the dialer's TCP keep-alive interval. Default: 30s.
func WithMaxIdleConns ¶ added in v1.10.0
func WithMaxIdleConns(n int) ClientOption
WithMaxIdleConns overrides the transport's global idle connection cap. Default: 100.
func WithMaxIdleConnsPerHost ¶ added in v1.10.0
func WithMaxIdleConnsPerHost(n int) ClientOption
WithMaxIdleConnsPerHost overrides the transport's per-host idle connection cap. Default: 20.
func WithResponseHeaderTimeout ¶ added in v1.10.0
func WithResponseHeaderTimeout(d time.Duration) ClientOption
WithResponseHeaderTimeout overrides how long the transport waits for response headers after writing the request (including its body). Default: 10s. Raise this for slow upstreams (e.g. LLM APIs with high time-to-first-byte); the overall call deadline should still come from context, not this field.
func WithRoundTripper ¶ added in v1.10.0
func WithRoundTripper(wrap func(http.RoundTripper) http.RoundTripper) ClientOption
WithRoundTripper appends a RoundTripper-wrapping function to the chain, for cross-cutting concerns that belong above the transport: injecting an Authorization header or API key, HMAC-signing requests, rate limiting, custom retry logic. wrap receives the next RoundTripper in the chain (closer to the wire) and must return the RoundTripper to use in its place.
Chain order mirrors NewServer's WithMiddleware: the first wrapper passed is outermost (sees the request first). otel instrumentation (WithClientOtel) is always innermost, closest to the transport, so a span covers only the actual network round trip — not time spent in these wrappers (e.g. an OAuth2 token refresh should get its own span via its own instrumented client, not inflate this request's span).
func WithTLSConfig ¶ added in v1.10.0
func WithTLSConfig(cfg *tls.Config) ClientOption
WithTLSConfig sets the transport's TLS client configuration — client certificates for mTLS against a partner API, a custom root CA pool for a privately-issued server certificate, or (for internal/test environments only) InsecureSkipVerify. Default: nil, which uses Go's standard certificate verification against the system trust store.
func WithTLSHandshakeTimeout ¶ added in v1.10.0
func WithTLSHandshakeTimeout(d time.Duration) ClientOption
WithTLSHandshakeTimeout overrides the transport's TLS handshake timeout. Default: 5s.
func WithTransportOptions ¶ added in v1.10.0
func WithTransportOptions(fns ...func(*http.Transport)) ClientOption
WithTransportOptions applies arbitrary mutator functions to the built *http.Transport, in the order given, after every named option (WithMaxIdleConns, WithTLSConfig, etc.) has already been applied. Escape hatch for the long tail of *http.Transport fields (MaxConnsPerHost, DisableCompression, DisableKeepAlives, ...) that don't warrant a dedicated named option.
Mutators run before otel instrumentation and RoundTripper wrapping, so they only ever see the raw *http.Transport, never the wrapped http.RoundTripper chain.
type Config ¶
type Config struct {
// Port is the TCP port the listener binds to. Default: "80".
Port string
// ReadTimeout caps the time taken to read the full request including
// body. Default: 1 second. Keep small for non-upload endpoints.
ReadTimeout time.Duration
// WriteTimeout caps the time taken to write the full response body.
// For server-streaming endpoints this MUST be large — a 24h value
// is common for long-lived SSE / server-streaming RPCs. Default: 10s.
WriteTimeout time.Duration
// ShutdownTimeout caps the graceful shutdown phase. If in-flight
// requests do not complete within this window, the server is
// force-closed. Default: 10 seconds.
ShutdownTimeout time.Duration
}
Config holds the server's network and timeout settings. All fields are optional — zero values are replaced with defaults in NewServer.
type Option ¶ added in v1.3.0
type Option func(*Server)
Option configures a new Server.
func WithHealthcheckFrom ¶ added in v1.3.0
func WithHealthcheckFrom(h lifecycle.Healthchecker) Option
WithHealthcheckFrom wires a lifecycle.Healthchecker (typically an *app.App) into the /readyz handler. On every /readyz request, the passed Healthchecker's Healthcheck method is invoked and its result aggregated with any WithReadyzCheck-registered checks.
If no healthchecker is set, /readyz returns 200 OK unconditionally (same as /livez).
func WithListener ¶ added in v1.3.0
WithListener overrides the default TCP listener. Useful for tests (net.Listen on :0) or for exposing the server on a unix socket.
func WithLogger ¶ added in v1.3.0
WithLogger attaches a *slog.Logger to the server. Used by the recovery middleware and for lifecycle events. Defaults to slog.Default() when omitted.
func WithMetricsHandler ¶ added in v1.3.0
WithMetricsHandler registers an http.Handler at MetricsPath. Typical use: pass the promhttp.Handler wrapping an otel Prometheus exporter. Kept as a generic Handler parameter so core/http2 does not depend on prometheus or otel/exporters/prometheus directly.
Without this option, /metrics returns 404.
func WithMiddleware ¶ added in v1.3.0
WithMiddleware appends one or more middleware functions to the chain. The chain is applied outer-first: the first middleware passed is the outermost wrapper (sees the request first, writes the response last).
Built-in middlewares (WithOtel, WithRecovery) are applied AFTER user middlewares so that panics from inside user middleware are still caught and traces span user middleware work.
func WithOtel ¶ added in v1.3.0
WithOtel enables OpenTelemetry instrumentation by wrapping the final handler with otelhttp.NewHandler. Uses the global TracerProvider and MeterProvider, so otel.Setup must have been called and registered with app.App before NewServer so the providers are non-noop.
WithOtel records one span per request with the route pattern matched by the underlying ServeMux, including HTTP method, status code, and latency.
otelhttp's default instrumentation records http.server.request.duration, http.server.request.body.size, and http.server.response.body.size, each tagged with the full default attribute set (route, method, status_code, network/scheme/server attributes). Combined with explicit histogram bucket boundaries (see core/otel's WithHistogramBuckets), this can produce significant active-series cardinality on quota-limited backends. Pass otelhttp.WithMetricAttributesFn(...) via opts to trim or rename attributes per-server instead of working around it at the collector.
func WithReadyzCheck ¶ added in v1.3.0
WithReadyzCheck adds a named inline readiness check. Useful for app-level gates that are not tied to a registered Resource/Runner (e.g., "warm cache loaded", "migration finished").
The check function receives the request's context and must respect its deadline. Returning a non-nil error fails the /readyz response.
func WithRecovery ¶ added in v1.3.0
func WithRecovery() Option
WithRecovery installs a panic-recovery middleware that logs the panic with a stack trace (via the configured slog.Logger) and responds with HTTP 500. Without this, a panic in a handler crashes the goroutine and terminates the connection without a response.
Recommended for every production-facing server.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is an HTTP/2-capable http.Server wrapped with middleware chain, built-in /livez, /readyz, /metrics routes, and a context-safe graceful shutdown. Implements lifecycle.Runner.
func NewServer ¶
NewServer constructs a Server with the given Config and options. Zero values in cfg are replaced with defaults (see defaultXxx constants).
The returned *Server implements lifecycle.Runner. Register it with app.App via a.Add(srv); do not call Run directly outside of tests.
func (*Server) Mount ¶
Mount registers handler at pattern on the underlying ServeMux. Patterns follow net/http.ServeMux rules (see https://pkg.go.dev/net/http#ServeMux).
Mount must be called before Run. Calling it concurrently with Run is undefined behavior.
func (*Server) Run ¶
Run starts the HTTP listener and blocks until ctx is cancelled or the server fails. On ctx cancellation, Run invokes Shutdown with a FRESH timeout context (NOT the already-cancelled ctx) so graceful drain has its full budget.
Implements lifecycle.Runner.
Run must be called at most once per Server instance — the underlying http.Server cannot be reused after Shutdown. A second call returns ErrServerAlreadyStarted.