# httpx

```go
import "apistock.dev/httpx"
```

Package httpx provides the HTTP foundation of an apistock app: a server that runs under app.Run with safe timeouts, security middleware, and the RFC 9457 problem+json error contract with an application-owned error mapping (ADR-0018).

A typical middleware chain, outermost first:

	handler := httpx.Chain(mux,
		httpx.Recover(logger),
		httpx.RequestID(),
		httpx.AccessLog(logger),
		httpx.SecureHeaders(httpx.SecureHeadersOptions{}),
		cors,
		crossOrigin,
		httpx.BodyLimit(1<<20),
	)

Stability: pre-1.0 (ADR-0015).

## Constants

### const DefaultReadHeaderTimeout, …

```go
const (
	DefaultReadHeaderTimeout = 5 * time.Second
	DefaultReadTimeout       = 30 * time.Second
	DefaultWriteTimeout      = 60 * time.Second
	DefaultIdleTimeout       = 120 * time.Second
	DefaultShutdownTimeout   = 20 * time.Second
	DefaultMaxHeaderBytes    = 1 << 20
)
```

Server timeouts used by [NewServer](#NewServer) unless overridden.

### const ProblemContentType

```go
const ProblemContentType = "application/problem+json"
```

ProblemContentType is the media type of problem responses.

## Variables

### var ErrTrustAll

```go
var ErrTrustAll = errors.New("httpx: a trusted proxy range can't cover every address")
```

ErrTrustAll reports a trusted-proxy range covering every address, which would let any client choose its own IP.

## Functions

### func Chain

```go
func Chain(h http.Handler, middlewares ...Middleware) http.Handler
```

Chain wraps h with middlewares. The first middleware is the outermost: it sees the request first and the response last.

### func DefaultCode

```go
func DefaultCode(status int) string
```

DefaultCode returns the generic code for a status without a mapping, such as "not\_found" for 404.

### func ParseTrustedProxies

```go
func ParseTrustedProxies(list string) ([]netip.Prefix, error)
```

ParseTrustedProxies reads a comma-separated list of CIDR ranges or single addresses, such as "10.0.0.0/8, 192.0.2.10". An empty list trusts nothing. It refuses ranges covering every IPv4 or IPv6 address ([ErrTrustAll](#ErrTrustAll)).

### func WriteProblem

```go
func WriteProblem(w http.ResponseWriter, r *http.Request, p *Problem)
```

WriteProblem writes p as application/problem+json, filling the request ID from the request context when empty.

## Types

### type CORSOptions

```go
type CORSOptions struct {
	// AllowedOrigins are exact origins such as "https://app.example.com".
	// Empty disables CORS.
	AllowedOrigins []string
	AllowedMethods []string // default: GET, POST, PUT, PATCH, DELETE
	AllowedHeaders []string // default: Authorization, Content-Type, X-Request-ID
	ExposedHeaders []string // default: X-Request-ID, Retry-After
	// AllowCredentials allows cookies. It can't be combined with a wildcard origin.
	AllowCredentials bool
	MaxAge           time.Duration // preflight cache; default 10 minutes
}
```

CORSOptions configure [CORS](#CORS).

### type FieldError

```go
type FieldError struct {
	Location string `json:"location,omitempty" doc:"Where the error occurred" example:"body.name"`
	Message  string `json:"message" doc:"What is wrong" example:"expected length >= 1"`
}
```

FieldError describes one invalid input field. It never echoes the submitted value, which may be a password or personal data.

### type Mapper

```go
type Mapper struct {
	// contains filtered or unexported fields
}
```

Mapper turns errors into problems using application-owned mappings. Errors without a mapping become a generic 500 and are logged once with the request ID. It is safe for concurrent use.

#### func NewMapper

```go
func NewMapper(logger *slog.Logger, mappings ...Mapping) (*Mapper, error)
```

NewMapper returns a mapper with the given mappings.

#### func (*Mapper) Add

```go
func (m *Mapper) Add(mappings ...Mapping) error
```

Add registers mappings. Each needs a non-nil error, a 4xx or 5xx status and a snake\_case code. Each error is mapped once; several errors may share a code only with the same status, such as "unauthenticated" returned by different modules.

#### func (*Mapper) Match

```go
func (m *Mapper) Match(err error) (*Problem, bool)
```

Match returns the problem for err if err is (or wraps) a \*[Problem](#Problem) or a mapped error. It never logs.

#### func (*Mapper) Problem

```go
func (m *Mapper) Problem(ctx context.Context, err error) *Problem
```

Problem returns the problem for err. Unmapped errors become 500 "internal\_error" with a generic detail and are logged.

#### func (*Mapper) Write

```go
func (m *Mapper) Write(w http.ResponseWriter, r *http.Request, err error)
```

Write maps err and writes the problem response.

### type Mapping

```go
type Mapping struct {
	Err    error
	Status int
	Code   string
	Detail string
}
```

A Mapping maps a sentinel error (matched with [errors.Is](https://pkg.go.dev/errors#Is)) to an HTTP status and stable code. Detail defaults to the error's message.

### type Middleware

```go
type Middleware func(http.Handler) http.Handler
```

Middleware wraps an http.Handler.

#### func AccessLog

```go
func AccessLog(logger *slog.Logger) Middleware
```

AccessLog logs one line per request: method, route pattern, status, duration, response size and request ID. Query strings and bodies are never logged.

#### func BodyLimit

```go
func BodyLimit(n int64) Middleware
```

BodyLimit rejects request bodies larger than n bytes with a 413 problem. Bodies without a declared length are cut off at n bytes while reading.

#### func CORS

```go
func CORS(opts CORSOptions) (Middleware, error)
```

CORS returns middleware allowing cross-origin requests from an explicit allowlist. A "\*" origin is rejected when credentials are allowed.

#### func CrossOrigin

```go
func CrossOrigin(trustedOrigins ...string) (Middleware, error)
```

CrossOrigin protects against cross-site request forgery using the browser's Sec-Fetch-Site and Origin headers ([http.CrossOriginProtection](https://pkg.go.dev/net/http#CrossOriginProtection)). Non-browser clients, which send neither header, are allowed. Denied requests receive a 403 problem with code "cross\_origin\_request\_denied".

#### func Recover

```go
func Recover(logger *slog.Logger) Middleware
```

Recover turns a panic into a logged error and a 500 problem response. [http.ErrAbortHandler](https://pkg.go.dev/net/http#ErrAbortHandler) panics are re-raised, as net/http expects.

#### func RequestID

```go
func RequestID() Middleware
```

RequestID accepts a valid incoming X-Request-ID or generates one, stores it in the request context and echoes it in the response header.

#### func SecureHeaders

```go
func SecureHeaders(opts SecureHeadersOptions) Middleware
```

SecureHeaders sets security headers suitable for a JSON API. HTML routes such as /docs set their own Content-Security-Policy.

#### func TrustedProxies

```go
func TrustedProxies(trusted []netip.Prefix) Middleware
```

TrustedProxies sets each request's RemoteAddr to its client's address when it arrives through trusted proxies (ADR-0052). For a request whose peer is in trusted, it walks X-Forwarded-For from the right, skips trusted addresses, and uses the first untrusted one. Requests from other peers keep their address and their forwarding headers are ignored, so clients can't choose their IP. With no trusted ranges it changes nothing.

Install it first, after recovery, so logs, audit events and rate limits see the client.

### type Problem

```go
type Problem struct {
	Type      string       `json:"type,omitempty" doc:"URI identifying the problem type"`
	Title     string       `json:"title" doc:"Short summary of the problem type" example:"Conflict"`
	Status    int          `json:"status" doc:"HTTP status code" example:"409"`
	Code      string       `json:"code" doc:"Stable machine-readable error code" example:"project_name_taken"`
	Detail    string       `json:"detail,omitempty" doc:"Human-readable explanation" example:"project name is already taken"`
	RequestID string       `json:"request_id,omitempty" doc:"Correlates with server logs and traces" example:"req_9f86d081884c7d65"`
	Errors    []FieldError `json:"errors,omitempty" doc:"Field-level validation errors"`
}
```

Problem is an RFC 9457 problem details response extended with a stable machine-readable code and the request ID. Codes are public API; titles and details are not (ADR-0015).

#### func NewProblem

```go
func NewProblem(status int, code, detail string) *Problem
```

NewProblem returns a problem with the standard title for status.

#### func (*Problem) ContentType

```go
func (p *Problem) ContentType(ct string) string
```

ContentType returns application/problem+json for JSON responses.

#### func (*Problem) Error

```go
func (p *Problem) Error() string
```

Error returns the detail, or the title when detail is empty.

#### func (*Problem) GetStatus

```go
func (p *Problem) GetStatus() int
```

GetStatus returns the HTTP status. It lets frameworks such as Huma use a Problem as a status error.

### type SecureHeadersOptions

```go
type SecureHeadersOptions struct {
	// HSTSMaxAge enables Strict-Transport-Security when positive. Enable it
	// only when the app is served exclusively over HTTPS.
	HSTSMaxAge time.Duration
}
```

SecureHeadersOptions configure [SecureHeaders](#SecureHeaders).

### type Server

```go
type Server struct {
	// contains filtered or unexported fields
}
```

Server is an HTTP server that implements app.Runner.

#### func NewServer

```go
func NewServer(addr string, handler http.Handler, opts ...ServerOption) *Server
```

NewServer returns a server for handler listening on addr.

#### func (*Server) Addr

```go
func (s *Server) Addr(ctx context.Context) (string, error)
```

Addr returns the listening address once [Server.Run](#Server.Run) has started listening, blocking until then or until ctx is done. It is useful with port 0 in tests.

#### func (*Server) Run

```go
func (s *Server) Run(ctx context.Context) error
```

Run listens and serves until ctx is done, then shuts down gracefully: it stops accepting connections and waits for in-flight requests up to the shutdown timeout. Run returns nil after a graceful shutdown.

### type ServerOption

```go
type ServerOption interface {
	// contains filtered or unexported methods
}
```

A ServerOption configures a [Server](#Server).

#### func WithErrorLogger

```go
func WithErrorLogger(logger *slog.Logger) ServerOption
```

WithErrorLogger routes the server's internal errors (for example TLS handshake failures) to logger.

#### func WithShutdownTimeout

```go
func WithShutdownTimeout(d time.Duration) ServerOption
```

WithShutdownTimeout sets how long graceful shutdown may take. Default: [DefaultShutdownTimeout](#DefaultShutdownTimeout).

#### func WithTimeouts

```go
func WithTimeouts(readHeader, read, write, idle time.Duration) ServerOption
```

WithTimeouts overrides read-header, read, write and idle timeouts. Zero values keep the defaults.

