Documentation
¶
Overview ¶
Package middlewares is a set of production-ready HTTP middleware for the standard net/http. Every middleware has the ordinary shape func(http.Handler) http.Handler, so it works with any router: the standard http.ServeMux, the goloop/mux router, or hand-written handlers.
It is not a framework. It closes the common cross-cutting needs that the standard library leaves out - request identifiers, real client IP, panic recovery, request logging, timeouts, response compression, concurrency throttling, CORS and security headers - with no third-party dependencies.
Compose middleware with Chain, or apply them one-shot with Handler:
h := middlewares.Chain(
middlewares.RequestID(),
middlewares.RealIP(),
middlewares.Recoverer(),
middlewares.Logger(),
middlewares.Compress(),
)(mux)
http.ListenAndServe(":8080", h)
Logging uses the standard log/slog. Pass any *slog.Logger through the relevant option; the package never imports a concrete logging backend.
Security notes worth reading before production use:
- RealIP trusts no proxy headers by default; configure trusted proxies explicitly, or a client can spoof its address.
- Timeout cancels the request context but cannot stop a handler goroutine that ignores it.
- Recoverer cannot change a response that was already committed; it only logs the panic in that case.
- Compress never touches text/event-stream, already-encoded responses, or bodies below the minimum size.
- CORS allows nothing until you name the origins; wildcard origin together with credentials is rejected at construction.
Index ¶
- Constants
- func Handler(next http.Handler, m ...Middleware) http.Handler
- func RealIPFrom(ctx context.Context) string
- func RequestIDFrom(ctx context.Context) string
- type CORSOption
- type CompressOption
- type LoggerOption
- type Middleware
- func CORS(opts ...CORSOption) Middleware
- func Chain(m ...Middleware) Middleware
- func Compress(opts ...CompressOption) Middleware
- func Logger(opts ...LoggerOption) Middleware
- func MaxBytes(n int64) Middleware
- func RealIP(opts ...RealIPOption) Middleware
- func Recoverer(opts ...RecovererOption) Middleware
- func RequestID(opts ...RequestIDOption) Middleware
- func SecurityHeaders(opts ...SecurityOption) Middleware
- func Throttle(limit int, opts ...ThrottleOption) Middleware
- func Timeout(d time.Duration, opts ...TimeoutOption) Middleware
- type RealIPOption
- type RecovererOption
- type RequestIDOption
- type SecurityOption
- type ThrottleOption
- type TimeoutOption
Examples ¶
Constants ¶
const DefaultCompressMinBytes = 1024
DefaultCompressMinBytes is the smallest response, in bytes, that Compress will gzip. Smaller bodies are sent as-is, since compression would not help.
const DefaultRequestIDHeader = "X-Request-ID"
DefaultRequestIDHeader is the header read and written by RequestID unless overridden with WithRequestIDHeader.
Variables ¶
This section is empty.
Functions ¶
func Handler ¶
func Handler(next http.Handler, m ...Middleware) http.Handler
Handler applies middleware to next in one call. It is shorthand for Chain(m...)(next).
func RealIPFrom ¶
RealIPFrom returns the client IP resolved by the RealIP middleware, or an empty string if none is present.
func RequestIDFrom ¶
RequestIDFrom returns the request identifier stored by the RequestID middleware, or an empty string if none is present.
Types ¶
type CORSOption ¶
type CORSOption func(*corsConfig)
CORSOption configures the CORS middleware.
func WithAllowCredentials ¶
func WithAllowCredentials() CORSOption
WithAllowCredentials allows credentialed requests (cookies, HTTP auth). It cannot be combined with a wildcard origin.
func WithAllowedHeaders ¶
func WithAllowedHeaders(headers ...string) CORSOption
WithAllowedHeaders sets the request headers advertised in preflight responses. When unset, the requested headers are reflected back.
func WithAllowedMethods ¶
func WithAllowedMethods(methods ...string) CORSOption
WithAllowedMethods sets the methods advertised in preflight responses.
func WithAllowedOrigins ¶
func WithAllowedOrigins(origins ...string) CORSOption
WithAllowedOrigins sets the origins permitted to make cross-origin requests. The special value "*" allows any origin. Nothing is allowed until this is set.
func WithExposedHeaders ¶
func WithExposedHeaders(headers ...string) CORSOption
WithExposedHeaders sets the response headers browsers may expose to script.
func WithMaxAge ¶
func WithMaxAge(seconds int) CORSOption
WithMaxAge sets how long, in seconds, a preflight result may be cached.
type CompressOption ¶
type CompressOption func(*compressConfig)
CompressOption configures the Compress middleware.
func WithCompressLevel ¶
func WithCompressLevel(level int) CompressOption
WithCompressLevel sets the gzip level (gzip.BestSpeed to gzip.BestCompression, plus gzip.DefaultCompression and gzip.HuffmanOnly). An out-of-range level is ignored and the default is kept, so a configuration mistake cannot turn into a deferred runtime nil-pointer panic on the first large request.
func WithCompressMinBytes ¶
func WithCompressMinBytes(n int) CompressOption
WithCompressMinBytes sets the minimum response size to compress.
func WithCompressTypes ¶
func WithCompressTypes(types ...string) CompressOption
WithCompressTypes replaces the set of compressible content types (matched on the media type, without parameters).
type LoggerOption ¶
type LoggerOption func(*loggerConfig)
LoggerOption configures the Logger middleware.
func WithLogLevel ¶
func WithLogLevel(level slog.Level) LoggerOption
WithLogLevel sets the level at which requests are logged (default Info).
func WithLogMessage ¶
func WithLogMessage(msg string) LoggerOption
WithLogMessage sets the log message (default "http request").
func WithLogger ¶
func WithLogger(l *slog.Logger) LoggerOption
WithLogger sets the slog.Logger used for request logs. When nil (the default), slog.Default is used.
type Middleware ¶
Middleware is the standard net/http middleware shape: it wraps a handler and returns a new handler.
func CORS ¶
func CORS(opts ...CORSOption) Middleware
CORS handles Cross-Origin Resource Sharing. Its defaults are safe: no origin is allowed until you name one with WithAllowedOrigins. A preflight OPTIONS request from an allowed origin is answered with 204 and the appropriate headers; other requests are annotated and passed through.
CORS panics if credentials are allowed together with a wildcard origin, which is an invalid and unsafe combination.
Example ¶
ExampleCORS answers a preflight request from an allowed origin.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/goloop/middlewares"
)
func main() {
h := middlewares.CORS(
middlewares.WithAllowedOrigins("https://app.example.com"),
middlewares.WithAllowedMethods("GET", "POST"),
)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
r := httptest.NewRequest(http.MethodOptions, "/", nil)
r.Header.Set("Origin", "https://app.example.com")
r.Header.Set("Access-Control-Request-Method", "POST")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, r)
fmt.Println(rec.Code)
fmt.Println(rec.Header().Get("Access-Control-Allow-Origin"))
}
Output: 204 https://app.example.com
func Chain ¶
func Chain(m ...Middleware) Middleware
Chain composes middleware into a single Middleware. The first middleware in the list is the outermost wrapper, so it runs first on the way in and last on the way out:
mw := middlewares.Chain(a, b, c) // request flows a -> b -> c -> handler
Example ¶
ExampleChain composes several middleware into one wrapper.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/goloop/middlewares"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello")
})
h := middlewares.Chain(
middlewares.RequestID(),
middlewares.Recoverer(),
middlewares.SecurityHeaders(),
)(mux)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/hello", nil))
fmt.Println(rec.Body.String())
fmt.Println(rec.Header().Get("X-Content-Type-Options"))
}
Output: hello nosniff
func Compress ¶
func Compress(opts ...CompressOption) Middleware
Compress gzips responses for clients that accept it. It buffers the response until it reaches the minimum size, then decides whether to compress based on the status, existing Content-Encoding and Content-Type. It never compresses HEAD responses, 204/304 replies, already-encoded bodies, text/event-stream (server-sent events) or bodies below the minimum size. The Vary: Accept-Encoding header is always set on a compressed response.
func Logger ¶
func Logger(opts ...LoggerOption) Middleware
Logger logs one line per request with method, path, status, response size and duration, using log/slog. It does no work when the request is skipped or when the logger is not enabled at the configured level. The request identifier from RequestID, if present, is included.
func MaxBytes ¶
func MaxBytes(n int64) Middleware
MaxBytes limits the size of a request body to n bytes using http.MaxBytesReader. A handler that reads past the limit gets an error, and the server responds appropriately. Use it to guard against oversized uploads.
func RealIP ¶
func RealIP(opts ...RealIPOption) Middleware
RealIP resolves the client IP and stores it in the request context, readable with RealIPFrom. By default it uses the direct peer address (RemoteAddr) and ignores forwarded headers, which is the safe choice: without a trusted proxy policy a client can spoof X-Forwarded-For. Configure WithTrustedProxies (or TrustLoopbackProxies) to honor forwarded headers from known proxies.
func Recoverer ¶
func Recoverer(opts ...RecovererOption) Middleware
Recoverer catches a panic from a downstream handler, logs it with a stack trace and turns it into a response. If the handler had already committed a response (status and some body written), Recoverer cannot change it and only logs the panic. http.ErrAbortHandler is re-panicked so the server can abort the connection as usual.
func RequestID ¶
func RequestID(opts ...RequestIDOption) Middleware
RequestID assigns each request an identifier, stores it in the request context (read with RequestIDFrom) and echoes it in the response header. By default it generates a new 128-bit random identifier and ignores any inbound value; use WithTrustedRequestIDHeader to accept a safe client-supplied one.
Example ¶
ExampleRequestID reuses a fixed generator so the identifier is predictable.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/goloop/middlewares"
)
func main() {
h := middlewares.RequestID(
middlewares.WithRequestIDGenerator(func() string { return "req-1" }),
)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println(middlewares.RequestIDFrom(r.Context()))
}))
h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil))
}
Output: req-1
func SecurityHeaders ¶
func SecurityHeaders(opts ...SecurityOption) Middleware
SecurityHeaders adds a conservative set of security response headers: X-Content-Type-Options, X-Frame-Options and Referrer-Policy by default, with optional HSTS and Content-Security-Policy. Existing header values set by the handler are not overwritten.
func Throttle ¶
func Throttle(limit int, opts ...ThrottleOption) Middleware
Throttle bounds how many requests run at once: a semaphore over the handler, shedding load instead of queueing it. It is an in-process concurrency limiter, not a rate limiter, and the two answer different questions: a rate limit says how often a client may come back, a concurrency limit says how much of this work the process can afford at the same moment. A rate limit of ten per minute still admits ten requests in the same second; ten concurrent slots do not, which is what matters in front of work that pays its cost per call - password hashing at tens of megabytes per verification is the classic case, where the limit turns a spike into a bounded memory ceiling.
When every slot is busy the request is rejected immediately with the configured status rather than queued. The waiters themselves would be cheap to hold; what a queue actually does is pin each caller's connection while its deadline runs out, so the process ends up doing expensive work for clients that have already given up. An immediate rejection with Retry-After tells them honestly to come back later.
Throttle panics if limit is not positive, since that is a programming error.
func Timeout ¶
func Timeout(d time.Duration, opts ...TimeoutOption) Middleware
Timeout enforces a per-request deadline. It cancels the request context after d and, if the handler has not finished, replies with 503 Service Unavailable. It is built on http.TimeoutHandler, so the timeout reply and the handler's own writes are synchronized and cannot race.
Timeout cannot stop a handler goroutine that ignores its context. Long handlers must observe r.Context().Done() themselves to actually stop working.
type RealIPOption ¶
type RealIPOption func(*realIPConfig)
RealIPOption configures the RealIP middleware.
func TrustLoopbackProxies ¶
func TrustLoopbackProxies() RealIPOption
TrustLoopbackProxies trusts the loopback ranges (127.0.0.0/8 and ::1/128), which is convenient for local development behind a reverse proxy.
func WithTrustedProxies ¶
func WithTrustedProxies(cidrs ...string) RealIPOption
WithTrustedProxies marks the given CIDR ranges as trusted proxies. Forwarded headers are honored only when the direct peer (RemoteAddr) falls inside one of them. Invalid CIDRs are ignored.
type RecovererOption ¶
type RecovererOption func(*recovererConfig)
RecovererOption configures the Recoverer middleware.
func WithRecoverHandler ¶
func WithRecoverHandler(fn func(http.ResponseWriter, *http.Request, any)) RecovererOption
WithRecoverHandler sets a handler that renders the response after a panic. It runs only when nothing has been written to the client yet; the recovered value is passed as the third argument. Without it, a bare 500 is sent on an uncommitted response.
func WithRecoverLogger ¶
func WithRecoverLogger(l *slog.Logger) RecovererOption
WithRecoverLogger sets the logger used to report a recovered panic. When nil (the default), slog.Default is used.
type RequestIDOption ¶
type RequestIDOption func(*requestIDConfig)
RequestIDOption configures the RequestID middleware.
func WithRequestIDGenerator ¶
func WithRequestIDGenerator(fn func() string) RequestIDOption
WithRequestIDGenerator sets the function that produces a new identifier.
func WithRequestIDHeader ¶
func WithRequestIDHeader(name string) RequestIDOption
WithRequestIDHeader sets the header used to read and write the identifier.
func WithTrustedRequestIDHeader ¶
func WithTrustedRequestIDHeader() RequestIDOption
WithTrustedRequestIDHeader lets RequestID reuse a client-supplied identifier when it is safe (ASCII, no control characters, within the length limit). Without this option a fresh identifier is always generated.
type SecurityOption ¶
type SecurityOption func(*securityConfig)
SecurityOption configures the SecurityHeaders middleware.
func WithCSP ¶
func WithCSP(policy string) SecurityOption
WithCSP sets a Content-Security-Policy. It is opt-in because a good policy depends on the application.
func WithFrameOptions ¶
func WithFrameOptions(v string) SecurityOption
WithFrameOptions sets the X-Frame-Options value (default "DENY"). An empty string omits the header.
func WithHSTS ¶
func WithHSTS(seconds int) SecurityOption
WithHSTS enables Strict-Transport-Security with the given max-age in seconds. It should only be set for sites served exclusively over HTTPS. Zero, the default, omits the header.
func WithReferrerPolicy ¶
func WithReferrerPolicy(v string) SecurityOption
WithReferrerPolicy sets the Referrer-Policy value (default "no-referrer"). An empty string omits the header.
type ThrottleOption ¶
type ThrottleOption func(*throttleConfig)
ThrottleOption configures the Throttle middleware.
func WithThrottleRetryAfter ¶
func WithThrottleRetryAfter(seconds int) ThrottleOption
WithThrottleRetryAfter sets the Retry-After header (in seconds) on a rejected request. Zero, the default, omits the header.
func WithThrottleStatus ¶
func WithThrottleStatus(code int) ThrottleOption
WithThrottleStatus sets the status returned when the limit is reached (default 503 Service Unavailable).
type TimeoutOption ¶
type TimeoutOption func(*timeoutConfig)
TimeoutOption configures the Timeout middleware.
func WithTimeoutMessage ¶
func WithTimeoutMessage(msg string) TimeoutOption
WithTimeoutMessage sets the body sent when the deadline is exceeded.