Middleware
Nova has ready-to-use middleware for net/http
. In this document, you will find the list of built-in middleware and how to use them.
Table of Contents
- What is Middleware?
- Custom Middleware
- Built-in Middleware
- LoggingMiddleware
- RecoveryMiddleware
- RequestIDMiddleware
- CORSMiddleware
- SecurityHeadersMiddleware
- TimeoutMiddleware
- BasicAuthMiddleware
- MethodOverrideMiddleware
- EnforceContentTypeMiddleware
- CacheControlMiddleware
- GzipMiddleware
- CSRFMiddleware
- ETagMiddleware
- HealthCheckMiddleware
- RealIPMiddleware
- MaxRequestBodySizeMiddleware
- TrailingSlashRedirectMiddleware
- ForceHTTPSMiddleware
- ConcurrencyLimiterMiddleware
- MaintenanceModeMiddleware
- IPFilterMiddleware
- RateLimitMiddleware
What is Middleware?
In the context of Nova’s web handling capabilities (using Go’s standard net/http
package), middleware refers to a function that wraps an http.Handler
. It follows the standard Go pattern: a function that takes an http.Handler
and returns a new http.Handler
.
// Middleware defines the function signature for middleware.
// A middleware is a function that wraps an http.Handler, adding extra behavior.
type Middleware func(http.Handler) http.Handler
Middleware functions sit between the server’s routing logic (like nova.Router
) and your final request handler (http.HandlerFunc
). They provide a way to process the http.Request
and http.ResponseWriter
or perform actions before or after your main handler logic runs.
Think of it like layers processing an incoming HTTP request:
- An HTTP request arrives.
- Nova’s router directs the request towards the appropriate handler.
- If middleware is applied (e.g., via
router.Use(...)
), the request passes through each middleware function in sequence. - Each middleware can:
- Examine or modify the
http.Request
(e.g., add context values, parse headers). - Wrap the
http.ResponseWriter
to intercept or modify the response (e.g., capture status code, compress data). - Perform tasks like logging, timing, authentication checks, authorization, rate limiting, or setting common headers.
- Decide whether to pass control to the
next
handler in the chain by callingnext.ServeHTTP(w, r)
. - Perform tasks after the
next
handler has completed (e.g., logging the response status, cleanup).
- Examine or modify the
- Finally, the core
http.HandlerFunc
for the route is executed (if the middleware chain allowed it).
Key Benefits of Using Middleware:
- Separation of Concerns: Keeps cross-cutting logic (like logging, authentication, compression, security headers) separate from your core request handling logic, making handlers cleaner and more focused.
- Reusability: Write common pre-processing or post-processing logic once as middleware and apply it to multiple routes or groups of routes easily using
router.Use(...)
orgroup.Use(...)
. - Composability: Chain multiple, small middleware functions together to build complex request processing pipelines in a modular and maintainable way.
This pattern is fundamental to building web applications and APIs in Go.
Custom Middleware
You can easily create your own middleware by defining a function that matches the nova.Middleware
type signature:
type Middleware func(http.Handler) http.Handler
Here’s a simple example of a custom middleware that adds a custom header to every response:
// CustomHeaderMiddleware adds a "X-Custom-Header" to responses.
func CustomHeaderMiddleware(headerName, headerValue string) nova.Middleware {
// Return the actual middleware function
return func(next http.Handler) http.Handler {
// Return the HandlerFunc that wraps the next handler
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set the header before calling the next handler
w.Header().Set(headerName, headerValue)
// Call the next middleware or handler in the chain
next.ServeHTTP(w, r)
// Could also perform actions *after* the handler runs here
})
}
}
// And then somewhere else where you want to use it on a router/group/subrouter:
router.Use(CustomHeaderMiddleware("X-App-Version", "1.2.3"))
Built-in Middleware
Nova provides a collection of standard net/http
middleware. Each middleware is typically configured using a specific Config
struct and applied using router.Use(...)
for global application or group.Use(...)
for group-specific application.
LoggingMiddleware
- Description: Logs incoming requests (start) and outgoing responses (completion), including method, path, remote address, status code, response size, and duration.
- Configuration:
nova.LoggingConfig
Logger *log.Logger
: Logger instance (defaults tolog.Default()
).LogRequestID bool
: Include request ID in logs (defaults to true). RequiresRequestIDMiddleware
.RequestIDKey contextKey
: Context key for request ID (defaults to internal key).
Example
func main() {
router := nova.NewRouter()
customLogger := log.New(os.Stdout, "[ACCESS] ", log.LstdFlags)
// Apply logging middleware
router.Use(nova.LoggingMiddleware(&nova.LoggingConfig{
Logger: customLogger,
LogRequestID: true, // Optional: Explicitly true (default)
}))
// Apply RequestID middleware if LogRequestID is true
router.Use(nova.RequestIDMiddleware(nil)) // Use defaults
}
RecoveryMiddleware
- Description: Recovers from panics in downstream handlers/middleware, logs the panic, and sends a 500 Internal Server Error response (or calls a custom handler).
- Configuration:
nova.RecoveryConfig
Logger *log.Logger
: Logger for panic messages (defaults tolog.Default()
).LogRequestID bool
: Include request ID in panic logs (defaults to true).RequestIDKey contextKey
: Context key for request ID (defaults to internal key).RecoveryHandler func(http.ResponseWriter, *http.Request, interface{})
: Custom function to handle recovered panics.
Example
func customPanicHandler(w http.ResponseWriter, r *http.Request, err interface{}) {
reqID := nova.GetRequestID(r.Context()) // Get request ID if available
log.Printf("[PANIC RECOVERY][%s] Custom handler called: %v", reqID, err)
http.Error(w, "Something went terribly wrong!", http.StatusInternalServerError)
}
func main() {
router := nova.NewRouter()
// Apply RequestID first so Recovery can log it
router.Use(nova.RequestIDMiddleware(nil))
// Apply recovery middleware (must be early in the chain, so it can set the recover mechanism)
router.Use(nova.RecoveryMiddleware(&nova.RecoveryConfig{
RecoveryHandler: customPanicHandler,
LogRequestID: true,
}))
router.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
panic("Simulating a handler panic!")
})
router.Get("/safe", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This is fine."))
})
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", router)
}
RequestIDMiddleware
- Description: Assigns a unique ID to each request (from header or generated), sets it in the response header, and adds it to the request context.
- Configuration:
nova.RequestIDConfig
HeaderName string
: Header to check/set (defaults toX-Request-ID
).ContextKey contextKey
: Context key for storage (defaults to internal key).Generator func() string
: ID generation function (defaults to timestamp-based).
- Context Helper:
nova.GetRequestID(ctx context.Context)
retrieves the ID.
Example
func main() {
router := nova.NewRouter()
// Apply RequestID middleware globally
router.Use(nova.RequestIDMiddleware(&nova.RequestIDConfig{
HeaderName: "X-Trace-ID", // Custom header name
Generator: func() string { // Custom generator (e.g., UUID)
return uuid.NewString() // From github.com/google/uuid
},
}))
router.Get("/whoami", func(w http.ResponseWriter, r *http.Request) {
reqID := nova.GetRequestID(r.Context())
fmt.Fprintf(w, "Your request ID is: %s", reqID)
})
}
CORSMiddleware
- Description: Handles Cross-Origin Resource Sharing (CORS) by setting
Access-Control-*
headers and managing preflightOPTIONS
requests. - Configuration:
nova.CORSConfig
AllowedOrigins []string
: Allowed origin domains (e.g.,["http://localhost:3000"]
,["*"]
). Defaults to none.AllowedMethods []string
: Allowed HTTP methods (defaults to GET, POST, PUT, DELETE, PATCH, OPTIONS).AllowedHeaders []string
: Allowed request headers (defaults to Content-Type, Authorization, X-Request-ID)."*"
allows any.ExposedHeaders []string
: Response headers accessible to the client script. Defaults to none.AllowCredentials bool
: Allow cookies/auth headers with requests (defaults to false). Cannot be true ifAllowedOrigins
is["*"]
.MaxAgeSeconds int
: Cache duration for preflight results (defaults to 86400).
Example
func main() {
router := nova.NewRouter()
// Apply CORS middleware globally
// OPTIONS requests are handled automatically by the middleware
router.Use(nova.CORSMiddleware(nova.CORSConfig{
AllowedOrigins: []string{"http://localhost:3000", "https://my-frontend.com"},
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
ExposedHeaders: []string{"X-Custom-Response-Header"},
AllowCredentials: true,
MaxAgeSeconds: 3600, // 1 hour
}))
router.Get("/api/data", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Custom-Response-Header", "SomeValue")
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "Data from API"}`))
})
}
SecurityHeadersMiddleware
- Description: Sets various HTTP security headers (CSP, HSTS, X-Frame-Options, etc.) to enhance application security.
- Configuration:
nova.SecurityHeadersConfig
(all fields optional, have sensible defaults)ContentTypeOptions string
: Defaults tonosniff
.FrameOptions string
: Defaults toDENY
.XSSProtection string
: Defaults to1; mode=block
.ReferrerPolicy string
: Defaults tostrict-origin-when-cross-origin
.HSTSMaxAgeSeconds int
: Enables HSTS if > 0 (defaults to 0).HSTSIncludeSubdomains *bool
: Defaults to true if HSTS enabled.HSTSPreload bool
: Defaults to false.ContentSecurityPolicy string
: Defaults to""
. Important: Define a policy specific to your app.PermissionsPolicy string
: Defaults to""
.
Example
func main() {
router := nova.NewRouter()
hstsEnabled := true // Example: Enable HSTS
// Apply Security Headers middleware globally
router.Use(nova.SecurityHeadersMiddleware(nova.SecurityHeadersConfig{
// Example: Define a basic Content Security Policy
ContentSecurityPolicy: "default-src 'self'; script-src 'self'; object-src 'none';",
// Example: Enable HSTS for 1 year
HSTSMaxAgeSeconds: int(365 * 24 * time.Hour.Seconds()),
HSTSIncludeSubdomains: &hstsEnabled, // Explicitly enable (default if HSTSMaxAgeSeconds > 0)
// HSTSPreload: true, // Use with caution after submission
// PermissionsPolicy: "geolocation=(), microphone=()", // Example policy
}))
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<h1>Secure Page</h1>"))
})
}
TimeoutMiddleware
- Description: Enforces a maximum duration for request handling. Cancels the request context and returns 503 Service Unavailable (or calls custom handler) on timeout.
- Configuration:
nova.TimeoutConfig
Duration time.Duration
: Maximum processing time (required if used).TimeoutMessage string
: Response body on timeout (defaults to “Service timed out”).TimeoutHandler http.Handler
: Custom handler for timeout events.
Example
func main() {
router := nova.NewRouter()
// Apply Timeout middleware globally
router.Use(nova.TimeoutMiddleware(nova.TimeoutConfig{
Duration: 5 * time.Second, // Set a 5-second timeout
// TimeoutMessage: "Request took too long!", // Optional custom message
// TimeoutHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// http.Error(w, "Custom timeout response", http.StatusGatewayTimeout)
// }), // Optional custom handler
}))
router.Get("/slow", func(w http.ResponseWriter, r *http.Request) {
log.Println("Slow handler started...")
select {
case <-time.After(10 * time.Second): // Simulate work longer than timeout
log.Println("Slow handler finished (too late).")
w.Write([]byte("Finished slow task."))
case <-r.Context().Done(): // Context cancelled by timeout middleware
log.Println("Slow handler cancelled by timeout.")
// Note: Cannot write response here, middleware handles it.
return
}
})
router.Get("/fast", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Finished quickly."))
})
}
BasicAuthMiddleware
- Description: Protects routes using HTTP Basic Authentication, validating credentials with a provided function.
- Configuration:
nova.BasicAuthConfig
Realm string
: Authentication realm (defaults to “Restricted”).Validator AuthValidator
: Functionfunc(user, pass string) bool
(required).StoreUserInContext bool
: Store authenticated user in context (defaults to false).ContextKey contextKey
: Context key for user (defaults to internal key).
- Context Helper:
nova.GetBasicAuthUser(ctx context.Context)
retrieves the user if stored.
Example
// Simple validator function (replace with real logic)
func myAuthValidator(username, password string) bool {
// WARNING: Hardcoded credentials are insecure! Use a proper check.
return username == "admin" && password == "password123"
}
func main() {
router := nova.NewRouter()
// Apply Basic Auth middleware globally or to a group
authMiddleware := nova.BasicAuthMiddleware(nova.BasicAuthConfig{
Validator: myAuthValidator,
Realm: "My Protected Area",
StoreUserInContext: true, // Store the username
})
router.Use(authMiddleware) // Apply globally
// Alternatively, apply to a group:
// adminGroup := router.Group("/admin")
// adminGroup.Use(authMiddleware)
// adminGroup.Get("/dashboard", ...)
router.Get("/secure", func(w http.ResponseWriter, r *http.Request) {
user := nova.GetBasicAuthUser(r.Context())
fmt.Fprintf(w, "Welcome, authenticated user: %s", user)
})
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", router)
}
MethodOverrideMiddleware
- Description: Allows overriding the HTTP method via a header (
X-HTTP-Method-Override
) or form field (_method
for POST requests). - Configuration:
nova.MethodOverrideConfig
HeaderName string
: Header to check (defaults toX-HTTP-Method-Override
).FormFieldName string
: Form field to check (defaults to_method
). Set to""
to disable form field check.
Example
func main() {
router := nova.NewRouter()
// Apply Method Override middleware globally
router.Use(nova.MethodOverrideMiddleware(nil)) // Use defaults
// Handler that might receive overridden methods
router.Post("/resource", func(w http.ResponseWriter, r *http.Request) {
// r.Method will be the overridden method (e.g., PUT, DELETE)
fmt.Fprintf(w, "Handling resource with method: %s", r.Method)
})
// Need corresponding handlers for the methods you expect to be overridden
router.Put("/resource", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Handling resource with method: PUT (via override or direct)")
})
router.Delete("/resource", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Handling resource with method: DELETE (via override or direct)")
})
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", router)
}
// Example client request using header:
// curl -X POST -H "X-HTTP-Method-Override: DELETE" http://localhost:8080/resource
// Example client request using form field (HTML form):
// <form action="/resource" method="POST">
// <input type="hidden" name="_method" value="PUT">
// <button type="submit">Update Resource</button>
// </form>
EnforceContentTypeMiddleware
- Description: Ensures requests for specific methods (POST, PUT, PATCH by default) have a
Content-Type
header matching an allowed list. - Configuration:
nova.EnforceContentTypeConfig
AllowedTypes []string
: List of allowed Content-Types (e.g.,["application/json"]
) (required).MethodsToCheck []string
: Methods to check (defaults to POST, PUT, PATCH).OnError func(w http.ResponseWriter, r *http.Request, err error)
: Custom error handler.
Example
func main() {
router := nova.NewRouter()
// Apply Content-Type enforcement globally or to relevant groups/routes
router.Use(nova.EnforceContentTypeMiddleware(nova.EnforceContentTypeConfig{
AllowedTypes: []string{"application/json", "application/xml"},
// MethodsToCheck: []string{"POST"}, // Optional: Only check POST
}))
router.Post("/submit", func(w http.ResponseWriter, r *http.Request) {
// Handler logic assumes Content-Type is valid here
w.Write([]byte("Data submitted successfully."))
})
router.Get("/fetch", func(w http.ResponseWriter, r *http.Request) {
// GET requests are not checked by default
w.Write([]byte("Data fetched."))
})
}
// Example failing request:
// curl -X POST -d 'data' http://localhost:8080/submit
// -> 400 Bad Request (Missing Content-Type)
// Example failing request:
// curl -X POST -H "Content-Type: text/plain" -d 'data' http://localhost:8080/submit
// -> 415 Unsupported Media Type
// Example successful request:
// curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' http://localhost:8080/submit
// -> 200 OK
CacheControlMiddleware
- Description: Sets the
Cache-Control
header on all responses. - Configuration:
nova.CacheControlConfig
CacheControlValue string
: The value for the header (e.g., “no-store”) (required).
Example
func main() {
router := nova.NewRouter()
// Apply Cache-Control middleware globally
router.Use(nova.CacheControlMiddleware(nova.CacheControlConfig{
CacheControlValue: "no-store, no-cache, must-revalidate",
}))
router.Get("/api/status", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
}
GzipMiddleware
- Description: Compresses response bodies using gzip if the client supports it (
Accept-Encoding: gzip
). Uses async.Pool
forgzip.Writer
reuse to improve performance. - Configuration:
nova.GzipConfig
CompressionLevel int
: Gzip level (e.g.,gzip.BestSpeed
,gzip.DefaultCompression
,gzip.BestCompression
). Defaults togzip.DefaultCompression
(-1).AddVaryHeader *bool
: AddsVary: Accept-Encoding
header. Defaults totrue
. Usenew(bool)
to set explicitly (e.g.,AddVaryHeader: new(bool) // false
).Logger *log.Logger
: Optional logger for errors. Defaults tolog.Default()
.Pool *sync.Pool
: Optionalsync.Pool
forgzip.Writer
reuse. Defaults to an internal pool.
Example
func main() {
router := nova.NewRouter()
// Custom pool example (optional)
gzipPool := &sync.Pool{
New: func() interface{} {
gw, _ := gzip.NewWriterLevel(io.Discard, gzip.BestSpeed)
return gw
},
}
// Apply Gzip middleware globally
router.Use(nova.GzipMiddleware(&nova.GzipConfig{
CompressionLevel: gzip.BestSpeed, // Optional: Prioritize speed
// AddVaryHeader: func() *bool { b := false; return &b }(), // Explicitly disable Vary
Pool: gzipPool, // Optional: Use custom pool
}))
router.Get("/large-data", func(w http.ResponseWriter, r *http.Request) {
// Simulate a large response
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
for i := 0; i < 1000; i++ {
w.Write([]byte("This is line number "))
w.Write([]byte(http.StatusText(i + 200)))
w.Write([]byte("\n"))
}
}
// Example request:
// curl -H "Accept-Encoding: gzip" http://localhost:8080/large-data --output - | gunzip
CSRFMiddleware
- Description: Provides Cross-Site Request Forgery (CSRF) protection using the Double Submit Cookie pattern. It sets a secure, HttpOnly cookie and expects a matching token in a header or form field for unsafe HTTP methods (POST, PUT, DELETE, etc.).
- Configuration:
nova.CSRFConfig
Logger *log.Logger
: Optional logger. Defaults tolog.Default()
.FieldName string
: Form field name for the token. Defaults to"csrf_token"
.HeaderName string
: HTTP header name for the token. Defaults to"X-CSRF-Token"
.CookieName string
: Name of the HttpOnly cookie storing the secret. Defaults to"_csrf"
.ContextKey contextKey
: Context key to store the expected token. Defaults to internal package key.ErrorHandler http.HandlerFunc
: Handler called on CSRF failure. Defaults to 403 Forbidden.CookiePath string
: Path for the CSRF cookie. Defaults to"/"
.CookieDomain string
: Domain for the CSRF cookie. Defaults to""
.CookieMaxAge time.Duration
: Max age of the cookie. Defaults to 12 hours.CookieSecure bool
: Secure flag for the cookie (requires HTTPS). Defaults tofalse
. Set totrue
in production.CookieSameSite http.SameSite
: SameSite attribute. Defaults tohttp.SameSiteLaxMode
.TokenLength int
: Byte length of the generated token. Defaults to 32.SkipMethods []string
: HTTP methods exempt from checks. Defaults to["GET", "HEAD", "OPTIONS", "TRACE"]
.
Example
// Simple template to include CSRF token
var formTmpl = template.Must(template.New("form").Parse(`
<!DOCTYPE html>
<html>
<body>
<h2>CSRF Demo Form</h2>
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{{.}}">
<label for="data">Data:</label>
<input type="text" id="data" name="data">
<button type="submit">Submit</button>
</form>
</body>
</html>
`))
func main() {
router := nova.NewRouter()
// Apply CSRF middleware
router.Use(nova.CSRFMiddleware(&nova.CSRFConfig{
CookieSecure: false, // Set to true if using HTTPS
CookieSameSite: http.SameSiteStrictMode, // Often preferred
// ErrorHandler: func(w http.ResponseWriter, r *http.Request) { ... }, // Custom error
}))
// Handler to display the form
router.Get("/form", func(w http.ResponseWriter, r *http.Request) {
// Get the token set by the middleware via context helper
csrfToken := nova.GetCSRFToken(r.Context())
w.Header().Set("Content-Type", "text/html")
formTmpl.Execute(w, csrfToken) // Pass token to template
})
// Handler to process the form submission
router.Post("/submit", func(w http.ResponseWriter, r *http.Request) {
data := r.FormValue("data")
fmt.Fprintf(w, "Received data: %s", data)
})
}
// Example request (simulating a valid POST after getting the form):
// 1. Get form to get cookie and token: curl -c cookies.txt http://localhost:8080/form
// 2. Extract token from HTML output (e.g., TOKEN_VALUE)
// 3. Make POST request with cookie and token header:
// curl -b cookies.txt -X POST -H "X-CSRF-Token: TOKEN_VALUE" -d "data=hello" http://localhost:8080/submit
// Example request (simulating a failed POST - missing token):
// curl -b cookies.txt -X POST -d "data=hello" http://localhost:8080/submit
// Response: Forbidden
ETagMiddleware
- Description: Adds an
ETag
header to successful responses based on a hash of the response body. HandlesIf-None-Match
conditional requests, potentially returning a304 Not Modified
status without the response body if the client’s cached ETag matches. Note: This middleware buffers the entire response body in memory to calculate the hash, which may be unsuitable for very large responses. - Configuration:
nova.ETagConfig
Weak bool
: Generate weak ETags (prefixed withW/
). Defaults tofalse
(strong ETags).SkipNoContent bool
: Skip ETag generation/checking for204 No Content
responses. Defaults totrue
.
Example
var lastModified = time.Now()
var responseBody = "Initial content"
func main() {
router := nova.NewRouter()
// Apply ETag middleware
router.Use(nova.ETagMiddleware(&nova.ETagConfig{
// Weak: true, // Optional: Use weak ETags
}))
router.Get("/content", func(w http.ResponseWriter, r *http.Request) {
// Simulate content that might change
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Cache-Control", "max-age=60") // Advise caching
fmt.Fprint(w, responseBody)
})
// Example route to change the content
router.Get("/update", func(w http.ResponseWriter, r *http.Request) {
responseBody = fmt.Sprintf("Content updated at %s", time.Now())
lastModified = time.Now()
fmt.Fprint(w, "Content updated. Try /content again.")
})
}
// Example requests:
// 1. First request: curl -v http://localhost:8080/content
// (Note the ETag header in the response, e.g., ETag: "HASH_VALUE")
// 2. Second request (client sends If-None-Match):
// curl -v -H 'If-None-Match: "HASH_VALUE"' http://localhost:8080/content
// (Response should be 304 Not Modified with an empty body)
// 3. Update content: curl http://localhost:8080/update
// 4. Request again with old ETag:
// curl -v -H 'If-None-Match: "HASH_VALUE"' http://localhost:8080/content
// (Response should be 200 OK with the new content and a *new* ETag)
HealthCheckMiddleware
- Description: Provides a dedicated health check endpoint (e.g.,
/healthz
). Requests to this path are handled directly by the middleware (returning a status, often 200 OK), bypassing subsequent middleware and application handlers. Useful for load balancers and monitoring systems. - Configuration:
nova.HealthCheckConfig
Path string
: The URL path for the health check endpoint. Defaults to"/healthz"
.Handler http.HandlerFunc
: The handler function to execute for the health check. If nil, a default handler returning 200 OK with “OK” body is used. Can be customized to check database connections, etc.
Example
// Example custom health check handler
func customHealthHandler(w http.ResponseWriter, r *http.Request) {
// Simulate checking a dependency (e.g., database)
time.Sleep(10 * time.Millisecond)
isHealthy := true // Replace with actual check logic
if isHealthy {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{"status": "UP"}`)
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintln(w, `{"status": "DOWN", "reason": "database connection failed"}`)
}
}
func main() {
router := nova.NewRouter()
// Apply HealthCheck middleware (often placed early, but after recovery/logging if needed)
router.Use(nova.HealthCheckMiddleware(&nova.HealthCheckConfig{
Path: "/status", // Optional: Custom path
Handler: customHealthHandler, // Optional: Custom check logic
}))
// Other middleware and routes...
router.Use(nova.LoggingMiddleware(nil)) // Example: Log other requests
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Main application page")
})
}
// Example requests:
// curl http://localhost:8080/status
// Response (with custom handler): {"status": "UP"}
// curl http://localhost:8080/
// Response: Main application page (Logs will show this request, but not the /status request if logging is after health check)
RealIPMiddleware
- Description: Extracts the client’s real IP address from trusted proxy headers (e.g.,
X-Forwarded-For
,X-Real-IP
). Warning: Only use behind a trusted proxy. - Configuration:
nova.RealIPConfig
TrustedProxyCIDRs []string
: CIDR ranges of trusted proxies (e.g.,["10.0.0.0/8", "192.168.1.1/32"]
). Required for header trusting.IPHeaders []string
: Headers to check in order (defaults toX-Forwarded-For
,X-Real-IP
).StoreInContext bool
: Store the real IP in context (defaults to true).ContextKey contextKey
: Context key for IP (defaults to internal key).
- Context Helper:
nova.GetRealIP(ctx context.Context)
retrieves the IP. - Usage:
func main() {
router := nova.NewRouter()
// Apply RealIP middleware globally (ensure it runs early)
router.Use(nova.RealIPMiddleware(nova.RealIPConfig{
// IMPORTANT: Only list CIDRs of proxies you TRUST
TrustedProxyCIDRs: []string{"127.0.0.1/32", "::1/128"}, // Example: Trust localhost proxy
IPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, // Default
StoreInContext: true, // Default
}))
// Apply Logging middleware *after* RealIP so logs show the real IP
router.Use(nova.LoggingMiddleware(nil))
router.Get("/ip", func(w http.ResponseWriter, r *http.Request) {
realIP := nova.GetRealIP(r.Context())
// r.RemoteAddr might also be updated (with port 0) if IP found via header
fmt.Fprintf(w, "Your detected IP: %s\n", realIP)
fmt.Fprintf(w, "Request RemoteAddr: %s\n", r.RemoteAddr)
})
}
// Example request (simulating proxy):
// curl -H "X-Forwarded-For: 1.2.3.4" http://localhost:8080/ip
MaxRequestBodySizeMiddleware
- Description: Limits the size of incoming request bodies using
http.MaxBytesReader
. - Configuration:
nova.MaxRequestBodySizeConfig
LimitBytes int64
: Maximum body size in bytes (required).OnError func(w http.ResponseWriter, r *http.Request)
: Custom error handler (defaults to 413 response).
Example
func main() {
router := nova.NewRouter()
// Apply Max Body Size middleware globally or to upload routes
router.Use(nova.MaxRequestBodySizeMiddleware(nova.MaxRequestBodySizeConfig{
LimitBytes: 1 * 1024 * 1024, // 1 MB limit
// OnError: func(w http.ResponseWriter, r *http.Request) {
// http.Error(w, "Request body too large!", http.StatusRequestEntityTooLarge)
// }, // Optional custom handler
}))
router.Post("/upload", func(w http.ResponseWriter, r *http.Request) {
// Attempt to read the body. MaxBytesReader will return an error
// if the limit is exceeded during the read.
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
// Error might be due to size limit or other read issues.
// The middleware's default OnError usually handles the 413 response
// before the handler even gets here if ContentLength is too large.
// If reading fails *during* the stream due to limit, this handler sees error.
log.Printf("Error reading body: %v", err)
// Check if it was a MaxBytesError (though http package might not export it easily)
// A simple check:
if r.ContentLength == -1 { // If ContentLength wasn't known beforehand
// Assume error might be due to limit exceeded during read
// The http.MaxBytesReader already wrote the 413 error response
return
}
// Otherwise, handle other potential read errors
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
log.Printf("Received %d bytes", len(bodyBytes))
w.Write([]byte("Upload received successfully."))
})
log.Println("Starting server on :8080")
http.ListenAndServe(":8080", router)
}
// Example request exceeding limit:
// curl -X POST --data-binary @large_file.dat http://localhost:8080/upload
// -> 413 Request Entity Too Large
TrailingSlashRedirectMiddleware
- Description: Redirects requests to add or remove a trailing slash from the URL path for consistency.
- Configuration:
nova.TrailingSlashRedirectConfig
AddSlash bool
: Enforce trailing slash (defaults to false - removes slash).RedirectCode int
: HTTP redirect status code (defaults to 301). Use 308 for POST/PUT etc. to preserve method.
Example
func main() {
router := nova.NewRouter()
// Apply Trailing Slash middleware globally (usually early)
router.Use(nova.TrailingSlashRedirectMiddleware(nova.TrailingSlashRedirectConfig{
AddSlash: false, // Default: remove trailing slash
RedirectCode: http.StatusMovedPermanently, // Default: 301
// Or to enforce slashes:
// AddSlash: true,
// RedirectCode: http.StatusPermanentRedirect, // 308 to preserve method
}))
// Define routes WITHOUT the trailing slash (if AddSlash is false)
router.Get("/users", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("List of users"))
})
router.Get("/products", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("List of products"))
})
}
// Example request:
// curl -L http://localhost:8080/users/
// -> Redirects (301) to http://localhost:8080/users
// -> Responds with "List of users"
ForceHTTPSMiddleware
- Description: Redirects incoming HTTP requests to their HTTPS equivalent.
- Configuration:
nova.ForceHTTPSConfig
TargetHost string
: Override host in redirect URL (defaults to request host).TargetPort int
: Override port in redirect URL (defaults to standard 443).RedirectCode int
: Redirect status code (defaults to 301).ForwardedProtoHeader string
: Header to check for original protocol (defaults toX-Forwarded-Proto
).TrustForwardedHeader *bool
: Trust the forwarded header (defaults to true). Set false if proxy doesn’t set it reliably.
Example
func main() {
router := nova.NewRouter()
// Apply Force HTTPS middleware globally (very early)
trustHeader := true
router.Use(nova.ForceHTTPSMiddleware(nova.ForceHTTPSConfig{
// RedirectCode: http.StatusPermanentRedirect, // Use 308 if needed
// ForwardedProtoHeader: "X-Scheme", // If your proxy uses a different header
TrustForwardedHeader: &trustHeader, // Default is true
}))
// Add other middleware like HSTS *after* ForceHTTPS potentially
router.Use(nova.SecurityHeadersMiddleware(nova.SecurityHeadersConfig{
HSTSMaxAgeSeconds: 31536000, // Example: 1 year HSTS
}))
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome to the secure site!"))
})
}
// Example request:
// curl http://localhost:8080
// -> Redirects (301) to https://localhost:8080 (or https://localhost if port 443)
ConcurrencyLimiterMiddleware
- Description: Limits the number of requests processed concurrently using a semaphore.
- Configuration:
nova.ConcurrencyLimiterConfig
MaxConcurrent int
: Max concurrent requests (required).WaitTimeout time.Duration
: Max time to wait for a slot (0 = wait forever).OnLimitExceeded func(w http.ResponseWriter, r *http.Request)
: Custom handler for limit exceeded (defaults to 503).
Example
func main() {
router := nova.NewRouter()
// Apply Concurrency Limiter middleware globally or to heavy routes
router.Use(nova.ConcurrencyLimiterMiddleware(nova.ConcurrencyLimiterConfig{
MaxConcurrent: 10, // Allow only 10 requests at a time
WaitTimeout: 2 * time.Second, // Wait max 2s for a slot
// OnLimitExceeded: func(w http.ResponseWriter, r *http.Request) {
// http.Error(w, "Too busy, try later", http.StatusServiceUnavailable)
// }, // Optional custom handler
}))
router.Get("/process", func(w http.ResponseWriter, r *http.Request) {
log.Println("Processing request...")
time.Sleep(5 * time.Second) // Simulate work
log.Println("Finished processing.")
w.Write([]byte("Processing complete."))
})
}
// Example: Run 15 concurrent requests:
// for i in {1..15}; do curl http://localhost:8080/process & done
// -> First 10 start immediately, next ~5 wait up to 2s. Some might get 503.
MaintenanceModeMiddleware
- Description: Returns 503 Service Unavailable if enabled via an atomic flag, allowing bypass for specified IPs/CIDRs.
- Configuration:
nova.MaintenanceModeConfig
EnabledFlag *atomic.Bool
: Pointer to the control flag (required).AllowedIPs []string
: IPs/CIDRs that bypass maintenance (e.g.,["192.168.1.100", "10.0.0.0/8"]
).StatusCode int
: Status code during maintenance (defaults to 503).RetryAfterSeconds int
: Value forRetry-After
header (defaults to 300).Message string
: Response body during maintenance.Logger *log.Logger
: Logger for errors (defaults tolog.Default()
).
Example
var maintenanceEnabled atomic.Bool // The control flag
func main() {
router := nova.NewRouter()
// Set initial state (e.g., false = not in maintenance)
maintenanceEnabled.Store(false)
// Apply Maintenance Mode middleware globally (very early)
router.Use(nova.MaintenanceModeMiddleware(nova.MaintenanceModeConfig{
EnabledFlag: &maintenanceEnabled,
AllowedIPs: []string{"127.0.0.1", "::1"}, // Allow localhost bypass
RetryAfterSeconds: 600, // 10 minutes
Message: "Down for scheduled maintenance. Please try again later.",
}))
// Example route to toggle maintenance mode (in real app, use signals or admin API)
router.Get("/admin/maintenance/on", func(w http.ResponseWriter, r *http.Request) {
maintenanceEnabled.Store(true)
w.Write([]byte("Maintenance mode ENABLED"))
log.Println("Maintenance mode ENABLED")
})
router.Get("/admin/maintenance/off", func(w http.ResponseWriter, r *http.Request) {
maintenanceEnabled.Store(false)
w.Write([]byte("Maintenance mode DISABLED"))
log.Println("Maintenance mode DISABLED")
})
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Application is running normally."))
})
}
// Example:
// curl http://localhost:8080/ -> Shows normal page
// curl http://localhost:8080/admin/maintenance/on -> Enables maintenance
// curl http://<your_external_ip>:8080/ -> Shows 503 maintenance page
// curl http://localhost:8080/ -> Still shows normal page (due to AllowedIPs)
// curl http://localhost:8080/admin/maintenance/off -> Disables maintenance
IPFilterMiddleware
- Description: Restricts access based on client IP using allow/block lists (CIDR supported).
- Configuration:
nova.IPFilterConfig
AllowedIPs []string
: Allowed IPs/CIDRs.BlockedIPs []string
: Blocked IPs/CIDRs (takes precedence).BlockByDefault bool
: Block IPs not matching any list (defaults to false - allow unless blocked).OnForbidden func(w http.ResponseWriter, r *http.Request)
: Custom handler for forbidden IPs (defaults to 403).Logger *log.Logger
: Logger for errors (defaults tolog.Default()
).
Example
func main() {
router := nova.NewRouter()
// Apply IP Filter middleware globally or to specific areas
router.Use(nova.IPFilterMiddleware(nova.IPFilterConfig{
// Example 1: Allow only specific IPs/ranges
// AllowedIPs: []string{"192.168.1.0/24", "10.0.0.5"},
// BlockByDefault: true, // Block anything not in AllowedIPs
// Example 2: Block specific IPs, allow others
BlockedIPs: []string{"1.2.3.4", "5.6.7.0/24"},
BlockByDefault: false, // Default: Allow unless explicitly blocked
// OnForbidden: func(w http.ResponseWriter, r *http.Request) {
// http.Error(w, "Access denied from your IP.", http.StatusForbidden)
// }, // Optional custom handler
}))
router.Get("/sensitive-data", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("This is sensitive information."))
})
}
RateLimitMiddleware
- Description: Simple in-memory rate limiting using a token bucket algorithm per client IP (by default). Warning: Basic, single-instance only, potential memory growth without cleanup.
- Configuration:
nova.RateLimiterConfig
Requests int
: Max requests per duration (required).Duration time.Duration
: Time window (required).Burst int
: Allowed burst size (defaults toRequests
).KeyFunc func(r *http.Request) string
: Function to get client key (defaults to IP).OnLimitExceeded func(w http.ResponseWriter, r *http.Request)
: Custom handler for limit (defaults to 429).CleanupInterval time.Duration
: How often to clean old entries (0 = no cleanup).Logger *log.Logger
: Logger for errors (defaults tolog.Default()
).
Example
func main() {
router := nova.NewRouter()
// Apply Rate Limiter middleware globally or to specific APIs
router.Use(nova.RateLimitMiddleware(nova.RateLimiterConfig{
Requests: 5, // Allow 5 requests...
Duration: 1 * time.Minute, // ...per minute
Burst: 10, // Allow initial burst of 10 requests
// KeyFunc: func(r *http.Request) string { // Optional: Limit by API key header
// key := r.Header.Get("X-API-Key")
// if key == "" { return r.RemoteAddr } // Fallback to IP if no key
// return key
// },
CleanupInterval: 10 * time.Minute, // Clean up old entries every 10 mins
// OnLimitExceeded: func(w http.ResponseWriter, r *http.Request) {
// w.WriteHeader(http.StatusTooManyRequests)
// w.Write([]byte("Rate limit exceeded. Please try again later."))
// }, // Optional custom handler
}))
router.Get("/api/resource", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Resource data"))
})
}
// Example: Hit the endpoint repeatedly
// for i in {1..15}; do curl -I http://localhost:8080/api/resource; sleep 0.1; done
// -> First ~10 requests get 200 OK, subsequent ones get 429 Too Many Requests