Skip to content

Add middleware

// Named middleware is owned by the service that registers it, which is what
// lets a teardown disable it everywhere it is referenced.
func register(router *gas.Router) {
router.Register("auth", "require-auth", func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// validate a token, then:
next.ServeHTTP(w, r)
})
})
}
func apply(router *gas.Router) {
// Globally, by name.
router.UseMiddlewareByName("require-auth")
// Globally, inline. An inline func has no owner and survives any teardown.
router.UseMiddlewareFunc(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
})
}

The distinction matters more than it looks. Named middleware is owned by the service that registered it, so a teardown of that service disables it everywhere it is referenced. Inline middleware has no owner and survives any teardown. See Ownership and teardown.

UseMiddlewareByName panics if the name is not registered, which is deliberate: a typo should not silently leave a route unguarded.

router.UseMiddlewareFunc(gas.RequestLogger[gas.Logger]())

Logs method, path, status, bytes, duration, and remote address, at error level for statuses at 400 and above. Needs a scoped gas.Logger in the container. Covered in Structured logging.

// Secure defaults out of the box; override individually, or pass an empty
// string to drop a header entirely.
func security(router *gas.Router) {
router.UseMiddlewareFunc(gas.SecurityHeaders(
gas.WithSecurityHeadersFrameOptions("SAMEORIGIN"),
gas.WithSecurityHeadersContentSecurityPolicy("default-src 'self'"),
))
}

Applied by default:

Header Default
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy camera=(), microphone=(), geolocation=()

Emitted only when you configure them, because a wrong value is worse than none: Content-Security-Policy, Strict-Transport-Security, Cross-Origin-Opener-Policy, and Cross-Origin-Resource-Policy.

func caching(router *gas.Router) {
// Fingerprinted assets: cache hard.
router.UseMiddlewareFunc(gas.CacheControl(
gas.WithCacheControlPathPrefix("/static/"),
gas.WithCacheControlPublic(),
gas.WithCacheControlMaxAge(365*24*time.Hour),
gas.WithCacheControlImmutable(),
))
// API responses: never store.
router.UseMiddlewareFunc(gas.CacheControl(
gas.WithCacheControlPathPrefix("/api/"),
gas.WithCacheControlNoStore(),
))
}

Path filters accept exact paths, prefixes, and suffixes, in singular and plural forms. With no filters the header applies to every request; with no directives the middleware passes through without setting anything.

Gas enables Go’s http.CrossOriginProtection by default. Non-safe cross-origin browser requests, POST, PUT, PATCH, DELETE, are rejected unless the origin is trusted. Safe methods always pass, and requests with no Sec-Fetch-Site or Origin header, such as curl and server-to-server calls, are allowed through.

Same-origin apps need no configuration. Anything else does:

// Cross-origin protection is on by default. Add the front-ends you trust.
func csrf() []gas.AppOption {
return []gas.AppOption{
gas.WithTrustedOrigin("https://app.example.com"),
// Webhook receivers validate their own signatures.
gas.WithCSRFInsecureBypassPattern("/webhooks/stripe"),
gas.WithCSRFDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = gas.WriteError(w, r, gas.Forbidden("cross-origin request denied"))
})),
}
}

A deny handler runs before the request scope exists, so resolve nothing from DI inside it. gas.WriteError is safe there: it renders the unified error shape without logging or touching the scope.

Middleware runs in registration order, outermost first. Global middleware wraps group middleware, which wraps per-route middleware. Register logging early so it observes everything downstream, and authentication before anything that assumes a principal is present.