apistockdocs
v0.4 GitHub apistock.dev
Technical/Core packages

httpx

import "apistock.dev/httpx"Source on GitHub

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 unless overridden.

const ProblemContentType#

go
const ProblemContentType = "application/problem+json"

ProblemContentType is the media type of problem responses.

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

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 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) 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). 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 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.

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.

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

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.

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.

v0.4
esc
↑↓ move↵ openesc close