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:
| Hook | Runs | Sees the matched route? | Use it for |
|---|---|---|---|
setupGlobalMiddleware | Before swagger routing — wraps everything (spec, UI, all routes) | No | Cross-cutting concerns: security headers, panic recovery, a metrics mount |
setupMiddlewares | After routing — only matched operations | Yes (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
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.
Related
- Todo list server — where the
configure_*.gohooks come from. - Generation flags —
--exclude-specand flag strategies (no--exclude-mainneeded here).