Skip to content
You are reading the v0.20 documentation. Go to the latest version (v1.0)

The basics

Middleware

Standard func(http.Handler) http.Handler middleware in three stacks, plus Vukem's built-in middleware.

Last updated:

Middleware has the standard signature, so any net/http middleware works with Vukem:

Go
type Middleware = func(http.Handler) http.Handler

Three stacks#

StackRegistered withRuns
Serverr.UseServer(mw...)Every request, even when no route matches (404s, static files, CORS preflight)
Routerr.Use(mw...)Only when a route matches
Route / groupg.Use(mw...), route.Use(mw...), resource.Use(mw...)Only for the routes it is attached to
Go
r := router.New(services)

r.UseServer(
	middleware.Recover(),
	middleware.RequestID(),
	middleware.Logger(app.Log),
	middleware.SecureHeaders(middleware.SecureHeadersConfig{}),
	middleware.MethodOverride(),
	static,
)

r.Use(middleware.Timeout(30 * time.Second))

r.Group("/api", func(g *router.Group) {
	g.Use(middleware.JSONOnly(), middleware.BodyLimit(1<<20))
	g.APIResource("posts", posts)
})

The order is outermost first: server stack, then router stack, then each enclosing group from the outside in, then the route's own middleware.

Writing middleware#

Go
func requireAdmin(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !isAdmin(r) {
			web.Fail(w, r, errs.ErrForbidden) // rendered by the error handler
			return
		}
		next.ServeHTTP(w, r)
	})
}

web.Fail(w, r, err) renders an error with the same format as handlers (HTML page, htmx fragment or problem+json). web.SecurityEvent(r, "authz.denied", ...) logs a security event at WARN with a stable event attribute.

Built-in middleware#

MiddlewarePurpose
Recover()Converts panics to 500 through the error handler; the stack trace is logged server-side only. http.ErrAbortHandler is re-raised.
RequestID()Reuses a well-formed incoming X-Request-ID (max 128 safe characters) or generates one; stores it in the context and echoes it in the response.
Logger(log)One structured slog line per request: method, path, status, duration, bytes, request id. 5xx at ERROR, 4xx at WARN.
Timeout(d)Sets a deadline on the request context.
BodyLimit(n)Caps request bodies at n bytes; larger bodies fail with 413.
MethodOverride()Lets HTML forms send PUT, PATCH and DELETE via a hidden _method field on POST. Register it as server middleware so routing sees the new method.
JSONOnly()Write requests must send JSON (415 otherwise) and errors render as problem+json.
CORS(cfg)Cross-origin resource sharing, deny by default.
SecureHeaders(cfg)Security headers and a nonce-based Content-Security-Policy.
Static(root, cfg) / StaticFS(fsys, cfg)Serves files from an *os.Root or any fs.FS. See Static files.
Gzip()Compresses text-like responses of 1 KiB or more for clients that accept gzip.

CORS#

Go
cors, err := middleware.CORS(middleware.CORSConfig{
	AllowedOrigins:   []string{"https://app.example.com"},
	AllowCredentials: true,
	MaxAge:           10 * time.Minute,
})
if err != nil {
	return err // "*" with credentials is refused at boot
}
r.UseServer(cors)

The zero CORSConfig allows no cross-origin requests. Defaults: methods GET, HEAD, POST, PUT, PATCH, DELETE; headers Accept, Authorization, Content-Type, X-Requested-With, X-CSRF-Token. Register it as server middleware so preflight requests are answered before routing.

Secure headers and CSP nonces#

Go
r.UseServer(middleware.SecureHeaders(middleware.SecureHeadersConfig{
	HSTSMaxAge:          365 * 24 * time.Hour,
	TrustForwardedProto: true, // only behind a trusted proxy
}))
HeaderDefault
Content-Security-Policymiddleware.DefaultCSP with a fresh nonce per request
Strict-Transport-Securitymax-age=63072000; includeSubDomains on HTTPS requests
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Cross-Origin-Opener-Policysame-origin
Permissions-Policycamera=(), microphone=(), geolocation=()

CSP accepts a custom policy where {nonce} is replaced per request; "-" disables the header (same for FrameOptions). A negative HSTSMaxAge disables HSTS. Read the nonce with middleware.CSPNonce(r.Context()) and add it to inline scripts and styles:

Go
nonce := middleware.CSPNonce(c.Context())
return c.HTML(http.StatusOK, `<script nonce="`+nonce+`">init()</script>`)

Default stack#

vukem.DefaultMiddleware(app, r), called by the starter's routes.Web, installs:

StackMiddleware
ServerRecover, RequestID, Logger, SecureHeaders, MethodOverride
RouterBodyLimit(web.DefaultMaxBody), sessions, CSRF (CSRF_TRUSTED_ORIGINS adds trusted origins)

Generating middleware#

Terminal
vukem make:middleware RequireAdmin   # app/middleware/require_admin.go

The generated func RequireAdmin(next http.Handler) http.Handler is plain Go: attach it with r.Use, g.Use or route.Use. There is no named-middleware registry.