middleware

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 13, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package middleware provides composable github.com/gorilla/mux middleware for building server-side-rendered web applications.

Each constructor returns a github.com/gorilla/mux.MiddlewareFunc (an http.Handler decorator), so they can be registered on a router with Use or chained by hand. Most ordering is flexible, but a few have prerequisites, noted on each function. A typical chain is:

Recovery → PopulateRequestID → PopulateTraceID → PopulateLogger →
SecureHeaders → GzipResponse → RequireSession → HandleCSRF →
PopulateTemplateVariables → InjectCurrentPath → ProcessLocale

Authentication is intentionally pluggable: implement the Authenticator interface (for example, an OIDC client) and install RequireAuthenticated. The framework itself stays free of any specific auth provider dependency.

Index

Examples

Constants

View Source
const (
	// CSRFHeaderField is the header carrying the CSRF token.
	CSRFHeaderField = "X-CSRF-Token"

	// CSRFFormField is the form field carrying the CSRF token.
	CSRFFormField = "csrf_token"
	// CSRFFormFieldTemplate renders a hidden CSRF form input.
	CSRFFormFieldTemplate = `<input type="hidden" name="%s" value="%s" />`

	// CSRFMetaTagName is the meta tag name used by JavaScript to read the token.
	CSRFMetaTagName = "csrf-token"
	// CSRFMetaTagTemplate renders the CSRF meta tag.
	CSRFMetaTagTemplate = `<meta name="%s" content="%s">`

	// TokenLength is the length of the CSRF token, in bytes.
	TokenLength = 64
)
View Source
const (
	ErrMissingExistingToken = Error("missing existing csrf token in session")
	ErrMissingIncomingToken = Error("missing csrf token in request")
	ErrInvalidToken         = Error("invalid csrf token")
)

CSRF-related sentinel errors.

View Source
const (
	// HeaderDebug is the request header that, when present with any value,
	// triggers debug response headers.
	HeaderDebug = "x-debug"
	// HeaderDebugBuildID is the response header carrying the build ID.
	HeaderDebugBuildID = "x-build-id"
	// HeaderDebugBuildTag is the response header carrying the build tag.
	HeaderDebugBuildTag = "x-build-tag"
)
View Source
const (
	// HeaderAcceptLanguage is the standard content-negotiation language header.
	HeaderAcceptLanguage = "Accept-Language"
	// QueryKeyLanguage is the query parameter that overrides the language.
	QueryKeyLanguage = "lang"

	// LeftAlign and RightAlign are the text-direction values placed on the
	// template map for use in templates (e.g. the html dir attribute).
	LeftAlign  = "ltr"
	RightAlign = "rtl"
)
View Source
const CSPNoncePlaceholder = "{{nonce}}"

CSPNoncePlaceholder is the token in a Content-Security-Policy template that ContentSecurityPolicy replaces with the request's nonce.

View Source
const TraceHeader = "X-Cloud-Trace-Context"

TraceHeader is the request header carrying distributed-trace context. It matches the header injected by Google Cloud load balancers, but any upstream that sets it will be honored.

Variables

This section is empty.

Functions

func AddOperatingSystemFromUserAgent

func AddOperatingSystemFromUserAgent() mux.MiddlewareFunc

AddOperatingSystemFromUserAgent inspects the request's User-Agent and stores the inferred client operating system (webctx.OSIOS, webctx.OSAndroid, or webctx.OSUnknown) on the context.

func CheckSessionIdleNoAuth

func CheckSessionIdleNoAuth(idleTTL time.Duration, onIdle http.HandlerFunc) mux.MiddlewareFunc

CheckSessionIdleNoAuth enforces an idle-timeout on the session independent of authentication. If the time since the session's last activity exceeds idleTTL, onIdle is invoked (e.g. to redirect to a login or logout page, or to write a 401) and the request does not proceed. Use it on routes that have no other auth check; routes behind RequireAuthenticated can enforce idleness there instead. Install it after RequireSession.

func ConfigureStaticAssets added in v0.2.0

func ConfigureStaticAssets(devMode bool) mux.MiddlewareFunc

ConfigureStaticAssets prepares responses for static assets: it sets cache headers (never cached in dev mode so edits show up on reload; publicly cacheable for a week otherwise — safe because the renderer's asset tags append the build ID as a cache-busting query string, see github.com/mikehelmick/go-bananas/render.WithBuildID) and rejects directory requests (paths ending in "/") with a 404, so a wrapped http.FileServerFS never emits auto-generated directory listings.

Pair it with a file-serving handler. With templates and assets in an embed.FS (the layout the renderer expects), wire it as:

static := middleware.ConfigureStaticAssets(devMode)
r.PathPrefix("/static/").Handler(static(http.FileServerFS(assets)))

For defense in depth, consider serving an io/fs.Sub of just the static subtree so a routing mistake can never expose templates or other embedded files.

func ContentSecurityPolicy added in v0.2.0

func ContentSecurityPolicy(policy string) mux.MiddlewareFunc

ContentSecurityPolicy sets the Content-Security-Policy response header to policy on every request. Any occurrence of CSPNoncePlaceholder in the policy is replaced with the per-request nonce generated by ProcessNonce, so the header and templates (which read the nonce via webctx.NonceFromContext) share a single nonce:

r.Use(middleware.ProcessNonce())
r.Use(middleware.ContentSecurityPolicy(
	"default-src 'self'; script-src 'self' 'nonce-{{nonce}}'; object-src 'none'"))

When the policy contains the placeholder, install this middleware after ProcessNonce. If no nonce is on the context, the nonce'd source entries are dropped from the emitted header (and a warning is logged) rather than emitting an empty 'nonce-' source.

Example

ExampleContentSecurityPolicy sets a static policy. To include a per-request nonce, add the {{nonce}} placeholder and install ProcessNonce first: "script-src 'self' 'nonce-{{nonce}}'".

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/mikehelmick/go-bananas/middleware"
)

func main() {
	h := middleware.ContentSecurityPolicy("default-src 'self'; object-src 'none'")(
		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
			w.WriteHeader(http.StatusOK)
		}))

	w := httptest.NewRecorder()
	h.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/", nil))

	fmt.Println(w.Header().Get("Content-Security-Policy"))
}
Output:
default-src 'self'; object-src 'none'

func GzipResponse

func GzipResponse() mux.MiddlewareFunc

GzipResponse gzip-compresses responses for clients that advertise gzip support via the Accept-Encoding header.

func HandleCSRF

func HandleCSRF(h *render.Renderer) mux.MiddlewareFunc

HandleCSRF manages per-request CSRF tokens. It reads (or generates and stores) a token on the session, exposes masked token helpers on the template map ("csrfToken", "csrfField", "csrfMeta", "csrfHeaderField", "csrfMetaTagName"), and, for mutating methods, verifies the incoming token. It must be installed after RequireSession.

func InjectCurrentPath

func InjectCurrentPath() mux.MiddlewareFunc

InjectCurrentPath stores the request's path on the template map as a *Path under the key "currentPath", so templates can reason about the active route.

func LogRequests added in v0.2.0

func LogRequests() mux.MiddlewareFunc

LogRequests emits one structured Info log line per request with the method, path, response status, bytes written, and duration. It logs through logging.FromContext, so when installed after PopulateLogger each line is automatically tagged with the request (and trace) ID.

r.Use(middleware.PopulateLogger(logging.DefaultLogger()))
r.Use(middleware.LogRequests())

func MutateMethod

func MutateMethod() mux.MiddlewareFunc

MutateMethod lets HTML forms emulate verbs other than GET and POST by supplying a "_method" form value (e.g. PATCH or DELETE), which is then used as the request method before routing. It must be installed very early in the chain, before the router matches a route.

func OnlyIfEnabled

func OnlyIfEnabled(enabled bool, h *render.Renderer) mux.MiddlewareFunc

OnlyIfEnabled hides routes behind a 404 when enabled is false, so a feature can be toggled off without removing its routes or revealing their existence.

func PopulateLogger

func PopulateLogger(originalLogger *slog.Logger) mux.MiddlewareFunc

PopulateLogger stores a request-scoped logger on the context, enriched with the request ID (and trace ID, if present) so every log line within the request can be correlated. Install it after PopulateRequestID and PopulateTraceID.

If an upstream middleware already placed a non-default logger on the context, that logger is used as the base instead of originalLogger.

func PopulateRequestID

func PopulateRequestID(h *render.Renderer) mux.MiddlewareFunc

PopulateRequestID assigns a random UUID request ID to the context if one is not already present, so it can be logged and surfaced in templates. Install it before PopulateLogger.

func PopulateTemplateVariables

func PopulateTemplateVariables(cfg TemplateConfig) mux.MiddlewareFunc

PopulateTemplateVariables seeds the template map with common values (server name, endpoint, build identifiers, dev-mode flag, and any Extra values) and bootstraps the map so later middleware can add to it. Install it after the session middleware and before handlers render.

func PopulateTraceID

func PopulateTraceID() mux.MiddlewareFunc

PopulateTraceID extracts the trace ID from the trace header (the portion before the first "/") and stores it on the context, if present, well-formed (alphanumeric/dash/underscore, at most 128 characters), and not already set. Install it before PopulateLogger.

func ProcessDebug

func ProcessDebug(buildID, buildTag string) mux.MiddlewareFunc

ProcessDebug echoes the provided build identifiers in response headers when the request includes the "X-Debug" header with any value. This aids debugging without exposing build information to ordinary clients.

func ProcessLocale

func ProcessLocale(p LocaleProvider) mux.MiddlewareFunc

ProcessLocale resolves the request locale via p and stores the translator on the context (for the "t"/"tDefault" template functions) along with template map values "locale", "acceptLanguage", "textLanguage", and "textDirection". Install it after the session/template middleware.

func ProcessNonce

func ProcessNonce() mux.MiddlewareFunc

ProcessNonce generates a fresh, cryptographically-random Content-Security- Policy nonce per request and stores it on the context, where templates can read it (via webctx.NonceFromContext) to mark trusted inline scripts/styles.

The nonce is generated server-side; it is deliberately NOT read from a request header, because a client-controlled nonce would let an attacker predict it and defeat the CSP. To take effect, emit a Content-Security-Policy header that references the same nonce — ContentSecurityPolicy does this via its CSPNoncePlaceholder when installed after this middleware.

func Recovery

func Recovery(h *render.Renderer) mux.MiddlewareFunc

Recovery recovers from panics in downstream handlers, logging the panic in a structured format and returning a 500 to the client so the server keeps running. It is typically the outermost middleware in the chain.

Example

ExampleRecovery demonstrates assembling the recommended middleware chain on a gorilla/mux router. The order matters: Recovery is outermost, request/trace IDs and the logger come next, then security and session middleware.

package main

import (
	"fmt"
	"net/http"
	"testing/fstest"

	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"
	"github.com/mikehelmick/go-bananas/cookiestore"
	"github.com/mikehelmick/go-bananas/logging"
	"github.com/mikehelmick/go-bananas/middleware"
	"github.com/mikehelmick/go-bananas/render"
)

func main() {
	h, err := render.New(fstest.MapFS{
		"500.html": &fstest.MapFile{Data: []byte(`{{define "500"}}error{{end}}`)},
	})
	if err != nil {
		panic(err)
	}

	store := cookiestore.New(func() ([][]byte, error) {
		return [][]byte{make([]byte, 64)}, nil
	}, &sessions.Options{Path: "/"})

	r := mux.NewRouter()
	r.Use(middleware.Recovery(h))
	r.Use(middleware.PopulateRequestID(h))
	r.Use(middleware.PopulateTraceID())
	r.Use(middleware.PopulateLogger(logging.DefaultLogger()))
	r.Use(middleware.SecureHeaders(false, middleware.ServerTypeHTML))
	r.Use(middleware.GzipResponse())
	r.Use(middleware.RequireSession(store, nil, h))
	r.Use(middleware.HandleCSRF(h))
	r.Use(middleware.InjectCurrentPath())

	r.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprintln(w, "hello")
	})

	fmt.Println("router configured")
}
Output:
router configured

func RequireAuthenticated

func RequireAuthenticated(a Authenticator, h *render.Renderer) mux.MiddlewareFunc

RequireAuthenticated rejects anonymous requests. It calls a's Authenticate: a non-nil error renders a 500; a nil principal renders a 401; otherwise the principal is stored on the context (see webctx.PrincipalFromContext) and the request proceeds.

The plumbing an OIDC flow needs is provided by other middleware in this package: RequireSession for token/claims storage, HandleCSRF, SecureHeaders, and CheckSessionIdleNoAuth for idle expiry.

func RequireHeader

func RequireHeader(header string, h *render.Renderer) mux.MiddlewareFunc

RequireHeader requires that the request carry the named header with any non-empty value, returning 401 otherwise.

func RequireHeaderValues

func RequireHeaderValues(header string, allowed []string, h *render.Renderer) mux.MiddlewareFunc

RequireHeaderValues requires that the request carry the named header with a value matching one of allowed, returning 401 otherwise.

func RequireHostHeader

func RequireHostHeader(allowed []string, h *render.Renderer, stripPort bool) mux.MiddlewareFunc

RequireHostHeader requires that the request's Host header match one of allowed (case-insensitively), returning 401 otherwise. When stripPort is true, any port in the Host header is ignored during comparison.

func RequireNamedSession

func RequireNamedSession(store sessions.Store, name string, splitValues []any, h *render.Renderer) mux.MiddlewareFunc

RequireNamedSession is like RequireSession but uses a specific session (cookie) name instead of the default.

func RequireSession

func RequireSession(store sessions.Store, splitValues []any, h *render.Renderer) mux.MiddlewareFunc

RequireSession loads or creates a session from store, stores it on the request context, and ensures the session's flash data is available to templates. Any handler that uses sessions, flash messages, or CSRF must be wrapped with this.

splitValues names session keys whose values are large enough to warrant their own cookie (browsers cap individual cookies at ~4KB); each listed key is split out into a separate companion cookie on save and rejoined on load.

func SecureHeaders

func SecureHeaders(devMode bool, serverType ServerType) mux.MiddlewareFunc

SecureHeaders installs a sensible set of security-related response headers (HSTS, nosniff, referrer policy, and, for HTML servers, frame denial). When devMode is true, HTTPS redirects and HSTS are relaxed for local development.

It deliberately does NOT set a Content-Security-Policy: a useful CSP is highly application-specific. Add one with ContentSecurityPolicy, combined with ProcessNonce for per-request nonces on trusted inline scripts/styles.

Types

type Authenticator

type Authenticator interface {
	// Authenticate returns the authenticated principal for the request, or
	// (nil, nil) if the request is anonymous (no credentials presented). A
	// non-nil error indicates the authentication attempt itself failed (for
	// example, an identity provider was unreachable) and results in a 500.
	Authenticate(r *http.Request) (principal any, err error)
}

Authenticator authenticates an HTTP request. It is the single seam through which applications plug in an identity provider (OIDC, an API key scheme, a signed token, etc.) without the framework depending on any of them.

Implementations typically read a session, cookie, or header populated by an earlier login flow. The returned principal is opaque to the framework: its concrete type is the application's own user/session model, retrievable later with webctx.PrincipalFromContext.

type Error

type Error string

Error is a constant, comparable error type for this package.

func (Error) Error

func (e Error) Error() string

Error implements the error interface.

type LocaleProvider

type LocaleProvider interface {
	// Lookup returns the best translator for the given ordered hints (typically
	// the "lang" query parameter followed by the Accept-Language header), along
	// with its BCP-47 language tag (e.g. "en", "ar"). It may return a nil
	// translator, in which case templates fall back to default strings.
	Lookup(hints ...string) (t gotext.Translator, lang string)
}

LocaleProvider resolves the best gotext.Translator for a request. It is the pluggable seam for internationalization: the framework does not prescribe how translations are loaded, only how the chosen translator reaches templates.

type Path

type Path struct {
	// contains filtered or unexported fields
}

Path wraps the request URL and is placed on the template map under "currentPath" by InjectCurrentPath. Its methods make it easy for templates to highlight the active nav item.

func (*Path) IsDir

func (p *Path) IsDir(s string) bool

IsDir reports whether the request URI is prefixed by s.

func (*Path) IsFile

func (p *Path) IsFile(s string) bool

IsFile reports whether the final path segment of the request URI equals s.

func (*Path) IsPath

func (p *Path) IsPath(s string) bool

IsPath reports whether the request URI exactly equals s.

func (*Path) String

func (p *Path) String() string

String returns the full request URI.

type ServerType

type ServerType string

ServerType describes the kind of responses a server emits, which tweaks a few secure-header defaults (for example, whether to deny framing).

const (
	// ServerTypeHTML is for servers that render HTML pages; it enables
	// clickjacking protection (X-Frame-Options: DENY).
	ServerTypeHTML ServerType = "html"
	// ServerTypeAPI is for servers that emit machine-readable responses.
	ServerTypeAPI ServerType = "api"
)

type TemplateConfig

type TemplateConfig struct {
	// ServerName is the application name; it is used as the default page title.
	ServerName string

	// ServerEndpoint is the canonical base URL of the server. If empty, it is
	// derived from each request via [response.RealHostFromRequest].
	ServerEndpoint string

	// BuildID and BuildTag identify the running build.
	BuildID  string
	BuildTag string

	// DevMode indicates the server is running in development mode.
	DevMode bool

	// Extra holds any additional application-specific values to expose to every
	// template (for example, feature flags). Keys here are copied onto the
	// template map verbatim.
	Extra map[string]any
}

TemplateConfig holds the common values seeded onto the template map by PopulateTemplateVariables.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL