middlewares

package module
v0.3.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: 15 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.
  • RateLimit - how often one client may come back, with a pluggable store and an in-process token bucket by default.
  • 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, rate limiting, 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:

Rate limiting and throttling are different tools

RateLimit 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 instant, which is what exhausts memory in front of an expensive handler; ten concurrent slots do not, and neither stops a client from returning every second forever. In front of sign-in both belong.

middlewares.RateLimit(middlewares.RateLimitConfig{
    Limit: 20, Window: 15 * time.Minute,
})

The store decides, and the default one counts only what this process saw. A limit that has to hold across instances needs a RateLimitStore they share; the interface is one method so that a Redis or database implementation is a short piece of application code rather than a dependency of this package.

When a store cannot answer, the request is refused with 503 rather than served. A limiter usually sits in front of sign-in, password reset and one-time codes, and a store outage that quietly removed the limit from exactly those routes would turn an availability problem into a security one. WithFailOpen chooses the other trade for routes where it is the right one.

Anything keyed by client address inherits the caveat on RealIP: behind a reverse proxy with no trusted-proxy policy, every client resolves to the proxy and the limit becomes one budget shared by everyone. Configure WithTrustedProxies before relying on KeyByIP.

Correlated logs

Logger writes one line per request. WithContextLogger additionally puts a logger into the request context, already carrying the request identifier, method and path, so that handler logs join up with it:

h := middlewares.Logger(middlewares.WithContextLogger())
// in a handler:
middlewares.LoggerFrom(r.Context()).Warn("upstream refused", "status", code)

The alternative is remembering to add the identifier at every call site, which fails quietly and selectively - the lines that are missing it are the ones written in a hurry, which are the ones written during an incident.

  • 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 KeyByHeader added in v0.3.0

func KeyByHeader(name string) func(*http.Request) string

KeyByHeader groups requests by a header value, for limits keyed by an API key or a tenant. Requests without the header fall back to the client address, so a missing header cannot buy an unlimited budget.

func KeyByIP added in v0.3.0

func KeyByIP() func(*http.Request) string

KeyByIP groups requests by client address, using the address RealIP resolved when it ran.

Read the note on RealIP before relying on this. Without WithTrustedProxies, every client behind a reverse proxy resolves to the proxy's own address, so this returns one key for all of them and the limit becomes a budget the whole userbase shares. Nothing errors; the limiter simply stops being per-client.

func LoggerFrom added in v0.3.0

func LoggerFrom(ctx context.Context) *slog.Logger

LoggerFrom returns the logger WithContextLogger put in the context, carrying the request identifier, method and path. Without that option, or outside a request, it returns slog.Default so a caller never has to check:

middlewares.LoggerFrom(r.Context()).Info("upstream refused", "status", code)

The line then carries the identifier of the request that caused it, which is what makes the two halves of an incident join up.

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.

This is the only way to read that address: RealIP writes it here and leaves Request.RemoteAddr untouched. An empty result means the middleware did not run, and falling back to RemoteAddr is then the caller's decision to make knowingly - behind a proxy it is the proxy's address.

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 HSTS added in v0.3.0

type HSTS struct {
	// MaxAge is how long a browser should refuse plain HTTP for this host.
	// It is a duration rather than a count of seconds because a year is
	// easier to read as 365*24*time.Hour than as 31536000. Values below a
	// second round down to zero, which omits the header.
	MaxAge time.Duration

	// IncludeSubDomains extends the policy to every subdomain. Almost every
	// real policy wants it, and it is worth meaning: a subdomain that cannot
	// serve HTTPS becomes unreachable for as long as MaxAge lasts.
	IncludeSubDomains bool

	// Preload asks for inclusion in the browsers' built-in list. It requires
	// IncludeSubDomains and a long MaxAge, and removal from that list takes
	// months, so set it only for a host that will be HTTPS-only indefinitely.
	Preload bool
}

HSTS is a complete Strict-Transport-Security policy.

type LoggerOption

type LoggerOption func(*loggerConfig)

LoggerOption configures the Logger middleware.

func WithContextLogger added in v0.3.0

func WithContextLogger() LoggerOption

WithContextLogger puts a logger for this request into its context, already carrying the request identifier, method and path, so that handlers can log with LoggerFrom and have their lines correlate without threading anything through.

It is worth the option because the alternative fails quietly. Logger writes one line per request with the identifier on it, and every line a handler writes is separate and unrelated; joining them afterwards means matching on timestamps. The usual fix is to remember to add the identifier at each call site, and what actually happens is that someone does not - including people who have just written in a design document that the logs correlate.

It is opt-in rather than automatic because it allocates a context and a logger per request, and a service that never reads it should not pay for it.

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 MemoryRateLimitStore added in v0.3.0

type MemoryRateLimitStore struct {
	// contains filtered or unexported fields
}

MemoryRateLimitStore is a token bucket per key, held in this process.

A bucket refills continuously rather than resetting on a schedule, which is what keeps a burst at a window boundary from being twice the limit: a fixed window that resets at noon will happily serve a full budget at 11:59:59 and another at 12:00:00. The bucket also lets a caller who has been quiet spend a little faster, up to the limit, which is usually what "ten per minute" was meant to allow.

It counts only what this process saw. Behind a load balancer with several instances, each keeps its own buckets and the effective limit is multiplied by the number of instances; a limit that has to hold across them needs a store they share.

func NewMemoryRateLimitStore added in v0.3.0

func NewMemoryRateLimitStore() *MemoryRateLimitStore

NewMemoryRateLimitStore returns an empty in-process token-bucket store.

func (*MemoryRateLimitStore) Take added in v0.3.0

func (s *MemoryRateLimitStore) Take(
	_ context.Context,
	key string,
	limit int,
	window time.Duration,
) (RateLimitResult, error)

Take records an attempt and reports whether it is allowed.

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.

It is an alias rather than a defined type on purpose. Routers declare their own name for this same shape, and a defined type here would force an explicit conversion at every boundary; an alias makes everything this package returns assignable to any of them directly.

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 RateLimit added in v0.3.0

func RateLimit(cfg RateLimitConfig, opts ...RateLimitOption) Middleware

RateLimit limits how often one caller may make a request.

It is not Throttle and does not replace it. 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 instant, which is what exhausts memory in front of an expensive handler; ten concurrent slots do not, and neither stops a client from returning every second forever. In front of sign-in both belong.

A rejected request gets the configured status, a Retry-After header, and the X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. So does an allowed one, minus Retry-After, so a well-behaved client can slow down before it is refused.

RateLimit panics if Limit is not positive or Window is not positive, since either is a programming error rather than a runtime condition.

func RealIP

func RealIP(opts ...RealIPOption) Middleware

RealIP resolves the client IP and stores it in the request context, readable with RealIPFrom.

The context is the only place it writes. It does not rewrite Request.RemoteAddr, and nothing downstream that reads RemoteAddr sees a resolved address - a handler doing so gets the peer, which behind a proxy is the proxy. Anything that wants the client address has to ask RealIPFrom for it; that is the whole interface.

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.

Know what that default costs behind a reverse proxy. If requests arrive through nginx, a dev server or anything else in front, the direct peer is that proxy, so every client resolves to one address. Nothing errors and nothing looks wrong: rate limits, audit trails and ban lists keyed by IP simply become shared by everyone at once. A limit of twenty attempts per address is then twenty for the whole userbase, and the second person to mistype a password is locked out by the first.

That is the price of not trusting a header anyone can forge, and it is the right default - but it is only correct once WithTrustedProxies tells this middleware which peer is the proxy. Configure it before relying on anything keyed by client address.

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 RateLimitConfig added in v0.3.0

type RateLimitConfig struct {
	// Key groups requests that share a budget. Nil means [KeyByIP].
	Key func(*http.Request) string

	// Limit is how many requests one key may make per Window. A limit of
	// zero or less is a programming error and panics at construction.
	Limit  int
	Window time.Duration

	// Store decides. Nil means a fresh [NewMemoryRateLimitStore], which is
	// per-process: with more than one instance running, each gets its own
	// budget and the effective limit multiplies by the instance count.
	Store RateLimitStore
}

RateLimitConfig describes a limit.

type RateLimitOption added in v0.3.0

type RateLimitOption func(*rateLimitConfig)

RateLimitOption configures the RateLimit middleware.

func WithFailOpen added in v0.3.0

func WithFailOpen() RateLimitOption

WithFailOpen serves requests when the store cannot answer, instead of refusing them.

The default is the other way round, and deliberately: a limiter is usually in front of sign-in, password reset or one-time codes, and a store outage that silently removed the limit from exactly those routes would be an availability problem turning itself into a security one. Choose this for routes where being reachable matters more than being limited - and know that while the store is down, they are not limited at all.

func WithRateLimitStatus added in v0.3.0

func WithRateLimitStatus(code int) RateLimitOption

WithRateLimitStatus sets the status returned when the limit is reached (default 429 Too Many Requests).

type RateLimitResult added in v0.3.0

type RateLimitResult struct {
	// Allowed says whether to serve the request.
	Allowed bool

	// Remaining is how much budget is left after this attempt.
	Remaining int

	// RetryAfter is how long until the caller may try again. It is only
	// meaningful when Allowed is false.
	RetryAfter time.Duration

	// Reset is when the budget returns to full. Zero when the store does not
	// track it.
	Reset time.Time
}

RateLimitResult is one store decision.

type RateLimitStore added in v0.3.0

type RateLimitStore interface {
	// Take records an attempt against key and reports whether it is allowed.
	// limit and window describe the budget; a store keyed by more than one
	// budget can use them to size the bucket it creates.
	Take(ctx context.Context, key string, limit int, window time.Duration) (RateLimitResult, error)
}

RateLimitStore decides whether one more request may be served for a key.

It is an interface because the useful implementations are not the library's to write: a limit that holds across several instances of a service needs something both instances can see, and that means a dependency this package will not take. The shape here is what a shared store can answer efficiently - one call that both decides and reports what is left.

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.

Use WithHSTSPolicy for the directives a full policy usually carries.

func WithHSTSPolicy added in v0.3.0

func WithHSTSPolicy(p HSTS) SecurityOption

WithHSTSPolicy sets a full Strict-Transport-Security policy, including includeSubDomains and preload.

It replaces rather than extends WithHSTS; the last of the two applied wins. A zero MaxAge omits the header, so a policy can be turned off in one place without unpicking the rest of it.

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