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
- Variables
- func AcceptsExplicitly(r *http.Request, mediaType string) bool
- func ClientIP(r *http.Request) string
- func DecodeForm(r *http.Request, dst any) error
- func DecodeJSON(r *http.Request, dst any) error
- func Error(w http.ResponseWriter, r *http.Request, err error)
- func Flash(next http.Handler) http.Handler
- func FlashText(ctx context.Context, kind FlashKind) string
- func IsForm(r *http.Request) bool
- func IsLocalPath(target string) bool
- func JSON(w http.ResponseWriter, status int, value any)
- func Logger(ctx context.Context) *slog.Logger
- func MethodOverride(next http.Handler) http.Handler
- func Negotiate(r *http.Request, offers ...string) string
- func ParseForm(r *http.Request) (url.Values, error)
- func PrefersHTML(r *http.Request) bool
- func RecordRoute(ctx context.Context, pattern string)
- func Redirect(w http.ResponseWriter, target string) error
- func RequestID(ctx context.Context) string
- func SetFlash(w http.ResponseWriter, r *http.Request, kind FlashKind, text string) error
- func WithLogger(ctx context.Context, logger *slog.Logger) context.Context
- func WithRequestID(ctx context.Context, id string) context.Context
- func WithRouteInfo(ctx context.Context, info *RouteInfo) context.Context
- type ErrorBody
- type ErrorDetail
- type FieldError
- type FlashKind
- type FlashMessage
- type FormErrors
- type RouteInfo
- type TrustedProxies
Constants ¶
const ( MaxFlashCookieBytes = 2048 MaxFlashMessages = 4 MaxFlashTextBytes = 512 )
Flash limits: the encoded cookie value, the messages per response and the bytes of one message.
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.
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.
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 ¶
ErrFlashUnavailable is returned by SetFlash when the Flash middleware is not installed.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 IsForm ¶
IsForm reports whether the request body is declared as application/x-www-form-urlencoded.
func IsLocalPath ¶
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 ¶
Logger returns the request-scoped logger, which carries the request ID. It never returns nil.
func MethodOverride ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 SetFlash ¶
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 ¶
WithLogger returns a context that carries the request-scoped logger.
func WithRequestID ¶
WithRequestID returns a context that carries the request ID; the server sets it for every request.
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 ¶
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.
type FlashMessage ¶
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 ¶
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.
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.