📖 6 min read (~ 1200 words).

Request pipeline

The middleware package wires an analyzed OpenAPI spec into a working http.Handler. Every request goes through the same conventional sequence of stages — covered briefly on core / interfaces, and expanded here with the actual call sites.

The full picture

flowchart TD
    req(((HTTP request)))
    router["Router · NewRouter / Context.RouteInfo<br/>match path/method against the analyzed spec<br/>404 / 405 if no route"]
    sec["Security · Context.Authorize<br/>RouteAuthenticators.Authenticate<br/>then optional Authorizer<br/>401 / 403 on failure"]
    bvr["BindValidRequest"]
    neg["ContentType / Accept negotiation<br/>pick Consumer + target Producer<br/>400 / 415 / 406 on failure"]
    bind["Binder<br/>path / query / header / body params<br/>— uses Consumer —"]
    val["Validator<br/>spec rules + Validatable<br/>422 with CompositeValidationError on failure"]
    op["OperationHandler.Handle<br/>your business logic"]
    resp["Responder · Context.Respond<br/>— uses Producer —"]
    out(((HTTP response)))

    req --> router --> sec --> bvr
    bvr --> neg --> bind --> val
    val --> op --> resp --> out

The middle three stages — negotiation, binding, validation — all live inside the single call Context.BindValidRequest. Splitting them out in the diagram makes the failure modes (400, 415, 406, 422) easier to trace.

The diagram shows the typical sequence — what the runtime’s default untyped wiring does and what go-swagger’s generated typed handlers do. The actual ordering and composition is an implementation detail of the RoutableAPI plugged into the middleware.Context; a custom one can compose the per-route handler differently.

The RoutableAPI seam

The middleware package handles routing, negotiation, validation and the high-level lifecycle helpers (RouteInfo, Authorize, BindValidRequest, Respond). Everything that has to know about your API — the per-operation handler, the registered codecs, the auth schemes — sits behind a single interface:

package middleware

type RoutableAPI interface {
    HandlerFor(method, path string) (http.Handler, bool)
    ServeErrorFor(path string) func(http.ResponseWriter, *http.Request, error)
    ConsumersFor(mediaTypes []string) map[string]runtime.Consumer
    ProducersFor(mediaTypes []string) map[string]runtime.Producer
    AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator
    Authorizer() runtime.Authorizer
    Formats() strfmt.Registry
    DefaultProduces() string
    DefaultConsumes() string
}
MethodThe runtime asks for…
HandlerForthe http.Handler that runs the per-operation pipeline for this route
ServeErrorForthe error-rendering function for a given path (defaults to the API’s)
ConsumersFora mediaType → Consumer map for the given list (route’s consumes)
ProducersFora mediaType → Producer map for the given list (route’s produces)
AuthenticatorsFora scheme name → Authenticator map for the security schemes in scope
Authorizerthe optional Authorizer to gate the principal post-authentication
Formatsthe strfmt.Registry used by validation
DefaultProduces / DefaultConsumesthe API-level defaults to fall back to when the route is unspecified

The router calls HandlerFor(method, path) once per matched route and serves whatever it gets back. What that handler does is entirely up to the implementation — the RoutableAPI decides how the bind/validate/security/operation/respond steps are composed.

Constructors that take a custom RoutableAPI

// Default — untyped.API wrapped in a routableUntypedAPI.
ctxDefault := middleware.NewContext(spec, api, nil)

// Custom — anything that implements RoutableAPI.
ctxCustom := middleware.NewRoutableContext(spec, myAPI, nil)

// Same, with a pre-analyzed spec to skip re-analysis.
ctxAnalyzed := middleware.NewRoutableContextWithAnalyzedSpec(spec, analyzed, myAPI, nil)

Full source: docs/examples/server/pipeline/main.go

Use NewRoutableContext when you have your own implementation (typically the one go-swagger generates for typed APIs, but any type satisfying the interface works). Reach for NewRoutableContextWithAnalyzedSpec if you have already produced an *analysis.Spec and want to avoid the second analysis pass.

Two implementations the runtime sees in practice

The runtime ships one RoutableAPI implementation — routableUntypedAPI, internal to the middleware package. It wraps untyped.API and is what middleware.Serve / ServeWithBuilder builds for you.

go-swagger generates a second implementation per spec — the *operations.MyAPI type implements every method on RoutableAPI directly, with HandlerFor returning the per-operation ServeHTTP shown below.

The next section walks both.

Two assembly paths

The two RoutableAPI implementations introduced above produce equivalent pipelines, but differ in where the per-route handler is assembled — the untyped one builds it in the runtime via a closure; the typed one is generated source you can read directly.

Untyped — middleware.Serve / ServeWithBuilder

doc, _ := loads.Spec("api.yaml")
api := untyped.NewAPI(doc)
api.RegisterConsumer(runtime.JSONMime, runtime.JSONConsumer())
api.RegisterProducer(runtime.JSONMime, runtime.JSONProducer())
api.RegisterOperation("get", "/pets/{id}", myGetPetHandler)

srv := &http.Server{
	Addr:              ":8080",
	Handler:           middleware.Serve(doc, api),
	ReadHeaderTimeout: readHeaderTimeout,
}
log.Fatal(srv.ListenAndServe())

Full source: docs/examples/server/pipeline/main.go

Internally middleware.newRoutableUntypedAPI builds one http.Handler per route. The bind/validate/handle/respond logic lives in a single closure; if the route declares any security requirement, that closure is wrapped with newSecureAPI so security runs first:

// excerpt from middleware/context.go
var handler http.Handler = http.HandlerFunc(func(w, r) {
    bound, r, validation = context.BindAndValidate(r, route)
    if validation != nil { context.Respond(...); return }
    result, err := oh.Handle(bound)
    // …
})
if len(schemes) > 0 {
    handler = newSecureAPI(context, handler)   // ← wraps with Authorize
}

Typed — generated ServeHTTP per operation

go-swagger generates a small handler per operation that calls the same primitives in the same order, but spelt out explicitly:

// excerpt from a go-swagger generated *operation.ServeHTTP
func (o *GetOrder) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
    route, rCtx, _ := o.Context.RouteInfo(r)
    if rCtx != nil { *r = *rCtx }

    var Params = NewGetOrderParams()
    uprinc, aCtx, err := o.Context.Authorize(r, route)        // ← security
    if err != nil { o.Context.Respond(rw, r, route.Produces, route, err); return }
    // …

    if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // ← bind+validate
        o.Context.Respond(rw, r, route.Produces, route, err)
        return
    }

    res := o.Handler.Handle(Params, principal)                 // ← operation
    o.Context.Respond(rw, r, route.Produces, route, res)       // ← respond
}

Same primitives, same order. Neither shape is enforced by the runtime: a route is just an http.Handler, and you can wrap or replace it. middleware.Builder exists precisely to compose your own chain on top.

Composing extra middleware — Builder

type Builder func(http.Handler) http.Handler

Builder is the standard http.Handler decorator type, aliased so the API reads cleanly. The runtime exposes several entry points that take one:

Entry pointPurpose
middleware.Serve(spec, api)Untyped, no extra middleware (uses PassthroughBuilder).
middleware.ServeWithBuilder(spec, api, builder)Untyped, decorate the routes handler with builder.
Context.APIHandler(builder, opts…)Mounts the routes plus the default Swagger UI / spec serve middleware.
Context.APIHandlerWithUI(builder, ui, opts…)Same, but pick the UI flavour (docui.SwaggerUI / RapiDoc / Redoc).
Context.RoutesHandler(builder)Just the routes — no UI middleware. Useful when you mount under your own mux.

A typical pattern with the justinas/alice middleware library — log, rate-limit, then hand off to the runtime:

decorate := func(next http.Handler) http.Handler {
	return alice.New(loggingMW, rateLimitMW).Then(next)
}

handler := middleware.ServeWithBuilder(doc, api, decorate)

Full source: docs/examples/server/pipeline/main.go

PassthroughBuilder is the identity decorator if you need a place to start.

Failure modes by stage

StageStatusSurfaced as
Router404errors.NotFound
Router405errors.MethodNotAllowed (with Allow header)
Security401errors.Unauthenticated (“invalid credentials”)
Security403errors.New(403, …) if the Authorizer returns a non-errors.Error
Negotiation400malformed Content-Type ⇒ wrapped errors.ParseError
Negotiation415errors.InvalidContentType (no consumes entry matches)
Negotiation406errors.InvalidResponseFormat (no produces entry satisfies Accept)
Binding/Validation422errors.CompositeValidationError aggregating every parameter-level violation (does not stop on first failure)
Operationvarieswhatever the handler returns (error ⇒ runs through Context.Respond and the API’s ServeError)

For the matching algorithm and the v0.30 parameter-honouring change behind 415/406 outcomes, see standalone / content negotiation and the canonical tutorials / media-type selection.

Reading values out of the request

Each stage stashes its result in the request context so downstream middleware can read it without re-doing the work:

HelperReturns
middleware.MatchedRouteFrom(r) *MatchedRoutethe route matched by the router
middleware.SecurityPrincipalFrom(r) anythe principal returned by Authorize
middleware.SecurityScopesFrom(r) []stringthe union of scopes for the matched scheme

Use these inside extra middleware mounted via Builder.