Pular para o conteúdo
Esta página ainda não foi traduzida. O conteúdo abaixo está em inglês. Ler em inglês

The basics

Middleware

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

Última atualização:

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. web.AuditEvent(r, "payout.recorded", ...) logs a routine audit record at INFO with the same envelope.

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, peer (the RemoteAddr host) and ip (the client resolved through TRUSTED_PROXIES, as c.IP()). 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
	TrustedProxies:      proxies, // []*net.IPNet: honour X-Forwarded-Proto only from these peers
}))

X-Forwarded-Proto counts only as a single header line saying https. SecureHeadersConfig.Validate() (called by DefaultMiddleware at boot) refuses TrustForwardedProto without TrustedProxies; without that check, an empty list trusts the header from any peer. DefaultMiddleware and APIMiddleware already install SecureHeaders; there, set TRUST_FORWARDED_PROTO=true and TRUSTED_PROXIES instead of adding a second one. When the proxy's range cannot be trusted, HSTSForce: true (HSTS_FORCE=true) sends HSTS on every response instead; see HSTS without a trusted range for the trade-offs.

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>`)

CSPSources adds sources to the policy (the default or your CSP) without rewriting it. A fetch directive missing from the policy starts from the directive the browser would fall back to: script-src-elem from script-src (so it keeps the nonce), worker-src from child-src then script-src, frame-src from child-src, the others from default-src. ReportURI (an https:// URL without credentials, or a path starting with one /) adds report-uri and report-to plus the matching Reporting-Endpoints header:

Go
cfg := middleware.SecureHeadersConfig{
	CSPSources: map[string][]string{
		"script-src":  {"https://plausible.io"},
		"connect-src": {"https://plausible.io"},
		"frame-src":   {"https://www.youtube-nocookie.com"}, // becomes 'self' https://www.youtube-nocookie.com
	},
	ReportURI: "/csp-reports",
}
if err := cfg.Validate(); err != nil {
	return err
}

Refused sources: 'unsafe-*' keywords, static nonces, * hosts (*, *:*, https://*), wildcards over fewer than two labels (*.com), host wildcards in default-src, script-src*, object-src, worker-src and base-uri, and unsafe schemes. Scheme-only sources are accepted only as https:, data: or blob: in img-src/media-src (https:/data: in font-src) and https: or wss: in connect-src.

SecureHeaders never fails: invalid extras are dropped (the policy stays stricter) with one warning in the log, so call Validate at boot (vukem.DefaultMiddlewareWith does). Anything weaker than the helper allows needs a full CSP string.

Styles injected at runtime need the nonce too, since style-src has no 'unsafe-inline'. The starters already handle the common cases:

  • Inertia progress bar: createInertiaApp({ nonce }) reads it from <meta property="csp-nonce"> (the scaffolds do this).
  • htmx 4 indicator: injected as a constructed stylesheet, which the CSP does not block. On htmx 2, set htmx.config.inlineStyleNonce or includeIndicatorStyles: false.
  • A library that injects a fixed <style> and accepts no nonce: add its hash, e.g. CSPSources: {"style-src": {"'sha256-…'"}} (the browser console prints the hash it expects).
  • style="…" attributes in server-rendered HTML stay blocked; use classes. Setting el.style.x from JavaScript is not affected.

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)

vukem.DefaultMiddlewareWith(app, r, opts) installs the same stack with options; the zero vukem.MiddlewareOptions is DefaultMiddleware:

Go
err := vukem.DefaultMiddlewareWith(app, r, vukem.MiddlewareOptions{
	// Runs after the defaults (Vite dev server) are set; validated at boot.
	SecureHeaders: func(cfg *middleware.SecureHeadersConfig) {
		cfg.CSPSources = map[string][]string{
			"script-src":  {"https://plausible.io"},
			"connect-src": {"https://plausible.io"},
		}
		cfg.ReportURI = "/csp-reports"
	},
	SessionBypass: []string{"/webhooks/"},             // no session (no cookie) on these routes
	CSRFBypass:    []string{"POST /webhooks/{name}"},  // signed webhooks; logged at boot
})
OptionEffect
SecureHeadersEdits the SecureHeadersConfig (CSPSources, ReportURI, TrustForwardedProto, ...). An invalid result fails at boot.
StatelessNo session middleware at all, for cookieless apps. CSRF protection stays on as Origin/Sec-Fetch-Site checks only: csrfToken/csrfField render empty, and session features (c.Flash, session login) return an error.
SessionBypassServeMux patterns served without sessions. A pattern ending in / (/webhooks/) covers its whole subtree; POST /hooks/{id} matches one method and path. Refused together with Stateless.
CSRFBypassServeMux patterns (same matching) exempt from CSRF checks; each one is logged at boot. Protect them another way (signatures).
I18n*i18n.MiddlewareConfig for the locale resolver installed by WithTranslations (nil: defaults): sources and their order (Order), the selectable Locales, parameter and cookie names. Validated at boot; refused without WithTranslations. See Localization.

Maintenance mode, the body limit and CSRF for every other route stay on: the options only add sources or skip what you list.

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.