Skip to main content

Middleware Guide

Learn about the production-ready middleware stack in Tracks-generated applications.

Overview

Middleware in Go web applications are functions that wrap HTTP handlers, executing code before and/or after the handler runs. Tracks generates applications with a carefully ordered middleware stack for security, observability, and performance.

Middleware Stack

Tracks applications include the following middleware, applied in this order:

MiddlewarePurposeConfigurable
RequestIDAssigns unique ID to each request for tracingNo
RealIPExtracts real client IP from proxy headersNo
CompressGzip response compressionNo
LoggerStructured request loggingNo
RecovererCatches panics, logs stack tracesNo
TimeoutCancels long-running requestsYes
ThrottleLimits concurrent requestsYes
CSPGenerates nonces for Content-Security-PolicyNo
SecurityHeadersSets security headers (X-Frame-Options, etc.)No
CORSHandles cross-origin requestsYes

Middleware Ordering

The order of middleware matters for both security and correctness:

  1. RequestID first - Ensures all subsequent logging includes the request ID
  2. RealIP before logging - Accurate client IP in logs
  3. Compress early - Wraps response writer before content is written
  4. Logger/Recoverer - Catch panics before they propagate
  5. Timeout before Throttle - Timeout applies to total request time
  6. Security headers last - Applied after request processing

Changing the order can break functionality or create security issues.

Configuration Reference

Configure middleware via environment variables in .env:

Timeout Middleware

Cancels requests that take too long, preventing slow clients from consuming server resources.

VariableTypeDefaultDescription
APP_MIDDLEWARE_TIMEOUT_REQUESTduration60sMaximum request duration

When to adjust:

  • Increase for endpoints with long-running operations (file uploads, report generation)
  • Decrease in high-traffic environments to fail fast
  • Set to 0 to disable (not recommended in production)

Behavior: When timeout is reached, the request context is cancelled. Well-behaved handlers should check ctx.Done() and stop processing.

Throttle Middleware

Limits concurrent requests to prevent server overload. Uses Chi's Throttle middleware.

VariableTypeDefaultDescription
APP_MIDDLEWARE_THROTTLE_LIMITint100Maximum concurrent requests
APP_MIDDLEWARE_THROTTLE_BACKLOG_LIMITint50Requests queued when at limit
APP_MIDDLEWARE_THROTTLE_BACKLOG_TIMEOUTduration30sHow long queued requests wait

When to adjust:

  • Increase limit for servers with more resources
  • Decrease limit if requests are resource-intensive
  • Set backlog to 0 to reject immediately when at limit (returns 503)

Behavior:

  1. Requests under limit: processed immediately
  2. Requests at limit with backlog space: queued
  3. Requests at limit with full backlog: 503 Service Unavailable
  4. Queued requests timing out: 503 Service Unavailable

CORS Middleware

Handles Cross-Origin Resource Sharing for requests from different domains. Disabled by default.

VariableTypeDefaultDescription
APP_MIDDLEWARE_CORS_ENABLEDboolfalseEnable CORS handling
APP_MIDDLEWARE_CORS_ALLOWED_ORIGINSstring[][]Allowed origins (comma-separated)
APP_MIDDLEWARE_CORS_ALLOWED_METHODSstring[]GET,POST,PUT,DELETE,OPTIONSAllowed HTTP methods
APP_MIDDLEWARE_CORS_ALLOWED_HEADERSstring[]Accept,Authorization,Content-Type,X-CSRF-TokenAllowed request headers
APP_MIDDLEWARE_CORS_EXPOSED_HEADERSstring[][]Headers exposed to JavaScript
APP_MIDDLEWARE_CORS_ALLOW_CREDENTIALSboolfalseAllow cookies/auth headers
APP_MIDDLEWARE_CORS_MAX_AGEint300Preflight cache duration (seconds)

When to enable:

  • SPA on different domain than API
  • Mobile app calling your API
  • Third-party integrations

When NOT to enable:

  • Same-origin requests (SPA served from same domain)
  • Server-rendered HTML with HTMX (same-origin by default)

Security considerations:

  • Never use * with AllowCredentials=true
  • Specify exact origins, not patterns
  • Be restrictive with allowed headers

Example configuration:

APP_MIDDLEWARE_CORS_ENABLED=true
APP_MIDDLEWARE_CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com
APP_MIDDLEWARE_CORS_ALLOW_CREDENTIALS=true

Security Headers

The SecurityHeaders middleware sets the following headers:

X-Content-Type-Options

X-Content-Type-Options: nosniff

Prevents browsers from MIME-sniffing responses, reducing XSS risk.

X-Frame-Options

X-Frame-Options: DENY

Prevents clickjacking by blocking the page from being embedded in frames.

Referrer-Policy

Referrer-Policy: strict-origin-when-cross-origin

Controls how much referrer information is sent with requests:

  • Same-origin: full URL
  • Cross-origin: origin only (no path)
  • HTTPS to HTTP: no referrer

Permissions-Policy

Permissions-Policy: geolocation=(), microphone=(), camera=()

Disables browser features that could be exploited. Modify if your app needs these features.

Content-Security-Policy

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-xxx'; ...

Restricts resource loading to prevent XSS. The CSP middleware generates a unique nonce for each request, allowing inline scripts that include the nonce.

CSP directives:

DirectiveValuePurpose
default-src'self'Fallback for unspecified directives
script-src'self' 'nonce-xxx'Scripts from same origin + nonced inline
style-src'self' 'unsafe-inline'Styles from same origin + inline
img-src'self' data:Images from same origin + data URIs
font-src'self'Fonts from same origin
connect-src'self'XHR/fetch to same origin
frame-ancestors'none'Cannot be framed
base-uri'self'Restricts <base> tag
form-action'self'Form submissions to same origin

Using CSP Nonces

The CSP middleware generates a nonce and stores it in the request context. Access it in templ components:

package components

import "github.com/a-h/templ"

templ InlineScript() {
<script nonce={ templ.GetNonce(ctx) }>
console.log("This script is allowed by CSP");
</script>
}

All HTMX scripts included by Tracks use the nonce automatically.

Adding Custom Middleware

Add middleware in internal/http/routes.go:

func (s *Server) routes() {
// ... existing middleware ...

// Add custom middleware before routes
s.router.Use(myCustomMiddleware)

// ... routes ...
}

func myCustomMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Before handler
start := time.Now()

next.ServeHTTP(w, r)

// After handler
duration := time.Since(start)
log.Printf("Request took %v", duration)
})
}

Route-Specific Middleware

Apply middleware to specific routes using r.Group():

s.router.Route("/admin", func(r chi.Router) {
r.Use(adminAuthMiddleware)
r.Get("/", adminHandler.Dashboard)
})

Disabling Middleware

To disable configurable middleware, set their values to disable:

  • Timeout: Set APP_MIDDLEWARE_TIMEOUT_REQUEST=0
  • Throttle: Set APP_MIDDLEWARE_THROTTLE_LIMIT=0
  • CORS: Set APP_MIDDLEWARE_CORS_ENABLED=false (default)

Non-configurable middleware (RequestID, Compress, Logger, etc.) must be removed from routes.go if not needed.

Warning: Disabling security middleware (Recoverer, SecurityHeaders) in production is not recommended.

Testing Middleware

Verify headers with curl:

# Check security headers
curl -I http://localhost:8080/

# Expected output includes:
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY
# Content-Security-Policy: default-src 'self'; ...

Test CORS preflight:

curl -X OPTIONS -H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-I http://localhost:8080/api/endpoint