web

package
v0.0.0-...-d267593 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package web provides the HTTP adapter toolkit for Forge handlers: JSON responses, a single error response format, strict request decoding and the request-scoped values (request ID, logger) set by the HTTP server.

Index

Constants

View Source
const (
	MaxFlashCookieBytes = 2048
	MaxFlashMessages    = 4
	MaxFlashTextBytes   = 512
)

Flash limits: the encoded cookie value, the messages per response and the bytes of one message.

View Source
const (
	MaxFormBytes  = 1 << 20
	MaxFormFields = 1000
)

Limits for application/x-www-form-urlencoded bodies. The server's body limit (FORGE_HTTP_MAX_BODY_BYTES) still applies and may be lower.

View Source
const (
	// MethodOverrideField carries PUT, PATCH or DELETE in a POST form; see
	// MethodOverride.
	MethodOverrideField = "_method"
	// CSRFTokenField carries the CSRF token in HTML forms; see package csrf.
	CSRFTokenField = "authenticity_token"
)

Form fields consumed by Forge middleware. DecodeForm ignores them and a struct may not bind them.

View Source
const FlashCookieName = "__Host-forge_flash"

FlashCookieName is the cookie that carries flash messages to the next request. The __Host- prefix makes browsers refuse it unless it is Secure, has Path=/ and no Domain, so a sibling subdomain cannot plant one.

Variables

View Source
var ErrFlashUnavailable = errors.New("web: flash middleware is not installed")

ErrFlashUnavailable is returned by SetFlash when the Flash middleware is not installed.

View Source
var ErrInvalidFields = fault.New("invalid_fields", "one or more fields are invalid", fault.CategoryInvalid, http.StatusUnprocessableEntity)

ErrInvalidFields is the fault behind FormErrors, so web.Error answers a FormErrors value with 422 invalid_fields.

View Source
var ErrUnsafeRedirect = fault.New("unsafe_redirect", "redirect target must be a local path", fault.CategoryInvalid, http.StatusBadRequest)

ErrUnsafeRedirect is returned by Redirect for a target that is not a local path.

Functions

func AcceptsExplicitly

func AcceptsExplicitly(r *http.Request, mediaType string) bool

AcceptsExplicitly reports whether the Accept header names mediaType itself with a quality above zero. Wildcards ("*/*", "text/*") do not count, so a client that accepts anything is not mistaken for one that asked for a special type, such as Turbo's text/vnd.turbo-stream.html.

func ClientIP

func ClientIP(r *http.Request) string

ClientIP returns the client address of r: the one TrustedProxies.Middleware resolved, or, when no resolver ran, the TCP peer address (the same result as trusting nobody). Login throttles, rate limits, stream caps and audit sources use it. It is a full address; callers that count clients bucket IPv6 addresses themselves (by /64).

func DecodeForm

func DecodeForm(r *http.Request, dst any) error

DecodeForm strictly decodes an application/x-www-form-urlencoded body into the struct dst points to. Only exported fields tagged `form:"name"` are bound; every tagged field is assigned, so an absent checkbox is false and an absent optional (pointer) field is nil.

Supported field types: string, bool, signed and unsigned integers, time.Time (HTML date, datetime-local in UTC, or RFC 3339), uuid.UUID, types implementing encoding.TextUnmarshaler, pointers to these (an empty value is nil), and []string.

Transport problems are faults (see ParseForm); unknown fields and repeated values for a scalar field are 400 invalid_form. Values that do not convert are returned together as FormErrors (422). The _method and authenticity_token fields are consumed by middleware and ignored here.

func DecodeJSON

func DecodeJSON(r *http.Request, dst any) error

DecodeJSON strictly decodes a single JSON object into dst: the content type must be JSON, unknown fields are rejected and trailing data is an error. Size limits are enforced by the server's body limit.

func Error

func Error(w http.ResponseWriter, r *http.Request, err error)

Error writes err as an ErrorBody. Only *fault.Error code and message reach the client; anything else becomes a generic 500. Server-side failures are logged with their full cause chain and the request ID.

func Flash

func Flash(next http.Handler) http.Handler

Flash is middleware that carries one-request messages, such as "Post was created." after a redirect, in the FlashCookieName cookie. It loads the messages the previous response set (see Flashes and FlashText) and expires the cookie in this response, so each message is shown once. A malformed or oversized cookie is discarded.

The cookie is HttpOnly, Secure, SameSite=Lax and unsigned. It carries display text only, never a decision: a client that edits it changes only what its own browser displays, and the text is escaped when rendered. Do not put secrets or personal data in a flash message; it travels in the clear inside the TLS session and the browser's cookie store.

func FlashText

func FlashText(ctx context.Context, kind FlashKind) string

FlashText returns the text of the incoming message of kind, or "".

func IsForm

func IsForm(r *http.Request) bool

IsForm reports whether the request body is declared as application/x-www-form-urlencoded.

func IsLocalPath

func IsLocalPath(target string) bool

IsLocalPath reports whether target is a same-origin absolute path that is safe to use as a redirect location. See Redirect.

func JSON

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

JSON writes value as a JSON response. The value is encoded into a buffer first, so the status line and body are written only when encoding succeeded. A value that cannot be encoded (NaN, a channel, a failing MarshalJSON) is logged and answered with a generic 500 instead of a truncated body under the requested status.

func Logger

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

Logger returns the request-scoped logger, which carries the request ID. It never returns nil.

func MethodOverride

func MethodOverride(next http.Handler) http.Handler

MethodOverride lets HTML forms, which can only send GET and POST, reach PUT, PATCH and DELETE routes: a POST with an application/x-www-form-urlencoded body whose _method field is put, patch or delete (any case) is routed as that method. Nothing else is rewritten: the query string is ignored, other methods and content types pass through, and any other _method value is 400 invalid_method_override. The body is read through ParseForm, so its size and field limits apply. Install it before routing and outside (before) CSRF protection, so CSRF checks and exemptions see the routed method. An override never produces a safe method, and the csrf package never exempts a form POST that carries _method.

func Negotiate

func Negotiate(r *http.Request, offers ...string) string

Negotiate returns the offer (a media type such as "text/html") the Accept header prefers, using quality values and the most specific matching range for each offer. Ties go to the earlier offer, and a request without Accept gets the first offer. It returns "" when every offer is refused (q=0).

func ParseForm

func ParseForm(r *http.Request) (url.Values, error)

ParseForm reads an application/x-www-form-urlencoded body once, with a size and field-count bound, and stores it in r.PostForm so later calls (and r.FormValue) reuse it. Query-string values are never included. Errors are faults: 415 unsupported_media_type, 413 body_too_large, 400 invalid_form or unreadable_body.

func PrefersHTML

func PrefersHTML(r *http.Request) bool

PrefersHTML reports whether the client prefers text/html over application/json. Browser navigations and form submissions do; API clients, fetch() defaults ("*/*") and requests without Accept do not.

func RecordRoute

func RecordRoute(ctx context.Context, pattern string)

RecordRoute stores the matched pattern in the RouteInfo carried by ctx, if any. It does not allocate.

func Redirect

func Redirect(w http.ResponseWriter, target string) error

Redirect answers 303 See Other to a local path such as "/posts/0190..." or "/posts?page=2". It writes nothing and returns ErrUnsafeRedirect when target could leave the site: absolute and scheme-relative URLs ("//host"), backslashes (which browsers treat as slashes), and control, space or non-ASCII bytes are all rejected. Use it with values derived from user input (for example a return_to parameter) as well as constants.

func RequestID

func RequestID(ctx context.Context) string

RequestID returns the ID the server assigned to the current request.

func SetFlash

func SetFlash(w http.ResponseWriter, r *http.Request, kind FlashKind, text string) error

SetFlash stores a message for the next request, typically just before Redirect. Setting a kind twice keeps the last text. It must be called before the response is written, and fails for an invalid kind, invalid UTF-8, text above MaxFlashTextBytes, or a cookie above MaxFlashCookieBytes.

func WithLogger

func WithLogger(ctx context.Context, logger *slog.Logger) context.Context

WithLogger returns a context that carries the request-scoped logger.

func WithRequestID

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

WithRequestID returns a context that carries the request ID; the server sets it for every request.

func WithRouteInfo

func WithRouteInfo(ctx context.Context, info *RouteInfo) context.Context

WithRouteInfo returns ctx carrying info for the router to fill.

Types

type ErrorBody

type ErrorBody struct {
	Error ErrorDetail `json:"error"`
}

ErrorBody is the only error format Forge APIs return.

type ErrorDetail

type ErrorDetail struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	RequestID string `json:"request_id,omitempty"`
}

ErrorDetail is the content of an ErrorBody: a stable code, a safe message and the request ID for correlation.

type FieldError

type FieldError struct {
	Field   string
	Code    string
	Message string
}

FieldError is one invalid form field. Message is safe to show to the user and never contains the submitted value.

func (FieldError) Error

func (e FieldError) Error() string

type FlashKind

type FlashKind string

FlashKind names a flash message. Kinds are lowercase letters, digits and underscores, starting with a letter, up to 32 bytes.

const (
	FlashNotice FlashKind = "notice"
	FlashAlert  FlashKind = "alert"
)

The built-in flash kinds, which the generated layout renders.

type FlashMessage

type FlashMessage struct {
	Kind FlashKind
	Text string
}

FlashMessage is one message. Templates render Text with contextual escaping, like any other value.

func Flashes

func Flashes(ctx context.Context) []FlashMessage

Flashes returns the messages set by the previous response, in the order they were set.

type FormErrors

type FormErrors []FieldError

FormErrors collects field errors for re-rendering a form with 422. It unwraps to ErrInvalidFields.

func (*FormErrors) Add

func (e *FormErrors) Add(field, code, message string)

Add appends a field error, for example a domain validation failure.

func (FormErrors) Error

func (e FormErrors) Error() string

func (FormErrors) Has

func (e FormErrors) Has(field string) bool

Has reports whether field has at least one error.

func (FormErrors) Message

func (e FormErrors) Message(field string) string

Message returns the first message for field, or "".

func (FormErrors) Messages

func (e FormErrors) Messages(field string) []string

Messages returns every message for field.

func (FormErrors) Unwrap

func (e FormErrors) Unwrap() error

type RouteInfo

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

RouteInfo receives the ServeMux pattern of the route that serves a request, such as "GET /users/{id}". Middleware that runs before routing (telemetry, access logs) installs one with WithRouteInfo and reads it after the handler returns; the router fills it through RecordRoute. Labeling requests by pattern instead of raw path keeps metric cardinality bounded.

It is safe for concurrent use: with http.TimeoutHandler the router may record the route on another goroutine after the middleware has read it. The zero value is ready to use.

func RouteInfoFromContext

func RouteInfoFromContext(ctx context.Context) *RouteInfo

RouteInfoFromContext returns the RouteInfo ctx carries, or nil. Middleware that needs the matched pattern reuses it instead of installing a second one, which would hide the route from the middleware outside it.

func (*RouteInfo) Pattern

func (r *RouteInfo) Pattern() string

Pattern returns the recorded pattern, or "" when no route matched yet.

type TrustedProxies

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

TrustedProxies is the set of networks whose X-Forwarded-For header is believed (ADR 0037). The zero value trusts nobody, so the client address is always the TCP peer. It is immutable and safe for concurrent use.

func NewTrustedProxies

func NewTrustedProxies(prefixes ...netip.Prefix) (TrustedProxies, error)

NewTrustedProxies returns the set of the given prefixes (for a single proxy, a /32 or /128); config.HTTP.TrustedProxies holds them, parsed from FORGE_HTTP_TRUSTED_PROXIES under the same rules, which are config.CheckTrustedProxy: it refuses invalid prefixes, IPv4 prefixes shorter than /8 and IPv6 prefixes shorter than /16 (they would let almost any client choose its address) and IPv4-mapped IPv6 prefixes (peers are compared in their IPv4 form, so such a prefix would silently never match; write the IPv4 form).

func (TrustedProxies) Empty

func (t TrustedProxies) Empty() bool

Empty reports whether nobody is trusted.

func (TrustedProxies) Middleware

func (t TrustedProxies) Middleware(next http.Handler) http.Handler

Middleware stores the resolved client address in each request's context, where ClientIP reads it. The HTTP server installs it outside all other middleware (see httpserver.NewWithOptions).

func (TrustedProxies) Prefixes

func (t TrustedProxies) Prefixes() []netip.Prefix

Prefixes returns a copy of the trusted prefixes.

func (TrustedProxies) Resolve

func (t TrustedProxies) Resolve(r *http.Request) string

Resolve returns the address of the client that sent r.

When the TCP peer (r.RemoteAddr) is not a trusted proxy, the result is the peer and every forwarding header is ignored. When it is, X-Forwarded-For is read from the right (the entry the peer itself appended) towards the left, skipping trusted proxies, and the first untrusted address is the client: entries to its left were written by the client or by proxies nobody vouches for. If an entry on that walk is empty or not a bare IP address, the result is the peer: a malformed chain is never resolved to a value the client may have chosen. If every entry is trusted, the leftmost one is the client. Forwarded and X-Real-IP are never read.

Addresses are returned in canonical form, without zone, with IPv4-mapped IPv6 addresses as IPv4. A peer that is not an IP address (a Unix socket, a test's "pipe") is returned unchanged without its port.

Jump to

Keyboard shortcuts

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