handler

package
v0.85.0 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package handler is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Bind

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

Bind populates dst by merging values from all sources: 1. Header values (via `header:"X-Request-ID"` tag) 2. Query parameters (via `query:"name"` tag) 3. Path parameters (via `path:"id"` tag) 4. JSON body (decodes entire struct, highest priority)

If the request has a JSON body and Content-Type is application/json, the body is decoded first. Then query/path/header values fill in any zero-valued fields.

If the body is present but not valid JSON, a 400 error is returned. If dst is not a pointer, Bind panics.

func CheckObjectKeys added in v0.82.0

func CheckObjectKeys(data []byte, fold func(string) string) error

CheckObjectKeys is CheckTopLevelKeys applied to every object at every nesting depth: no object anywhere in the value may repeat a key or hold two keys that fold to the same name. Non-object values and tokenisation failures return nil so the caller's decoder reports them under its own error contract.

func CheckTopLevelKeys added in v0.82.0

func CheckTopLevelKeys(data []byte, fold func(string) string) error

CheckTopLevelKeys walks the top level of a JSON object and refuses any key that repeats, or that collides with an earlier key once both are passed through fold. Callers with their own key normalisation (crud folds camelCase and snake_case spellings of one column onto each other) pass that normaliser as fold; nil means exact keys only. Non-object bodies and tokenisation failures return nil so the caller's decoder reports them under its own error contract.

func DecodeStrict added in v0.82.0

func DecodeStrict(r io.Reader, dst any) error

DecodeStrict reads one JSON value from r into dst with the same top-level key rule Bind applies to request bodies, for every decode site that is not a plain handler.Bind consumer: JSON-RPC envelopes, websocket frames, buffered bodies replayed through a helper, dev-tool endpoints. Stdlib encoding/json keeps the LAST duplicate key and matches struct tags case-insensitively, so {"action":"safe", "Action":"danger"} runs the second action while a reviewer (or an intercepting log) reads the first. The ambiguity itself is the bug; refusing it is the only resolution that does not privilege one parser's pick.

Struct destinations: every top-level key must exactly match a json tag (case-folded spellings and unknown keys are refused, as Bind does) and no key may repeat. Map and other destinations: no key may repeat, and no two keys may fold to the same name under ASCII case folding. Bodies that are not a top-level object decode unchanged. Errors are *Error with code 400, so a handler can hand them to respond/Error directly.

Size is the caller's job: wrap the reader in http.MaxBytesReader (or io.LimitReader) before calling. The probes that pinned each surface live beside the callers as *_security_test.go.

func GetLogger

func GetLogger(ctx context.Context) (any, bool)

GetLogger retrieves the logger from the context.

func GetRequestID

func GetRequestID(ctx context.Context) (string, bool)

GetRequestID retrieves the request ID from the context.

func GetTenant

func GetTenant(ctx context.Context) (any, bool)

GetTenant retrieves the tenant value from the context.

func GetUser

func GetUser(ctx context.Context) (any, bool)

GetUser retrieves the user value from the context.

func HandlerAdapter

func HandlerAdapter[I, O any](h Handler[I, O]) http.HandlerFunc

HandlerAdapter bridges a typed Handler into a standard http.HandlerFunc. It handles input binding, panic recovery, and output serialization.

func IsCrossSiteRequest added in v0.85.0

func IsCrossSiteRequest(r *http.Request) bool

IsCrossSiteRequest reports whether the request came from another origin, using Fetch Metadata first and the Origin header as the fallback. This is the ONE implementation of the repo's cross-site form guard; every battery and tool that refuses cross-site POSTs calls it and keeps only its own response shape.

Sec-Fetch-Site is authoritative where the browser sends it: "same-origin" and "none" (a user-initiated navigation: address bar, bookmark) are safe, "cross-site" is refused outright. "same-site" is NOT proof of same-origin: the site computation drops the port and folds sibling subdomains, so a page on evil.example.com (or on a sibling port of a localhost tool) is same-site while its Origin names a different origin, and a SameSite cookie still attaches. It falls through to the Origin-host comparison, as does any unknown value. Two copies of this guard (battery/setup, kiln/chat) trusted "same-site" and were driven to an attacker-owned admin account and an approved destructive plan by the 2026-09-06 probes; the copies are gone.

Without Fetch Metadata, an Origin whose host differs from r.Host is cross-site. An absent or opaque ("null") Origin cannot prove an attack and is allowed: a legitimate top-level same-origin form navigation sends Origin: null too, and non-browser clients (curl, tests, native apps) send neither header.

func IsCrossSiteRequestStrict added in v0.85.0

func IsCrossSiteRequestStrict(r *http.Request) bool

IsCrossSiteRequestStrict is IsCrossSiteRequest for surfaces that are only ever called by fetch(), never by a form navigation: there an opaque "Origin: null" with no Fetch Metadata to vouch for it is the sandboxed-iframe / cross-origin-redirect shape, not a legitimate top-level navigation, so it is refused too. Where the browser sends Sec-Fetch-Site the header decides exactly as in IsCrossSiteRequest.

func IsForgeableRequest added in v0.85.0

func IsForgeableRequest(r *http.Request) bool

IsForgeableRequest reports whether a cross-site page could have sent this request WITHOUT a CORS preflight: the CORS "simple request" content types plus the absent header (a bodyless fetch() sends no Content-Type). application/json and every other type are not forgeable: a cross-site POST carrying one is preflighted.

func RequestFromContext

func RequestFromContext(ctx context.Context) (*http.Request, bool)

RequestFromContext retrieves the *http.Request stored by HandlerAdapter.

func Respond

func Respond(w http.ResponseWriter, r *http.Request, out any)

Respond writes out the response based on out's type:

  • nil → 204 No Content
  • ResponseType → delegates to the custom type
  • any other value → JSON serialization, 200 OK

func SSEStream

func SSEStream(w http.ResponseWriter, events <-chan SSE)

SSEStream writes a channel of SSE events as a streaming response.

func SetLogger

func SetLogger(ctx context.Context, logger any) context.Context

SetLogger stores a logger in the context.

func SetRequestID

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

SetRequestID stores a request ID in the context.

func SetTenant

func SetTenant(ctx context.Context, tenant any) context.Context

SetTenant stores a tenant value in the context.

func SetUser

func SetUser(ctx context.Context, user any) context.Context

SetUser stores a user value in the context.

func UnmarshalStrict added in v0.82.0

func UnmarshalStrict(data []byte, dst any) error

UnmarshalStrict is DecodeStrict over bytes already in memory (a websocket frame, a buffered body).

func WriteError

func WriteError(w http.ResponseWriter, err error)

WriteError writes a structured error response to w.

An *Error is rendered verbatim (its Code, Message, and Fields are what the developer chose to expose). Any other error type is treated as an internal failure: the response message is a generic "internal server error" with status 500, and the original error stays out of the response body. Callers that *do* want the inner message to reach the client must wrap explicitly via Errorf or WrapError (the latter keeps the cause for `errors.Is/As` without leaking it).

This prevents accidental leaks of database errors, driver-specific strings ("pq: password authentication failed for user \"admin\""), or wrapped stack traces.

Types

type Error

type Error struct {
	Code    int                 // HTTP status code
	Message string              // human-readable message
	Err     error               // wrapped cause (optional)
	Fields  map[string][]string // field-level validation errors (optional)
}

Error is a structured HTTP error with optional field-level validation errors.

func Errorf

func Errorf(code int, format string, args ...any) *Error

Errorf creates a new Error with the given HTTP status code and formatted message.

func ValidationError

func ValidationError(fields map[string][]string) *Error

ValidationError creates a 400 error with field-level validation messages.

func WrapError

func WrapError(code int, message string, err error) *Error

WrapError wraps an existing error with an HTTP status code and message.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the wrapped cause for errors.Is/As.

type HTML

type HTML string

HTML is a response type that writes text/html.

func (HTML) ContentType

func (h HTML) ContentType() string

func (HTML) WriteBody

func (h HTML) WriteBody(w http.ResponseWriter) error

type Handler

type Handler[I, O any] func(ctx context.Context, in I) (O, error)

Handler is a typed function that processes an input of type I and returns an output of type O. It receives a context (which carries the *http.Request via RequestFromContext) and a fully-bound input struct.

type RawBytes

type RawBytes struct {
	Data []byte
	CT   string // content type, e.g. "image/png"
}

RawBytes is a response type for raw bytes with an explicit content type.

func (RawBytes) ContentType

func (r RawBytes) ContentType() string

func (RawBytes) WriteBody

func (r RawBytes) WriteBody(w http.ResponseWriter) error

type ResponseType

type ResponseType interface {
	// ContentType returns the MIME type for the response.
	ContentType() string
	// WriteBody writes the response body to w.
	WriteBody(w http.ResponseWriter) error
}

ResponseType is an interface for custom response types that control how they are serialized and what Content-Type is used.

type SSE

type SSE struct {
	Event string
	Data  string
	ID    string // optional event ID
}

SSE is a Server-Sent Event response.

func (SSE) ContentType

func (s SSE) ContentType() string

func (SSE) WriteBody

func (s SSE) WriteBody(w http.ResponseWriter) error

Jump to

Keyboard shortcuts

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