middlewares

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 14 Imported by: 0

README

deps.dev License License Stay with Ukraine

middlewares

middlewares is a set of production-ready HTTP middleware for Go's 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 has no third-party dependencies and logs through the standard log/slog.

It is not a framework - it closes the common cross-cutting needs the standard library leaves out, so a service can stay on the standard toolbox and still get request IDs, real client IP, panic recovery, request logging, timeouts, compression, throttling, CORS and security headers.

Features

  • RequestID - a random identifier per request, in the context and response header; inbound values are ignored unless explicitly trusted.
  • RealIP - the client IP, from RemoteAddr by default and from forwarded headers only behind trusted proxies.
  • Recoverer - turns a panic into a 500 and a logged stack trace, without corrupting an already-committed response.
  • Logger - one structured log/slog line per request with status, size and duration.
  • Timeout - a per-request deadline built on http.TimeoutHandler.
  • Compress - gzip with skip rules for small bodies, server-sent events and already-encoded responses.
  • Throttle - an in-process concurrency limiter: a semaphore with load shedding, for work that pays its cost per call (password hashing, image processing).
  • CORS - safe by default; nothing is allowed until you name the origins.
  • SecurityHeaders, MaxBytes - conservative headers and a request-body cap.
  • Chain and Handler - compose middleware.

Installation

go get -u github.com/goloop/middlewares
import "github.com/goloop/middlewares"

Requires Go 1.25 or newer.

Quick start

mux := http.NewServeMux()
mux.HandleFunc("GET /health", health)

h := middlewares.Chain(
    middlewares.RequestID(),
    middlewares.RealIP(middlewares.TrustLoopbackProxies()),
    middlewares.Recoverer(),
    middlewares.Logger(),
    middlewares.Compress(),
    middlewares.SecurityHeaders(),
)(mux)

http.ListenAndServe(":8080", h)

With the goloop/mux router (same type, no adapter needed):

r := mux.New()
r.Use(
    middlewares.RequestID(),
    middlewares.RealIP(),
    middlewares.Recoverer(),
    middlewares.Logger(),
)
r.Get("/users/{id}", usersShow)

Security notes

Read these before production use:

  • RealIP trusts no proxy headers by default. Configure WithTrustedProxies (or TrustLoopbackProxies) or a client can spoof its address.
  • Timeout cancels the request context but cannot stop a handler goroutine that ignores it - long handlers must watch r.Context().Done().
  • 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; a wildcard origin together with credentials is rejected at construction.
  • Throttle is an in-process concurrency limiter, not a distributed rate limiter - and not a substitute for one. A rate limit answers "how often may this client come back"; Throttle answers "how much of this work fits in the process at once". Ten-per-minute still admits ten in the same second; ten slots do not. Pair them in front of memory-bound endpoints such as login.

Logging

Logging uses the standard log/slog. Pass any *slog.Logger through WithLogger; the package never imports a concrete logging backend, so you can plug in whatever handler you already use.

Documentation

Contributing

Contributions are welcome. Please run go test ./..., go test -race ./..., go vet ./... and gofmt -l . before submitting a pull request.

License

middlewares is released under the MIT License. See LICENSE.

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

Examples

Constants

View Source
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.

View Source
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

func RealIPFrom(ctx context.Context) string

RealIPFrom returns the client IP resolved by the RealIP middleware, or an empty string if none is present.

func RequestIDFrom

func RequestIDFrom(ctx context.Context) string

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.

func WithSkip

func WithSkip(fn func(*http.Request) bool) LoggerOption

WithSkip skips logging for requests where fn returns true, for example health checks.

type Middleware

type Middleware func(http.Handler) http.Handler

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.

Jump to

Keyboard shortcuts

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