📖 4 min read (~ 800 words).

Custom middleware

A generated server is a standard net/http stack, so any http.Handler middleware composes with it. The middleware example wires two real concerns — security response headers and Prometheus metrics — into a generated server using only the hook points codegen already leaves for you. No --exclude-main and no custom router required.

Tip

Source: middleware/. Generated with swagger generate server -A Greeter -f ./swagger.yml. Answers go-swagger issues #2683 (security headers) and #1120 (a /metrics endpoint).

Two extension points

Codegen leaves two hooks in restapi/configure_*.go, and they run at different stages of the request:

HookRunsSees the matched route?Use it for
setupGlobalMiddlewareBefore swagger routing — wraps everything (spec, UI, all routes)NoCross-cutting concerns: security headers, panic recovery, a metrics mount
setupMiddlewaresAfter routing — only matched operationsYes (middleware.MatchedRouteFrom)Per-route concerns: instrumentation labelled by route template

The global hook — headers + metrics mount

setupGlobalMiddleware wraps the entire server. Reading outermost to innermost: metrics.Mount intercepts GET /metrics so scrape traffic bypasses routing; unrolled/secure adds HSTS and other headers to every response, including the spec and UI:

// setupGlobalMiddleware wraps everything the server serves, including the
// swagger spec document and the embedded UI.
//
// Order matters. From outermost to innermost:
//
//  1. metrics.Mount intercepts GET /metrics so scrape traffic bypasses the
//     swagger router as well as the headers and instrumentation below.
//  2. unrolled/secure injects HSTS and other security response headers on
//     every response, including /swagger.json and the UI assets.
//  3. The swagger handler itself (which will pass through setupMiddlewares
//     once a route is matched).
func setupGlobalMiddleware(handler http.Handler) http.Handler {
	sec := secure.New(secure.Options{
		STSSeconds:           63072000,
		STSIncludeSubdomains: true,
		// ForceSTSHeader: true emits HSTS even on plain-HTTP requests so the
		// example is easy to exercise with `curl http://...`. In production
		// you would either serve over HTTPS directly (the library detects it
		// from the TLS connection) or set SSLProxyHeaders so detection works
		// behind a TLS-terminating proxy — and leave ForceSTSHeader at false.
		ForceSTSHeader:     true,
		FrameDeny:          true,
		ContentTypeNosniff: true,
		ReferrerPolicy:     "no-referrer",
		// Send "X-XSS-Protection: 0" explicitly. The default BrowserXssFilter
		// option sends "1; mode=block", which is now considered harmful in
		// some browsers; modern OWASP / MDN guidance is to disable the
		// legacy XSS auditor outright. See README.
		CustomBrowserXssValue: "0",
	})

	return metrics.Mount(sec.Handler(handler))
}

Full source: middleware/restapi/configure_greeter.go

The per-route hook — instrumentation

setupMiddlewares runs after routing, so the matched route is available. That’s what lets metrics be labelled by route:

// setupMiddlewares wraps the swagger handler after routing.
//
// At this point, [middleware.MatchedRouteFrom] returns the matched route, so
// this is the right place to plug per-route instrumentation. Requests that do
// not match an operation (404/405) never reach this layer; that is by design
// for an example whose metrics describe API operation behaviour.
func setupMiddlewares(handler http.Handler) http.Handler {
	return metrics.Instrument(handler)
}

Full source: middleware/restapi/configure_greeter.go

The go-swagger-specific glue: the route label

The one piece that’s specific to a go-swagger server is the metrics route label. It uses middleware.MatchedRouteFrom(r).PathPattern — the swagger path template (/greet/{name}) rather than the literal path (/greet/alice) — so Prometheus label cardinality stays bounded instead of exploding one series per distinct URL:

// Instrument records request count and latency for the wrapped handler.
//
// It is meant to be installed in the generated server's setupMiddlewares hook
// (i.e. after swagger routing) so that [middleware.MatchedRouteFrom] returns
// the matched route. The route label is the swagger path template (e.g.
// "/greet/{name}") rather than the literal request path, to keep the metric
// label cardinality bounded.
func Instrument(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(rec, r)

		route := "unmatched"
		if mr := middleware.MatchedRouteFrom(r); mr != nil && mr.PathPattern != "" {
			route = mr.PathPattern
		}

		obs := prometheus.Labels{
			labelMethod: r.Method,
			labelRoute:  route,
			labelCode:   strconv.Itoa(rec.status),
		}
		requestsTotal.With(obs).Inc()
		requestDuration.With(obs).Observe(time.Since(start).Seconds())
	})
}

Full source: middleware/internal/metrics/metrics.go

Trying it

$ go run ./middleware/cmd/greeter-server --port 8080
$ curl -i http://127.0.0.1:8080/api/greet
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=63072000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
...
{"message":"hello"}

$ curl http://127.0.0.1:8080/metrics
http_requests_total{code="200",method="GET",route="/api/greet/{name}"} 3

The route label carries the basePath (/api) because the matched template is the full path the router serves.

Other middleware, same pattern

Any http.Handler middleware composes identically via setupGlobalMiddleware — panic recovery (gorilla/handlers.RecoveryHandler), a request ID, an access log, and so on. The example keeps to headers + metrics to stay focused on the wiring.