middleware

package
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package middleware holds net/http middleware for go-router and any other http.Handler: request IDs, logging through log/slog, recovery, the client IP behind trusted proxies, security headers and Content-Security-Policy, CORS, compression, ETags and Last-Modified checks, an output cache, and rate and body limits. Each middleware has the shape func(http.Handler) http.Handler. The package uses the standard library only.

Index

Constants

View Source
const (
	// DefaultCacheTTL is how long the output cache (NewCache) keeps a
	// response by default.
	DefaultCacheTTL = time.Minute

	// DefaultCacheMaxBytes is how much memory the output cache uses for
	// responses by default: 32 MiB.
	DefaultCacheMaxBytes = 32 << 20

	// DefaultCacheMaxEntryBytes is the largest response body the output
	// cache stores by default: 1 MiB.
	DefaultCacheMaxEntryBytes = 1 << 20

	// DefaultCacheMaxVarySets is how many Vary lists the output cache keeps
	// entries for under one key by default.
	DefaultCacheMaxVarySets = 4
)
View Source
const DefaultCompressMinSize = 1024

DefaultCompressMinSize is the smallest body Compress compresses by default: below it, gzip's header and the work cost more than they save.

View Source
const DefaultImmutable = "public, max-age=31536000, immutable"

DefaultImmutable is a Cache-Control for versioned URLs, whose content changes with their version: kept for a year without asking again.

View Source
const RequestIDHeader = "X-Request-ID"
View Source
const StrictCSP = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; " +
	"object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'"

StrictCSP is a Content-Security-Policy that allows only what the site itself serves: no inline scripts or styles, no eval, no plugins, and forms, <base> and framing limited to the site. Pages allow the inline scripts they need by hash, with AddCSPSource. Images may also be data: URLs.

Variables

View Source
var DefaultETagTypes = []string{
	"text/html",
	"text/plain",
	"text/css",
	"text/javascript",
	"text/xml",
	"application/javascript",
	"application/json",
	"application/xml",
	"application/atom+xml",
	"application/rss+xml",
}

DefaultETagTypes are the media types of the responses ETag gives an ETag by default: text formats that are the same for every visitor until their content changes, such as pages, feeds, JSON, scripts and styles. Changing the slice changes the default of ETag middleware made afterwards.

Functions

func AcceptsGzip added in v0.12.0

func AcceptsGzip(header string) bool

AcceptsGzip reports whether an Accept-Encoding header allows gzip: named or as *, with a quality above zero. An explicit gzip decides over *.

func AddCSPSource added in v0.12.0

func AddCSPSource(w http.ResponseWriter, directive string, sources ...string)

AddCSPSource adds sources, such as the hash of an inline script ("'sha256-…'"), to a directive of the response's Content-Security-Policy, as CSPPolicy.Add does: a missing directive starts from the one it falls back to, so the policy only ever allows more. Call it before the response is written.

It changes the policy in the header at once, and under CSP adds the sources again when the response is written, so they also survive a handler that sets the header itself afterwards. A response without a policy stays without one: a directive with only these sources would forbid everything else.

func BodyLimit added in v0.12.0

func BodyLimit(n int64, options ...BodyLimitOptions) func(http.Handler) http.Handler

BodyLimit returns middleware that limits request bodies to n bytes, as http.MaxBytesHandler does: reading more fails with an *http.MaxBytesError, so form parsing and decoding fail, and the server closes the connection after the response.

r.Use(middleware.BodyLimit(1 << 20)) // 1 MiB
r.Post("/upload", upload).Use(middleware.BodyLimit(64<<20, middleware.BodyLimitOptions{
	OnTooLarge: http.HandlerFunc(tooLarge),
}))

A handler tells a body over the limit by the error:

var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) { ... }

func CORS added in v0.2.0

func CORS(options CORSOptions) func(http.Handler) http.Handler

CORS returns a Middleware that handles Cross-Origin Resource Sharing. Every answer says Vary: Origin, also one to a request without an Origin, so that a shared cache keeps such an answer, without CORS headers, apart from the answers to the allowed origins.

func CSP added in v0.12.0

func CSP(policy string) func(http.Handler) http.Handler

CSP returns middleware that merges policy into the Content-Security-Policy of every response, directive by directive, and lets handlers add sources to it with AddCSPSource until the response is written:

r.Use(middleware.CSP(middleware.StrictCSP))

func page(w http.ResponseWriter, r *http.Request) {
	middleware.AddCSPSource(w, "script-src", importMap.ScriptHash())
	...
}

The policy of a response is made in this order; a later step wins over an earlier one for the directives it names, and keeps the others:

  1. The policy the response has when the middleware runs, set by an outer middleware, such as another CSP or SecurityHeaders.
  2. policy: its directives replace those of the same name, such as "frame-ancestors 'none'" replacing only frame-ancestors.
  3. What the handler, or a middleware inside this one, leaves in the header when the response is written: its directives replace those of the same name. Setting "frame-ancestors *" changes only frame-ancestors; it does not drop the rest. A handler that deletes the header sends no policy.
  4. The sources of AddCSPSource, added to the directives as they then stand (see CSPPolicy.Add).

So the innermost middleware and the handler have the last word: put a site-wide policy outside, and the policy of a group of routes, such as "frame-ancestors 'none'" for an admin, inside. The middleware sets the merged policy of steps 1 and 2 in the header before the handler runs, so the handler can read it, and merges steps 3 and 4 when the status is written, or after the handler returns without writing anything.

Only the first policy of the header is merged; further policies, which browsers enforce as well (see SplitCSP), stay as they are. An empty policy only lets handlers add sources to a policy set elsewhere.

Put CSP inside an output cache (NewCache): the cache stores the headers set inside it, so the policy, with the hashes of the page, is stored and served with the page.

func CacheControl added in v0.12.0

func CacheControl(value string) func(http.Handler) http.Handler

CacheControl returns middleware that sets the Cache-Control header of responses to value, before the handler runs, so the handler can still change it:

r.Get("/feed.xml", feed).Use(middleware.CacheControl("public, max-age=300"))

func CacheKey added in v0.12.0

func CacheKey(r *http.Request) (string, bool)

CacheKey is the default key of NewCache: the scheme, host (see Scheme and Host), path and query a request arrived with, as the client sent them, also when a mount stripped a prefix from the request's URL.

func CacheableTypes added in v0.12.0

func CacheableTypes(types ...string) func(status int, header http.Header) bool

CacheableTypes returns a Cacheable function for ETagOptions that reports whether a response is complete and every visitor may see it alike: status 200, without an ETag, Content-Encoding or Set-Cookie of its own, not private or no-store, and a Content-Type of one of types. An entry of types is a media type such as "application/json", all subtypes of a type such as "text/*", or a structured syntax suffix such as "+xml"; case does not matter.

CacheableTypes panics when an entry is none of these.

func ClientIP added in v0.12.0

func ClientIP(r *http.Request) string

ClientIP returns the client IP that RealIP or RealIPWithOptions found. When neither ran, it is the IP of the direct peer, or r.RemoteAddr when that is not an IP address (for example a Unix socket).

func Compress added in v0.12.0

func Compress(options ...CompressOptions) func(http.Handler) http.Handler

Compress returns middleware that gzips responses for clients that accept it (Accept-Encoding with gzip or *, at a quality above zero). Only responses of a compressible type (CompressibleType: text, JSON, XML, SVG and the like, not images, video, archives, WOFF fonts or server-sent events) of at least MinSize bytes are compressed. HEAD, Range and Upgrade requests, and responses that are encoded already, partial (206 or with a Content-Range), 204 or 304, or flushed before MinSize bytes, go out as they are. Every response that could be compressed says Vary: Accept-Encoding, so caches keep both versions apart, and a strong ETag of a compressed response becomes weak, since the compressed bytes differ.

r.Use(middleware.Compress())
r.Use(middleware.Compress(middleware.CompressOptions{Enabled: settings.Compress}))

The gzip writers are reused, and shared by every Compress of the process with the same Level: per level, up to min(GOMAXPROCS, 16) idle writers are kept, of about 0.8 MB each at gzip.BestSpeed and 1.05 to 1.2 MB at the other levels (0.3 MB at gzip.HuffmanOnly), with a sync.Pool for bursts beyond them.

It panics on an invalid Level.

func CompressibleType added in v0.12.0

func CompressibleType(contentType string) bool

CompressibleType reports whether a response of the content type is worth compressing: text (except server-sent events), JSON, JavaScript, XML, WebAssembly, SVG, icons, BMP and uncompressed fonts.

func ContentLengthMiddleware added in v0.2.0

func ContentLengthMiddleware(next http.Handler) http.Handler

ContentLengthMiddleware automatically sets the Content-Length header

func ETag added in v0.12.0

func ETag(options ...ETagOptions) func(http.Handler) http.Handler

ETag returns middleware that gives complete responses to GET and HEAD requests a weak ETag, a hash of their body, and answers requests that name it in If-None-Match with 304 Not Modified, without the body. It suits pages rendered per request, which are the same for every visitor until their content changes: the client still asks every time, but an unchanged page costs no transfer.

r.Use(middleware.ETag(middleware.ETagOptions{
	CacheControl: func(*http.Request) string { return "no-cache" },
}))

Responses that are not cacheable (see CacheableTypes and Types) go through as they are written, and so do responses that are flushed: they are streamed. The ETag is weak, so it stays valid when Compress, outside ETag, compresses the response. ETag answers If-None-Match only; put LastModified inside it for If-Modified-Since.

ETag panics when an entry of Types is not a media type, a type with "/*" or a suffix starting with "+".

func ETagMatches added in v0.12.0

func ETagMatches(header string, etag string) bool

ETagMatches reports whether an If-None-Match header names etag, comparing weakly (RFC 9110, 13.1.2): W/"x" and "x" match, and so does *.

func FromTrustedProxy added in v0.12.0

func FromTrustedProxy(r *http.Request) bool

FromTrustedProxy reports whether the request came directly from a trusted proxy (see RealIPWithOptions), so that its other forwarding headers, such as X-Forwarded-Proto and X-Forwarded-Host, can be believed as well.

func GetClientIP added in v0.12.0

func GetClientIP(ctx context.Context) string

GetClientIP returns the client IP stored by RealIP or RealIPWithOptions, or "" when neither ran.

func GetRequestID added in v0.9.0

func GetRequestID(ctx context.Context) string

GetRequestID returns the request ID stored in ctx, or "".

func Headers added in v0.12.0

func Headers(headers http.Header) func(http.Handler) http.Handler

Headers returns middleware that sets the given response headers before the handler runs, replacing values of the same name, so the handler can still change them:

r.Use(middleware.Headers(http.Header{
	"X-Robots-Tag":  {"noindex, nofollow"},
	"Cache-Control": {"no-store"},
}))

func Host added in v0.12.0

func Host(r *http.Request) string

Host returns the host, with its port if any, a request arrived on: r.Host, or, when the request came from a trusted proxy, what the proxy reports in X-Forwarded-Host, or else in the host of Forwarded, when that is a valid host name or IP address with an optional port.

func LastModified added in v0.12.0

func LastModified(next http.Handler) http.Handler

LastModified is middleware that answers If-Modified-Since with 304 Not Modified, without the body, when the handler's response has status 200 and a Last-Modified that is not newer than the date the client names, compared to the second. It follows RFC 9110 (13.1.3 and 13.2.2): only GET and HEAD requests are answered, and a request with If-None-Match is passed on as it is, since an entity tag decides before a date does (see ETag). An If-Modified-Since that is not a valid HTTP date is ignored.

r.Use(middleware.Compress())
r.Use(middleware.ETag())
r.Use(middleware.LastModified)

The handler still renders the response, which is then dropped. Put LastModified inside ETag, so a request answered by its date costs no hash (that 304 carries no ETag), and inside an output cache (NewCache), which answers both conditions from its entries itself and passes neither on to the handler.

A date can only tell that nothing changed when every change makes it newer. A response whose Last-Modified can go back, such as a feed whose newest entry is taken off the site, would be answered 304 to a client that holds the version before the change: give it no Last-Modified, or a date that includes such changes.

func LogHandler added in v0.12.0

func LogHandler(handler slog.Handler) slog.Handler

LogHandler wraps a slog handler so that every record logged with the context of a request carries the request's ID (see RequestID) as request_id, also records of code that knows nothing about requests:

logger := slog.New(middleware.LogHandler(slog.NewJSONHandler(os.Stderr, nil)))
logger.InfoContext(req.Context(), "page saved") // ... request_id=...

Records that name a request_id already, such as those of Logger and Recover, keep theirs. Wrapping a handler twice wraps it once.

func Logger added in v0.9.0

func Logger(options ...LoggerOptions) func(http.Handler) http.Handler

Logger returns middleware that logs every request when it is done, with its method, URI, status, response size, duration, client IP (see ClientIP) and request ID (see RequestID). A request whose handler panicked is logged as well, with panicked=true and status 500 when no status was written, before the panic goes on.

func NoStore added in v0.12.0

func NoStore(next http.Handler) http.Handler

NoStore keeps responses from being stored by browsers and caches, with Cache-Control: no-store, for pages with personal data, forms with a visitor's input and administration pages. A handler can still set a Cache-Control of its own.

func Observe added in v0.12.0

func Observe(observe func(*http.Request, ResponseInfo)) func(http.Handler) http.Handler

Observe returns middleware that calls observe for every request when it is done, with the response's status, size and duration, for metrics and access logs:

r.UseAll(middleware.Observe(func(req *http.Request, info middleware.ResponseInfo) {
	requests.WithLabelValues(req.Method, strconv.Itoa(info.Status)).Inc()
}))

The response headers are in ResponseInfo.Header, for an observer that tells responses apart by them, such as one that lists the 404s search engines may index:

middleware.Observe(func(req *http.Request, info middleware.ResponseInfo) {
	if info.Status == http.StatusNotFound && info.Header.Get("X-Robots-Tag") == "" {
		missed(req.URL.Path)
	}
})

observe also runs for a request whose handler panicked, before the panic goes on, so put Recover inside Observe to answer and count panics as a 500. With router.TrackRoutes, router.MatchedRoute(req) tells observe which route served the request, and router.MatchedLabel(req) its label without copying the route.

func Origin added in v0.12.0

func Origin(r *http.Request) string

Origin returns the scheme and host a request arrived on, such as "https://example.com", for absolute URLs and same-origin checks: Scheme and Host.

func ParseTrustedProxies added in v0.12.0

func ParseTrustedProxies(proxies []string) ([]netip.Prefix, error)

ParseTrustedProxies parses IP addresses and CIDR ranges. Empty entries are skipped; anything else that is not an address or range is an error.

func RateLimit added in v0.12.0

func RateLimit(options RateLimitOptions) func(http.Handler) http.Handler

RateLimit returns middleware that allows each client (Key, by default ClientIP) at most Limit requests per Window, and answers the others with OnLimit:

r.Post("/comments", post).Use(middleware.RateLimit(middleware.RateLimitOptions{
	Limit:  5,
	Window: time.Minute,
}))

Requests are counted in memory, per middleware and per process: an application on several servers allows Limit requests per server, and a restart forgets them. Behind proxies, use RealIPWithOptions so the client IP is the client's, not the proxy's.

func RealIP added in v0.9.0

func RealIP(next http.Handler) http.Handler

RealIP stores the client IP in the request context (see ClientIP). It trusts no proxies, so the client IP is the address of the direct peer and forwarding headers are ignored. Use RealIPWithOptions behind proxies.

func RealIPWithOptions added in v0.9.0

func RealIPWithOptions(options RealIPOptions) func(http.Handler) http.Handler

RealIPWithOptions returns middleware that finds the client IP and stores it in the request context (see ClientIP). Forwarding headers are only believed when the direct peer is a trusted proxy. X-Forwarded-For is then read from the right, skipping trusted proxies: the first address that is not a trusted proxy is the client. X-Real-IP, and then Forwarded, are used when X-Forwarded-For is absent.

When the client differs from the direct peer, r.RemoteAddr is rewritten to the client IP with port 0, so it stays parseable by net.SplitHostPort. Otherwise r.RemoteAddr is left alone.

It panics when a trusted proxy is not an IP address or CIDR range; use TryRealIP to get an error instead.

func Recover added in v0.2.0

func Recover(next http.Handler) http.Handler

Recover turns a panic in a handler into a 500 response and logs it with slog.Default(). See RecoverWithOptions.

func RecoverWithOptions added in v0.12.0

func RecoverWithOptions(options RecoverOptions) func(http.Handler) http.Handler

RecoverWithOptions returns middleware that recovers panics in handlers. The panic is logged with its stack. When the response has not started yet, a 500 Internal Server Error is written; when it has, the connection is aborted with http.ErrAbortHandler, so the client sees a broken response rather than a truncated one that looks complete. Panics with http.ErrAbortHandler are passed on unlogged, as net/http expects.

func RequestID added in v0.9.0

func RequestID(next http.Handler) http.Handler

RequestID gives every request an ID: the one sent in X-Request-ID when it is valid (at most 64 letters, digits and -_.:), otherwise a new random one. The ID is stored in the request context (see GetRequestID) and set on the response's X-Request-ID header.

func RequestIDWithOptions added in v0.12.0

func RequestIDWithOptions(options RequestIDOptions) func(http.Handler) http.Handler

RequestIDWithOptions is RequestID with a configurable header, generator and context hook.

func SafeMethod added in v0.12.0

func SafeMethod(r *http.Request) bool

SafeMethod reports whether the request's method is GET, HEAD, OPTIONS or TRACE, which do not change anything, for RateLimitOptions.Skip.

func SameOrigin added in v0.12.0

func SameOrigin(r *http.Request, u *url.URL) bool

SameOrigin reports whether u is an absolute http or https address on the host the request arrived on: its host equals Host(r), ignoring case, so it trusts what a trusted proxy reports. Use it for an address the client sends, such as the page an htmx request was made on (HX-Current-URL), to tell whether it is a page of this site:

current, err := url.Parse(req.Header.Get("HX-Current-URL"))
if err == nil && middleware.SameOrigin(req, current) {
	// current.Path is a page of this site.
}

The schemes are not compared: behind a proxy that does not report the scheme, a request may seem to arrive on http for a page on https. Hosts compare as written, so example.com and example.com:443 differ. A nil u, a relative address and another scheme are not same-origin.

func SameOriginReferer added in v0.12.0

func SameOriginReferer(r *http.Request) (string, bool)

SameOriginReferer returns the path and query of the request's Referer when it is an address on the host the request arrived on (SameOrigin), for returning a visitor to the page a form was on, and false otherwise. Only a local path is returned (see router.LocalURL), so it can be redirected to safely.

func Scheme added in v0.12.0

func Scheme(r *http.Request) string

Scheme returns the scheme a request arrived with, "https" or "http": its own TLS state, or, when the request came from a trusted proxy (see FromTrustedProxy), what the proxy reports in X-Forwarded-Proto, or else in the proto of Forwarded. The headers alone are not believed, since any client can send them. Of a list, the last entry counts: the one the trusted proxy itself added.

Use it, for example, to mark a cookie Secure:

http.SetCookie(w, &http.Cookie{Name: "vote", Value: id, Secure: middleware.Scheme(req) == "https"})

func SecurityHeaders added in v0.9.0

func SecurityHeaders(options ...SecurityHeadersOptions) func(http.Handler) http.Handler

SecurityHeaders returns middleware that sets security headers on every response. Without options it sets X-Content-Type-Options: nosniff, X-Frame-Options: DENY and Referrer-Policy: no-referrer; options replace those defaults entirely. The headers replace values of the same name set before it runs, and the handler can still change them, except Content-Security-Policy, which is merged (see CSP).

func SplitCSP added in v0.12.0

func SplitCSP(values []string) []string

SplitCSP splits the values of a Content-Security-Policy header into the policies they hold: a header may be given several times, and one value may list several policies separated by ",". A browser enforces every one of them. Empty policies are left out.

func Timeout added in v0.9.0

func Timeout(timeout time.Duration) func(http.Handler) http.Handler

func Timer

func Timer(next http.Handler) http.Handler

Timer logs how long every request took, with slog.Default(). See TimerWithOptions.

func TimerWithOptions added in v0.12.0

func TimerWithOptions(options TimerOptions) func(http.Handler) http.Handler

TimerWithOptions returns middleware that logs the duration, method and path of every request when it is done.

func TryRealIP added in v0.12.0

func TryRealIP(options RealIPOptions) (func(http.Handler) http.Handler, error)

TryRealIP is RealIPWithOptions returning an error for an invalid trusted proxy instead of panicking.

func ValidRequestID added in v0.12.0

func ValidRequestID(id string) bool

ValidRequestID reports whether id is an acceptable incoming request ID: at most 64 letters, digits and -_.: characters, so that whatever a client sends cannot break a log line or grow it much.

func WeakETag added in v0.12.0

func WeakETag(body []byte) string

WeakETag returns a weak validator of a body: weak, because the body may go out compressed or not, which changes its bytes but not what it shows.

func WithRequestID added in v0.12.0

func WithRequestID(ctx context.Context, requestID string) context.Context

WithRequestID returns ctx carrying requestID for GetRequestID.

Types

type BodyLimitOptions added in v0.12.0

type BodyLimitOptions struct {
	// OnTooLarge, when set, answers requests whose Content-Length is over
	// the limit before the handler runs, for example with a page of the
	// application's own. Without it, such requests reach the handler, whose
	// reads of the body fail, as for bodies without a length.
	OnTooLarge http.Handler
}

BodyLimitOptions configures BodyLimit.

type CORSOptions added in v0.2.0

type CORSOptions struct {
	AllowedOrigins   []string
	AllowedMethods   []string
	AllowedHeaders   []string
	ExposedHeaders   []string
	AllowCredentials bool
	MaxAge           int
}

type CSPPolicy added in v0.12.0

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

CSPPolicy is one Content-Security-Policy: its directives in the order they were added, each with its sources, such as "script-src" with "'self'" and "'sha256-…'". The zero value is an empty policy, ready to use. Directive names are kept in lower case; sources are kept as given, without duplicates. A CSPPolicy is not safe for concurrent changes; Clone it to change a shared one.

policy := middleware.ParseCSP(middleware.StrictCSP)
policy.Add("script-src", "'sha256-…'")
policy.Set("frame-ancestors", "'none'")
header.Set("Content-Security-Policy", policy.String())

func ParseCSP added in v0.12.0

func ParseCSP(policy string) CSPPolicy

ParseCSP parses one serialized policy, such as the value of a Content-Security-Policy header: directives separated by ";", each a name followed by its sources, separated by white space. Names are case insensitive. As browsers do, it skips empty directives and names with other characters than letters, digits and "-", and keeps the first of directives with the same name. A header value that holds several policies separated by "," is a list; see SplitCSP.

func (*CSPPolicy) Add added in v0.12.0

func (p *CSPPolicy) Add(name string, sources ...string)

Add adds sources to the named directive, leaving out those it has, and so only ever allows more:

  • A policy without the directive gets it at the end, starting from the sources of the directive browsers fall back to, so adding a hash to script-src of "default-src 'self'" gives "script-src 'self' 'sha256-…'", not a script-src that allows the hash alone. Without a fallback in the policy the directive starts empty.
  • 'none' is dropped when other sources join it, and adding 'none' to a directive with other sources changes nothing.

Keep in mind that browsers ignore 'unsafe-inline' in a directive that has a hash or a nonce, and that script-src-elem, when present, decides for <script> elements rather than script-src. An invalid name is ignored.

func (CSPPolicy) Clone added in v0.12.0

func (p CSPPolicy) Clone() CSPPolicy

Clone returns a copy of the policy that shares nothing with it.

func (*CSPPolicy) Delete added in v0.12.0

func (p *CSPPolicy) Delete(name string)

Delete removes the named directive.

func (CSPPolicy) IsZero added in v0.12.0

func (p CSPPolicy) IsZero() bool

IsZero reports whether the policy has no directives.

func (*CSPPolicy) Override added in v0.12.0

func (p *CSPPolicy) Override(other CSPPolicy)

Override sets every directive of other in p, as Set does: a directive of both gets the sources of other, in its place in p, and directives only other has are added at the end, in their order.

func (*CSPPolicy) Set added in v0.12.0

func (p *CSPPolicy) Set(name string, sources ...string)

Set sets the named directive to exactly sources, without duplicates, replacing its sources if the policy has it, in its place, and adding it at the end otherwise. A directive without sources, such as "upgrade-insecure-requests", is set with none. An invalid name is ignored.

func (CSPPolicy) Sources added in v0.12.0

func (p CSPPolicy) Sources(name string) ([]string, bool)

Sources returns a copy of the sources of the named directive, and whether the policy has it. It does not follow fallbacks: a policy with default-src but without script-src has no script-src.

func (CSPPolicy) String added in v0.12.0

func (p CSPPolicy) String() string

String returns the policy as a header value: the directives in order, separated by "; ", each name followed by its sources.

type CacheOptions added in v0.12.0

type CacheOptions struct {
	// Key returns the key a request's response is cached under, and false
	// for a request that is not cached. GET and HEAD requests share keys.
	// Default CacheKey: the scheme, host, path and query.
	Key func(*http.Request) (string, bool)

	// Version returns the version of the content a request would see now,
	// such as a counter the application raises on every change. An entry
	// stored at another version is stale and rendered again. Default: the
	// version is always 0, and entries only expire.
	Version func(*http.Request) uint64

	// Skip, when set, keeps the requests it reports true for away from the
	// cache: they are neither served from it nor stored, such as requests
	// of signed-in visitors.
	Skip func(*http.Request) bool

	// TTL is how long an entry is used. Default DefaultCacheTTL; a negative
	// TTL keeps entries until their version changes or they are evicted.
	TTL time.Duration

	// MaxBytes bounds the memory of all entries: the least recently used
	// are evicted beyond it. Default DefaultCacheMaxBytes.
	MaxBytes int64

	// MaxEntryBytes is the largest response body stored: a larger one goes
	// out as it is written, uncached. Default DefaultCacheMaxEntryBytes.
	MaxEntryBytes int64

	// MaxEntries, when above zero, bounds the number of entries too.
	MaxEntries int

	// MaxVarySets is how many different Vary lists the responses of one
	// key may name, each with entries of its own, such as a whole page and
	// an HTMX navigation of the same address that vary on other request
	// headers. Beyond it the entries of the list stored least recently are
	// evicted. Default DefaultCacheMaxVarySets.
	MaxVarySets int

	// VaryValue, when set, returns the value of a request header a
	// response varies on (Vary) as the variant of an entry is told by,
	// such as only the cookies that change a page out of the Cookie
	// header, and false for the default: the header's values, and of
	// Accept-Encoding whether gzip is accepted.
	VaryValue func(r *http.Request, name string) (string, bool)

	// NotFound stores responses with status 404 Not Found as well as those
	// with 200 OK.
	NotFound bool

	// StatusHeader, when set, names a response header, such as X-Cache,
	// that says how the cache answered: hit, miss or skip. For development.
	StatusHeader string
}

CacheOptions configures NewCache.

type CacheStats added in v0.12.0

type CacheStats struct {
	// Hits are requests answered from the cache, Misses those it rendered
	// (and stored, when the response could be), Skips the GET and HEAD
	// requests Skip or Key kept from it.
	Hits   uint64
	Misses uint64
	Skips  uint64

	// Evictions are entries removed to stay within MaxBytes, MaxEntries or
	// MaxVarySets.
	Evictions uint64

	// Entries and Bytes are what the cache holds now.
	Entries int
	Bytes   int64
}

CacheStats are the counts of a ResponseCache.

type CompressOptions added in v0.12.0

type CompressOptions struct {
	// Enabled, when set, decides per request whether responses may be
	// compressed, for example from a site setting. Default always.
	Enabled func(*http.Request) bool

	// MinSize is the smallest body that is compressed. Default
	// DefaultCompressMinSize; a negative value compresses every body.
	MinSize int

	// Level is the gzip level, such as gzip.BestSpeed. Default
	// gzip.DefaultCompression.
	Level int

	// Types, when set, decides by Content-Type which responses are worth
	// compressing. Default CompressibleType.
	Types func(contentType string) bool
}

CompressOptions configures Compress.

type ETagOptions added in v0.12.0

type ETagOptions struct {
	// Types are the media types of the responses that get an ETag, on the
	// other conditions of CacheableTypes. An entry is a media type such as
	// "application/json", all subtypes of a type such as "text/*", or a
	// structured syntax suffix such as "+xml", which takes
	// "application/atom+xml" and "image/svg+xml". Default DefaultETagTypes.
	// Types is not used when Cacheable is set.
	Types []string

	// Cacheable decides by its status and headers, once the handler starts
	// the body, whether a response gets an ETag: it is then held until it
	// is complete. Default CacheableTypes of Types.
	Cacheable func(status int, header http.Header) bool

	// CacheControl, when set, returns the Cache-Control header for
	// responses that get an ETag and have no Cache-Control of their own,
	// such as "no-cache", or "public, max-age=60" for a request without a
	// query.
	CacheControl func(*http.Request) string
}

ETagOptions configures ETag.

type Limiter added in v0.12.0

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

Limiter allows each key at most a number of events per sliding window, counted in memory. It is safe for concurrent use. A nil *Limiter allows everything.

func NewLimiter added in v0.12.0

func NewLimiter(limit int, window time.Duration) *Limiter

NewLimiter returns a limiter of limit events per window for each key, for limits RateLimit does not fit, such as counting only some requests of a handler. It returns nil, which allows everything, when limit or window is zero or less.

func (*Limiter) Allow added in v0.12.0

func (l *Limiter) Allow(key string, now time.Time) bool

Allow counts an event of key at now and reports whether it is within the limit. Events over the limit are not counted.

type LoggerOptions added in v0.9.0

type LoggerOptions struct {
	// Logger receives one record per request. Default slog.Default().
	Logger *slog.Logger

	// Level is the level of the records. Default slog.LevelInfo.
	Level slog.Level

	// OmitQuery logs the path without the query string, which may carry
	// tokens or personal data, under the same "uri" key.
	OmitQuery bool

	// Attrs, when set, returns attributes to add to the record of a
	// request, such as the name of the route that served it
	// (router.MatchedRoute).
	Attrs func(*http.Request, ResponseInfo) []slog.Attr
}

LoggerOptions configures Logger.

type RateLimitOptions added in v0.12.0

type RateLimitOptions struct {
	// Limit is how many requests a client may make per Window. Zero or less
	// turns the limit off.
	Limit int

	// Window is the sliding window requests are counted in. Zero or less
	// turns the limit off.
	Window time.Duration

	// Key returns who a request is counted for. Default ClientIP.
	Key func(*http.Request) string

	// Skip, when set, leaves the requests it reports true for uncounted and
	// unlimited, such as safe methods (SafeMethod) when only posts are
	// limited.
	Skip func(*http.Request) bool

	// OnLimit answers requests over the limit. Default 429 Too Many
	// Requests.
	OnLimit http.Handler
}

RateLimitOptions configures RateLimit.

type RealIPOptions added in v0.9.0

type RealIPOptions struct {
	// TrustedProxies lists the proxies whose forwarding headers are believed,
	// as IP addresses or CIDR ranges, for example "10.0.0.0/8" or "127.0.0.1".
	// Without trusted proxies the forwarding headers are ignored, since any
	// client can send them. Trusting "0.0.0.0/0" and "::/0" believes every
	// client and is only safe when the application cannot be reached except
	// through its proxies.
	TrustedProxies []string
}

RealIPOptions configures RealIPWithOptions.

type RecoverOptions added in v0.12.0

type RecoverOptions struct {
	// Logger receives the panic, with the request and the stack. Default
	// slog.Default().
	Logger *slog.Logger
}

RecoverOptions configures RecoverWithOptions.

type RequestIDOptions added in v0.12.0

type RequestIDOptions struct {
	// Header carries the ID in requests and responses. Default X-Request-ID.
	Header string

	// Generate returns a new ID for requests without a valid one. Default 32
	// random hex characters.
	Generate func() string

	// WithContext, when set, is called with the request context and the ID,
	// and its result becomes the request context. Use it to hand the ID to
	// other packages as well, for example a metrics or tracing library.
	WithContext func(ctx context.Context, id string) context.Context
}

RequestIDOptions configures RequestIDWithOptions.

type ResponseCache added in v0.12.0

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

ResponseCache keeps complete responses in memory and serves them again without calling the handler: see NewCache. Its methods are safe for concurrent use.

func NewCache added in v0.12.0

func NewCache(options ...CacheOptions) *ResponseCache

NewCache returns an output cache, whose Middleware keeps complete responses to GET requests in memory and serves them again, to GET and HEAD requests, without calling the handler. Stats reports how it does.

A response is stored when its status is 200 OK (or 404 Not Found, with NotFound), it is complete and not flushed, its body is at most MaxEntryBytes, and it sets no cookie, says neither private nor no-store in Cache-Control, and does not say Vary: *. Requests with Authorization, Range or Upgrade headers, and those Skip reports or Key refuses, are not cached.

Entries are kept by Key and by the values of the request headers the response names in Vary, Accept-Encoding by whether gzip is accepted. The responses of a key may name up to MaxVarySets different Vary lists, such as a whole page and an HTMX navigation: a request is answered by the entry of the list stored most recently that has one for it. An entry is stale once TTL has passed or Version returns another version than it was stored at. Many requests for a missing or stale entry at once render it once: the others wait and are served from the entry. The least recently used entries are evicted beyond MaxBytes.

Stored are the status, the body and the headers set inside the cache; headers set outside it, before it runs, such as a request ID, stay those of each request.

Conditional GET and HEAD requests are answered 304 Not Modified by the cache itself, from a fresh entry or from the response it renders, which is at the current version either way (RFC 9110, 13.2.2): a request with If-None-Match when it names the stored ETag, otherwise a request with If-Modified-Since when the stored Last-Modified is not newer, to the second. The cache renders the whole response to store it, so it passes neither header on to the handler. An entry stale by version or TTL answers no condition: the request renders the response again.

Put Compress, ETag and LastModified inside the cache, so the compressed body and the ETag are stored with the entry, and LastModified answers the requests the cache skips:

cache := middleware.NewCache(middleware.CacheOptions{
	Version: content.Version,
	Skip:    signedIn,
})
r.Use(cache.Middleware)
r.Use(middleware.Compress())
r.Use(middleware.ETag())
r.Use(middleware.LastModified)

func (*ResponseCache) Middleware added in v0.12.0

func (c *ResponseCache) Middleware(next http.Handler) http.Handler

Middleware serves requests from the cache, and stores the responses of the handler when they can be.

func (*ResponseCache) Stats added in v0.12.0

func (c *ResponseCache) Stats() CacheStats

Stats returns the counts of the cache.

type ResponseInfo added in v0.12.0

type ResponseInfo struct {
	// Status is the response status: 200 when the handler wrote a body
	// without a status, 101 for a hijacked connection, and 500 for a
	// request whose handler panicked before it wrote a status.
	Status int

	// Bytes is the size of the body written, before any compression the
	// writer below does.
	Bytes int64

	// Duration is how long the request took.
	Duration time.Duration

	// Panicked is set when a panic passed through, such as
	// http.ErrAbortHandler after the response started; the panic goes on
	// after the observer ran.
	Panicked bool

	// Header is the response's header map as the handler left it: the
	// headers written with the response, such as Location or
	// X-Robots-Tag, and any set after the status was written, which were
	// not sent. It is the writer's own map, not a copy: read it, do not
	// change it.
	Header http.Header
}

ResponseInfo is what Observe and Logger know about a response once it is done.

type SecurityHeadersOptions added in v0.9.0

type SecurityHeadersOptions struct {
	ContentTypeOptions string // X-Content-Type-Options, such as "nosniff"
	FrameOptions       string // X-Frame-Options, such as "DENY"
	ReferrerPolicy     string // Referrer-Policy, such as "no-referrer"

	// ContentSecurityPolicy is merged into the Content-Security-Policy of
	// the response directive by directive, as CSP does: "frame-ancestors
	// 'none'" replaces the frame-ancestors of a policy set outside and
	// keeps its other directives, and the handler can still change single
	// directives and add sources (AddCSPSource).
	ContentSecurityPolicy string

	// XRobotsTag is the X-Robots-Tag header, such as "noindex, nofollow"
	// for pages search engines should leave alone, such as an admin.
	XRobotsTag string
}

SecurityHeadersOptions configures SecurityHeaders. Headers left empty are not set.

type TimerOptions added in v0.12.0

type TimerOptions struct {
	// Logger receives one record per request. Default slog.Default().
	Logger *slog.Logger

	// Level is the level of the records. Default slog.LevelInfo.
	Level slog.Level
}

TimerOptions configures TimerWithOptions.

Jump to

Keyboard shortcuts

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