Middleware

Production-ready middleware for CORS, compression, security headers, logging, panic recovery, and more.

Overview

Middleware in ALOS wraps handlers to add pre/post processing. The signature is:

go
type MiddlewareFunc func(HandlerFunc) HandlerFunc

Apply middleware globally with Use() or per-group:

go
s.Router.Use(core.Recovery())
s.Router.Use(core.Logger())

api := s.Router.Group("/api", core.RequestID())

s.Router.Build()

Middleware executes in the order registered. The chain is pre-computed at build time for zero per-request overhead.

Built-in Middleware

Recovery

Catches panics and returns 500 with stack trace logging.

Logger

Logs method, path, status code, and response time.

CORS

Cross-origin resource sharing with hot-reload config.

Security Headers

HSTS, X-Frame-Options, XSS protection, CSP referrer.

Compress

gzip/deflate response compression with min-size threshold.

Real IP

Extracts client IP from X-Forwarded-For / X-Real-IP.

Request ID

Adds a unique X-Request-ID header to every request.

Timeout

Aborts handlers after a deadline and returns 504.

Body Limit

Rejects request bodies exceeding a byte threshold (413).

Allow Methods

Restricts routes to specific HTTP methods (405).

Basic Auth

HTTP Basic Authentication with constant-time comparison.

Header

Injects a static response header on every request.

If (Conditional)

Apply middleware only when a predicate returns true.

Recovery

Catches panics in handlers and returns a 500 Internal Server Error instead of crashing the server:

go
s.Router.Use(core.Recovery())

s.Router.GET("/panic", func(req *core.Request, resp *core.Response) {
    panic("something went wrong")
})

Always register Recovery() first. It must wrap all other middleware to catch panics from any layer in the chain.

Logger

Logs each request with method, path, status code, and duration:

go
s.Router.Use(core.Logger())

CORS

ALOS provides a runtime-swappable CORS engine. Configure origins, methods, and headers:

go
s.SetCORS(core.CORSConfig{
    AllowOrigins:     []string{"https://app.example.com", "https://admin.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE"},
    AllowHeaders:     []string{"Content-Type", "Authorization"},
    ExposeHeaders:    []string{"X-Request-ID"},
    AllowCredentials: true,
    MaxAge:           3600,
})

s.Router.Use(s.CORS.Middleware())

Hot-Reload CORS

Update CORS configuration at runtime without restarting. Uses lock-free atomic snapshots:

go
s.CORS.Update(core.CORSConfig{
    AllowOrigins: []string{"*"},
    AllowMethods: []string{"GET"},
})

Configuration Options

Option Type Description
AllowOrigins []string Allowed origins. Use "*" for all.
AllowMethods []string Allowed HTTP methods.
AllowHeaders []string Allowed request headers.
ExposeHeaders []string Headers visible to the browser.
AllowCredentials bool Allow cookies/auth headers.
MaxAge int Preflight cache duration (seconds).

Security Headers

Add essential security headers to all responses:

go
s.Router.Use(core.SecurityHeaders(core.DefaultSecurityHeaders()))

s.Router.Use(core.SecurityHeaders(core.SecurityHeadersConfig{
    ContentTypeNosniff: true,
    XFrameOptions:      "DENY",
    XSSProtection:      true,
    HSTSMaxAge:         63072000,
    HSTSSubdomains:     true,
    HSTSPreload:        true,
    ReferrerPolicy:     "strict-origin-when-cross-origin",
}))

This adds the following headers to every response:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection: 1; mode=block
  • Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  • Referrer-Policy: strict-origin-when-cross-origin

Compression

Automatically compress responses with gzip or deflate based on Accept-Encoding:

go
s.Router.Use(core.Compress(core.CompressConfig{
    Level:   6,
    MinSize: 512,
}))

Responses below MinSize bytes are sent uncompressed to avoid overhead on small payloads.

Timeout

Aborts handler execution after a deadline and returns 504 Gateway Timeout:

go
s.Router.Use(core.Timeout(5 * time.Second))

Apply different timeouts per route group:

go
api := s.Router.Group("/api", core.Timeout(10 * time.Second))
uploads := s.Router.Group("/upload", core.Timeout(60 * time.Second))

Timeout applies to the total handler execution time, not to request reading or response writing.

Body Limit

Rejects requests with bodies larger than the specified byte limit with 413 Request Entity Too Large:

go
s.Router.Use(core.BodyLimit(10 * 1024 * 1024))

Different limits per group:

go
api := s.Router.Group("/api", core.BodyLimit(1 * 1024 * 1024))
uploads := s.Router.Group("/upload", core.BodyLimit(100 * 1024 * 1024))

Allow Methods

Restricts a route group to specific HTTP methods. Returns 405 Method Not Allowed for anything else:

go
readOnly := s.Router.Group("/public",
    core.AllowMethods("GET", "HEAD"),
)

readOnly.GET("/info", infoHandler)
readOnly.GET("/status", statusHandler)

Combine with other middleware:

go
admin := s.Router.Group("/admin",
    core.AllowMethods("GET", "POST"),
    core.BasicAuth(core.BasicAuthConfig{
        Users: map[string]string{"admin": "secret"},
    }),
)

Basic Auth

HTTP Basic Authentication. Uses constant-time password comparison to prevent timing attacks. Returns 401 with WWW-Authenticate header on failure:

go
s.Router.Use(core.BasicAuth(core.BasicAuthConfig{
    Users: map[string]string{
        "admin":     "supersecret",
        "readonly":  "viewonly",
    },
    Realm: "Admin Panel",
}))

Protect only specific groups:

go
admin := s.Router.Group("/admin", core.BasicAuth(core.BasicAuthConfig{
    Users: map[string]string{"admin": "password"},
    Realm: "Admin",
}))

admin.GET("/dashboard", dashboardHandler)
admin.POST("/settings", settingsHandler)
Field Type Description
Users map[string]string Username → password pairs
Realm string Realm shown in the browser prompt

Real IP

When behind a reverse proxy, extract the real client IP from proxy headers:

go
s.Router.Use(core.RealIP())

s.Router.GET("/ip", func(req *core.Request, resp *core.Response) {
    resp.String(req.RemoteAddr)
})

Checks X-Forwarded-For and X-Real-IP headers (in that order).

Request ID

Adds a unique X-Request-ID header to every request for distributed tracing:

go
s.Router.Use(core.RequestID())

s.Router.GET("/debug", func(req *core.Request, resp *core.Response) {
    id := req.Header("X-Request-ID")
    resp.String(id)
})

Conditional (If)

Apply middleware only when a predicate function returns true. The inner middleware is skipped entirely when the predicate is false:

go
s.Router.Use(core.If(
    func(req *core.Request) bool {
        return req.Path != "/health"
    },
    core.Logger(),
))

Skip auth for public routes:

go
s.Router.Use(core.If(
    func(req *core.Request) bool {
        return req.Header("Authorization") != ""
    },
    AuthRequired(),
))

Compress only JSON responses:

go
s.Router.Use(core.If(
    func(req *core.Request) bool {
        return req.Header("Accept") == "application/json"
    },
    core.Compress(core.CompressConfig{Level: 6, MinSize: 256}),
))

Chain Helper

Compose a handler with specific middleware without registering them globally. Useful for per-route middleware:

go
s.Router.GET("/admin/stats", core.Chain(
    statsHandler,
    AuthRequired(),
    core.Timeout(5 * time.Second),
))

This applies AuthRequired and Timeout only to the /admin/stats route, without affecting other routes. The middleware is applied in the order listed (outermost first).

go
s.Router.POST("/upload", core.Chain(
    uploadHandler,
    core.BodyLimit(50 * 1024 * 1024),
    core.Timeout(120 * time.Second),
))

s.Router.GET("/metrics", core.Chain(
    metricsHandler,
    core.BasicAuth(core.BasicAuthConfig{
        Users: map[string]string{"ops": "secret"},
    }),
))

Custom Middleware

Write your own middleware by returning a function that wraps the next handler:

go
func AuthRequired() core.MiddlewareFunc {
    return func(next core.HandlerFunc) core.HandlerFunc {
        return func(req *core.Request, resp *core.Response) {
            token := req.Header("Authorization")
            if token == "" {
                resp.Status(401).JSON([]byte(`{"error":"unauthorized"}`))
                return
            }

            next(req, resp)
        }
    }
}

api := s.Router.Group("/api", AuthRequired())

Timing Middleware Example

go
func Timer() core.MiddlewareFunc {
    return func(next core.HandlerFunc) core.HandlerFunc {
        return func(req *core.Request, resp *core.Response) {
            start := time.Now()
            next(req, resp)
            elapsed := time.Since(start)
            resp.SetHeader("X-Response-Time", elapsed.String())
        }
    }
}
ESC