Help us shape the future of Go! Take the 2026 Go Developer Survey

httpx

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: GPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package httpx is the server's HTTP plumbing: the hardened http.Server, the middleware every route wears, and the decoders that read a request body.

It exists because the settings in Harden are the ones that get forgotten. A timeout that is not set is a slowloris; a body that is not capped is a memory limit; a JSON decoder that ignores unknown fields turns a client's typo into a silent default. This package is every one of those decided once and applied centrally, so that no route can be created without them.

Routing is net/http.ServeMux and nothing else. Go 1.22's method and wildcard patterns cover the whole API, and a framework on the request path would be one more dependency between a driver's lap and the database.

Index

Constants

View Source
const (
	// ReadHeaderTimeout is the slowloris guard: a client that opens a
	// connection and dribbles a header has five seconds to finish.
	ReadHeaderTimeout = 5 * time.Second
	// ReadTimeout and WriteTimeout bound a whole request and a whole response.
	ReadTimeout  = 30 * time.Second
	WriteTimeout = 30 * time.Second
	// IdleTimeout is how long a kept-alive connection may sit unused.
	IdleTimeout = 120 * time.Second
	// MaxHeaderBytes caps the header block.
	MaxHeaderBytes = 16 << 10
	// ShutdownTimeout is how long a graceful shutdown waits for requests in
	// flight before the process stops anyway.
	ShutdownTimeout = 20 * time.Second
)

The D-3 hardening table, as constants. Changing one of these is a decision, so it is made here rather than at a call site.

View Source
const FormLimit int64 = 64 << 10

FormLimit is the body cap on an HTML form post. The wizard and the admin panel send a handful of short fields; sixty-four kilobytes is generous for that and nowhere near enough to be a memory problem.

Variables

View Source
var ErrMalformed = errors.New("httpx: the request body is not readable")

ErrMalformed is returned when a body is not what it claimed to be.

View Source
var ErrTooLarge = errors.New("httpx: the request body is too large")

ErrTooLarge is returned when a body is longer than the route's cap.

Functions

func APIError

func APIError(w http.ResponseWriter, code wire.Code, message string)

APIError writes the v1 error envelope for one code and message, with the status the contract pairs with that code. message is shown to the driver as written, so it must be a complete sentence with no jargon.

func BearerToken added in v0.2.0

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

BearerToken pulls the token out of a request's Authorization header, or reports that there is no Bearer credential — a Basic one, or none at all.

func Chain

func Chain(h http.Handler, ms ...Middleware) http.Handler

Chain wraps h in every middleware, outermost first, so Chain(h, a, b) reads in the order the request travels: a, then b, then h.

func ClientHost

func ClientHost(r *http.Request) string

ClientHost is [clientHost] for the packages that rate-limit by address.

It deliberately does not read X-Forwarded-For. Behind the operator's own proxy that header is trustworthy and in front of one it is whatever the caller typed, and this package cannot tell which it is in. Rate limiting the proxy's own address is wrong but safe; trusting a forged header is neither.

func DecodeJSON

func DecodeJSON(w http.ResponseWriter, r *http.Request, dst any, limit int64) error

DecodeJSON reads one JSON document from the body into dst.

Three things are true of it and none of them are the standard library's defaults: the body is capped before a byte is allocated, an unknown field is an error rather than a silent default, and trailing content after the document is an error rather than being ignored. The first is a memory limit, and the other two turn a client's typo into a 422 it can see in a log rather than into a value that quietly went missing.

func DecodeJSONFingerprint

func DecodeJSONFingerprint(w http.ResponseWriter, r *http.Request, dst any, limit int64) ([]byte, error)

DecodeJSONFingerprint reads one JSON document into dst and returns a fingerprint of the request that carried it, for the idempotency table.

The fingerprint covers the method, the path and the body bytes, so the same key presented on two different endpoints is a conflict rather than a replay of the wrong answer. It is computed as the body streams past the decoder, so no second copy of the payload is made and the cap is still enforced before anything is allocated.

func Harden

func Harden(s *http.Server) *http.Server

Harden applies the D-3 table to a server. Every http.Server this binary creates goes through it.

func JSON

func JSON(w http.ResponseWriter, status int, body any)

JSON writes one JSON document with a status. It is the only place a success body is encoded, so the header and the body cannot disagree about what is being sent.

func Limit

func Limit(w http.ResponseWriter, r *http.Request, limit int64)

Limit caps the body of one route, per D-3: a cap belongs to an endpoint, not to the server, because the endpoint is the only place that knows what a reasonable body looks like.

func NewServer

func NewServer(addr string, h http.Handler) *http.Server

NewServer builds a hardened server for one address and handler.

func NoStore

func NoStore(next http.Handler) http.Handler

NoStore stops a browser and any proxy from keeping a page. Every page the admin panel serves is either a form with a token in it or a view of private data, so none of them may be cached.

func ParseBearer added in v0.2.0

func ParseBearer(header string) (string, bool)

ParseBearer is BearerToken on the header's value. The scheme is matched case-insensitively because RFC 7235 says it is case-insensitive, and a client that sends "bearer" is not wrong.

func ParseForm

func ParseForm(w http.ResponseWriter, r *http.Request, limit int64) error

ParseForm reads an HTML form body, capped at max. It is the form twin of DecodeJSON and exists so no handler calls r.ParseForm without a cap.

func ParseUpload

func ParseUpload(w http.ResponseWriter, r *http.Request, limit int64) error

ParseUpload reads a form that carries a file.

It is separate from ParseForm because the two need different caps. An ordinary form is a few text fields and is held to FormLimit; a form with a file on it is as large as the file, and the caller says how large that may be. The whole body is still capped, so an upload that claims to be small and keeps arriving is cut off rather than kept.

Nothing is spooled to disk: the in-memory allowance is the same limit, so a file inside it never reaches the file system and a file outside it is refused before any of it does.

which is the bound the rule is asking for; the analyser cannot see through the helper.

func Problem

func Problem(w http.ResponseWriter, r *http.Request, status int, message string)

Problem answers a request that cannot be served. A caller that wants JSON gets the v1 error envelope; a browser gets one line of plain text, because the alternative is an error page that says less.

func SecureHeaders

func SecureHeaders(next http.Handler) http.Handler

SecureHeaders sets the response headers that cost nothing and close off a whole class of problem. The content security policy is strict because the admin panel is server-rendered with no framework and no content delivery network: there is no inline script to allow and nothing to fetch from elsewhere.

func Serve

func Serve(ctx context.Context, srv *http.Server, ln net.Listener) error

Serve runs srv on ln until ctx is cancelled, then shuts it down gracefully. It returns nil for an ordinary shutdown, so a caller can treat any non-nil result as a fault.

Types

type Limiter

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

Limiter is a token bucket per key, plus a ceiling over all keys.

It is built for the setup token endpoint, which needs it first: an unauthenticated form that compares a secret has to cost an attacker something. The same type carries the per-device limits, which is why the limits are values rather than constants.

func NewLimiter

func NewLimiter(rate, burst, globalRate, globalBurst float64) *Limiter

NewLimiter builds a limiter allowing burst attempts immediately and then one every 1/rate seconds, per key, with globalBurst and globalRate the same over every key at once.

func (*Limiter) Allow

func (l *Limiter) Allow(key string) bool

Allow reports whether one attempt from key may proceed, and takes a token if it may.

func (*Limiter) Retry

func (l *Limiter) Retry() time.Duration

Retry is how long a caller should wait before the next attempt would be allowed. It is an estimate for a message, not a promise.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is the ordinary shape. Nothing in this server needs another.

func LogRequests

func LogRequests(log *slog.Logger) Middleware

LogRequests writes one line per request. The path is logged and the query string is not, because a query string is where a token ends up when someone takes a shortcut.

func Measure

func Measure(route string, report func(route string, status int, took time.Duration)) Middleware

Measure reports how one route finished, for the metrics port.

The route is given rather than taken from the request, because it is the ServeMux pattern and not the path: a label per driver would be a metric per driver, and a time series that grows with the roster is one nobody can query. It follows that Measure is applied where a route is registered, which is the only place the pattern is known.

A nil report is no middleware at all, so a server with no metrics port pays nothing for the ones it is not collecting.

func Recover

func Recover(log *slog.Logger) Middleware

Recover turns a panic in a handler into a 500 and a log line, rather than into a dead process. A panic mid-session would otherwise lose every driver's laps that were in flight, which makes this correctness rather than polish.

Jump to

Keyboard shortcuts

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