AnnouncementsIntroducing Arkoren

    Menu

    Middleware

    Scope and order request policies, write wrappers, and preserve lifecycle behavior across native HTTP adapters.

    What's on this page
    1. Apply middleware at router, group, or route scope with an explicit execution order.
    2. Write request-local policies and register response hooks before commitment.
    3. Preserve context and understand where native middleware renders returned errors.
    On this page

    Middleware wraps a handler to apply a policy to several requests: it can reject input, establish request state, or observe the result. Choose both its scope and its position. A wrapper sees only the handlers and middleware inside it, so an incorrect order can hide failures or leave work unprotected.

    These examples use router from arkoren.dev/core/http/router and a fresh router routes. Call Use before registering routes through that router or its derived routers; a later call panics. Configuration alternatives below are independent, not calls to append after earlier route examples.

    Choose Scope

    For behavior shared by every route, attach middleware to the router. Import arkoren.dev/core/http/middleware/secureheaders:

    routes.Use(router.Recover(), secureheaders.Middleware())

    Recovery protects the layers inside it; secure headers apply to the requests that reach that middleware. At router scope, unmatched 404 and 405 requests also traverse the stack.

    For a billing-only policy, group existing listInvoices and createInvoice handlers under requireBilling, your router.Middleware:

    routes.Group("/billing", func(billing *router.Router) {
    	billing.Use(requireBilling)
    	billing.Get("/invoices", listInvoices)
    	billing.Post("/invoices", createInvoice)
    })

    The group inherits parent middleware without adding its policy to unrelated routes. To protect one registration, use With instead:

    routes.With(requireBilling).Post("/invoices", createInvoice)

    With leaves the parent’s stack unchanged. All handler and middleware captures remain shared across concurrent requests; scoping a stack does not copy captured services or mutable state.

    Order Policies

    Middleware runs in the order passed to Use: the first wrapper sees the request first and the returned result last. For example, with existing []errormap.Mapping named mappings, import errormap, requestlog, bodylimit, and contenttype from arkoren.dev/core/http/middleware/:

    routes.Use(
    	errormap.Middleware(mappings...),
    	requestlog.Middleware(),
    	router.Recover(),
    	secureheaders.Middleware(),
    	bodylimit.Middleware(),
    	contenttype.Middleware(),
    )

    The mapper sees errors from all inner layers. Logging sees recovered errors; recovery sees panics from the policy middleware and handler. Body limits precede anything that reads the body. An outer logger in this arrangement sees the original error before the mapper replaces it; choose observation placement deliberately if you need mapped errors instead.

    Place request tracing outside logging when logs need its trace context. Place CORS before authentication or body policies when preflights should terminate before those checks. A policy outside security headers can reject a request before the headers run, so choose that boundary consciously.

    Write Policies

    A router.Middleware takes a handler and returns a handler. Construction runs when the chain is built, not per request. Keep request-local values inside the returned function. This example uses net/http, arkoren.dev/core/http/request, and arkoren.dev/foundation/problem:

    func requireWorkspace(next router.Handler) router.Handler {
    	return func(w http.ResponseWriter, r *http.Request) error {
    		if request.Header(r, "X-Workspace") == "" {
    			return problem.BadRequest().Derive(func(details *problem.Builder) {
    				details.Detail("A workspace header is required.")
    			})
    		}
    		return next(w, r)
    	}
    }

    Register it before the routes it protects. Missing input returns a 400 problem without calling the handler; otherwise the handler’s error passes outward. This checks presence only. Validate the workspace and enforce authorization before using it as a trusted tenant.

    A wrapper can inspect or replace the error from next. It cannot replace an already committed response by returning a different error. For shared domain translation, prefer error mapping.

    Use Response Hooks

    Header edits made after next can be too late. To add timing at the last safe moment, register a hook before calling the handler. Inside a middleware’s returned function, import fmt and time alongside the request package:

    started := time.Now()
    request.Hooks(r).BeforeWriteHeader(func(w http.ResponseWriter, _ int) {
    	w.Header().Set("Server-Timing",
    		fmt.Sprintf("app;dur=%d", time.Since(started).Milliseconds()))
    })
    
    return next(w, r)

    BeforeWriteHeader runs once before the final status, not for informational 1xx except 101. A body write or flush can commit an implicit 200. BeforeWrite observes an attempted body write, which may still fail afterward. Do not record raw body contents merely to measure writes.

    For work that observes handling completion, register an after-response callback. With an existing application metrics function recordOutcome:

    request.Hooks(r).AfterResponse(func(err error) {
    	recordOutcome(err)
    })

    Callbacks run in reverse registration order when router handling ends, including panic unwinding. Their argument is the handler’s returned error, not a delivery acknowledgment; an unrecovered panic can leave it nil. A hijacked connection can outlive these callbacks and remains the handler’s responsibility.

    request.Hooks requires the context installed by router.Handler.ServeHTTP, including normal routed requests. Outside that lifecycle it panics with request.ErrNoHooksMiddleware; there is no non-panicking public lookup. Only register hooks where wiring guarantees that lifecycle.

    Preserve Context

    Middleware can resolve application state with router.Provide. Here workspaces.FromHost validates a hostname against trusted configuration and withWorkspace derives a context containing that result. Import context:

    routes.Use(router.Provide(func(w http.ResponseWriter, r *http.Request) (context.Context, error) {
    	workspace, err := workspaces.FromHost(r.Context(), r.Host)
    	if err != nil {
    		return nil, err
    	}
    	return withWorkspace(r.Context(), workspace), nil
    }))

    An error stops the chain. Derive successful contexts from r.Context(), not Background or TODO; replacing the parent loses cancellation, lifecycle hooks, and upstream adapter state. The same rule applies to custom middleware calling next with a changed request. Static values can use ProvideStatic, but mutable captured values still need concurrency-safe ownership.

    Native Middleware

    For existing func(http.Handler) http.Handler middleware named compress, use the adapter:

    routes.Use(router.HTTPMiddleware(compress))

    Creating the adapter does not call compress. Its native constructor runs once per wrapping, normally during registration, not once per request or once globally. A single Methods or Any registration shares a chain across its methods; separate registrations build separate chains. The unmatched-request fallback builds one additional chain on first use and reuses it. Native handlers and captures must therefore support concurrent use.

    Returned errors are rendered inside native middleware while output remains uncommitted, so it can observe the error status and body. The error also returns outward for observation. To map that HTTP response, put the mapper inside the adapter:

    routes.Use(
    	router.HTTPMiddleware(compress),
    	errormap.Middleware(mappings...),
    )

    An outer mapper still receives the returned error but cannot rewrite the adapter’s committed output. An inner mapper sees only the layers it wraps. A recovered error rendered before earlier output is not treated as a post-commit panic; an actual panic after earlier output still follows the abort rules.

    The adapter tracks writes through next and commitment at enclosing native boundaries, including wrappers hiding Unwrap. Output in a native wrapper’s private buffer is not visible until forwarded through a tracked boundary. Use the supplied writer for normal output; unwrapping it to write bypasses tracking and hooks. Use http.ResponseController for capability operations, and preserve the incoming context when forwarding requests.

    Individual policies belong with their capabilities: Body Limits, Content Types, secure headers, rate limiting, and authentication explain their setup and boundaries. For composition and adapters, see the router and request API references.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive