Documentation
¶
Overview ¶
Package restmid provides application-layer middleware: rest.MidFunc values that wrap a typed rest.HandlerFunc and operate on the ResponseEncoder it returns (the typed value), not on raw bytes.
It is the typed-layer twin of rest/mid, which holds net/http transport middleware (func(http.Handler) http.Handler). Apply these via router.New, router.UseApp, router.WithApp, or a per-route handle — never router.Use, which takes transport middleware. See the router package for the two-layer model and the Use/UseApp, With/WithApp, Handle/HandleApp method pairs.
Middleware ¶
- Chain(Config): the standard application chain in order (Metrics, LocalizeErrors, MaskInternal, Errors, Panics) — like the stories sdk/mid NewMiddlewares, but for our *errs.Error types. Wrap it with your own request-context (outside) and translation (inside) middleware.
- Panics(log): recover a panic from a typed handler into an Internal *errs.Error so it flows through the pipeline (logged, masked, localized, JSON-encoded) instead of a bare 500. Innermost of the chain.
- Errors(log): observe 5xx errors — record them on the active OpenTelemetry span and log them server-side by domain code. Observability only (masking is MaskInternal, localization is LocalizeErrors).
- Metrics(record): report each request's outcome code (errs code, or "ok") to a backend-agnostic callback — errs-aware metrics by domain code.
- LocalizeErrors(tr, lang): translate an *errs.Error response into the request language (resolved by the lang accessor) before it is encoded. Place it outermost of the app middleware so it wraps and localizes errors returned by deeper auth/validation middleware.
- MaskInternal(log, mask): when mask is set, log a 5xx *errs.Error server-side and replace it with a detail-free generic error so internal failures (DB messages, etc.) don't leak to clients. Place it inside LocalizeErrors.
- CacheControl(maxAge, vary...): set Cache-Control/Vary on successful responses. Attach per handler or group, the developer's choice.
- ETag(): add a strong ETag to successful responses and answer a matching If-None-Match with 304. Attach per handler or group.
CacheControl and ETag set response headers, which the typed ResponseEncoder cannot express, so they reach the ResponseWriter via rest.GetWriter (installed by the HandlerFunc.ServeHTTP boundary). They only set headers — never call WriteHeader — so Respond stays the single writer and they compose in any order. Off the boundary (a handler called directly) GetWriter is nil and they are no-ops.
Index ¶
- func CacheControl(maxAge int, varyHeaders ...string) rest.MidFunc
- func Chain(cfg Config) []rest.MidFunc
- func ETag() rest.MidFunc
- func Errors(log *logger.Logger) rest.MidFunc
- func LocalizeErrors(tr *i18n.Translator, lang func(context.Context) string) rest.MidFunc
- func MaskInternal(log *logger.Logger, mask bool) rest.MidFunc
- func Metrics(record func(code string)) rest.MidFunc
- func Panics(log *logger.Logger) rest.MidFunc
- type Config
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CacheControl ¶
CacheControl returns application middleware that sets the Cache-Control (and, when given, Vary) response header for successful responses. It is an app-developer choice attached per handler or group — handle("GET /x", h.get, restmid.CacheControl(60)) — not global infrastructure. maxAge is in seconds; a non-positive maxAge sets no Cache-Control. Error and 204 responses are left uncached.
It sets a header via rest.GetWriter but never calls WriteHeader, so Respond remains the single writer of status and body and the middleware composes in any order with others (e.g. ETag). With no ResponseWriter in context (a handler invoked directly, off the ServeHTTP boundary) it is a no-op.
func Chain ¶
Chain returns the standard skit application middleware in order, outermost first: Metrics -> LocalizeErrors -> MaskInternal -> Errors -> Panics. It mirrors the idea of a single, ordered chain (cf. the stories sdk/mid NewMiddlewares) but works with our *errs.Error types and the two-layer model. The order is deliberate: Panics (innermost) turns a handler panic into an Internal error; Errors logs/traces the original 5xx; MaskInternal then hides its detail from the client (it gets a nil logger here because Errors already logged); LocalizeErrors localizes the result; Metrics (outermost) counts the outcome.
It is the SDK core only. Wrap it with your app's own middleware: put request-context parsing OUTSIDE it (so the language is in the context before LocalizeErrors reads it) and per-record translation INSIDE it, e.g.
mids := append([]rest.MidFunc{reqctx.Middleware()}, restmid.Chain(cfg)...)
mids = append(mids, translationrest.MiddlewareWithLang(...))
r := router.New(mids...)
Per-handler middleware (CacheControl, ETag, auth) are attached at the route, not here.
func ETag ¶
ETag returns application middleware that adds an entity-tag to successful responses and serves a 304 Not Modified when the client's If-None-Match matches. It is an app-developer choice attached per handler or group — handle("GET /x", h.get, restmid.ETag()) — typically on cacheable reads.
The tag is a strong validator computed from the encoded body, so it needs the body: ETag encodes the response once and forwards the bytes (no re-encode by Respond). It sets the ETag header via rest.GetWriter but never calls WriteHeader. Error, nil (204), and writer-less (direct-call) responses pass through untouched.
func Errors ¶
Errors returns application middleware that observes server-fault responses: on an *errs.Error in the 5xx class it records the error on the active OpenTelemetry span (so the trace is marked failed and carries the error) and logs it server-side with the domain code, HTTP status, and detail.
It is the errs-aware adaptation of the stories sdk/mid Errors handler, narrowed to a single responsibility — observability. The stories version also masked Internal errors in production and pushed code-location attributes into the access log via httplog; here masking is MaskInternal's job (place Errors inside it so it logs the original detail before masking), and the code location is omitted because our errs keeps it unexported. A nil log records only the span. 4xx (client-actionable) and non-error responses pass through untouched — the access log already covers those.
func LocalizeErrors ¶
LocalizeErrors returns application middleware that translates an *errs.Error response into the request language before rest.Respond encodes it. The language is resolved per request by lang (e.g. a reqctx accessor), so this middleware stays decoupled from how the language got into the context.
Non-error responses pass through unchanged; a nil translator or lang accessor makes it a no-op. Place it outermost of the application middleware (e.g. first in router.New) so it wraps the deeper auth/validation middleware and localizes the errors they return.
func MaskInternal ¶
MaskInternal returns application middleware that keeps server-fault error details from leaking to clients. When the wrapped handler returns an *errs.Error in the 5xx class (HTTPStatus >= 500 — Internal, Unavailable, etc.) and mask is true (typically in production), it logs the original error server-side — so the detail survives for diagnosis — and replaces the response with a generic error that keeps the same code (hence status) but carries no detail. With mask false (e.g. in development) the original error passes through unchanged so you can read it. Non-5xx errors (4xx are client-actionable) and non-error responses always pass through; a nil log skips the server-side line.
Place it inside LocalizeErrors (router.New(reqctx, LocalizeErrors, MaskInternal, ...)) so the localizer still runs on the masked error.
func Metrics ¶
Metrics returns application middleware that reports the outcome code of each request to record: the *errs.Error code (e.g. "internal", "not_found") for an error response, or "ok" for a success. It is errs-aware (counts by domain code, not just HTTP status) and backend-agnostic — record wires it to whatever metric system the app uses (e.g. a Prometheus CounterVec labeled by code). A nil record is a no-op.
func Panics ¶
Panics returns application middleware that recovers a panic from a downstream typed handler (or inner middleware) and turns it into an *errs.Error of code Internal, so it flows through the normal error pipeline — Errors logs/traces it, MaskInternal hides it from clients in production, LocalizeErrors localizes it, and Respond encodes it as a proper JSON error instead of a bare empty 500.
It logs the panic value and stack itself (a nil log skips that) and returns a detail-free Internal error, so the stack never reaches the client even when used alone. Place it innermost of the application middleware (closest to the handler) so the recovered error propagates back out through the rest of the chain. It complements, not replaces, a transport-level panic recovery (e.g. httplog) that guards raw handlers and other transport middleware.
Types ¶
type Config ¶
type Config struct {
// Translator localizes *errs.Error responses; nil disables localization.
Translator *i18n.Translator
// Lang resolves the request language from the context (e.g. a reqctx
// accessor). Required for localization; nil makes LocalizeErrors a no-op.
Lang func(context.Context) string
// Logger receives the span/log of 5xx errors (via Errors) and recovered
// panics (via Panics); nil disables that logging.
Logger *logger.Logger
// MaskInternal hides 5xx error detail from clients (typically true in
// production). See MaskInternal.
MaskInternal bool
// RecordMetric, when set, receives each request's outcome code (an
// *errs.Error code, or "ok") for errs-aware metrics. See Metrics.
RecordMetric func(code string)
}
Config configures the standard application middleware chain built by Chain.