Documentation
¶
Overview ¶
Package web provides the request context (*Ctx) used by Vukem handlers: request helpers, typed input, responses, and the central error handler.
A handler has the signature func(*Ctx) error. Returning an error hands it to the error handler, which renders the right response for the client (HTML, redirect with flashed errors, HTMX fragment or problem+json).
Index ¶
- Constants
- func Bind[T any](c *Ctx) (T, error)
- func DefaultErrorHandler(c *Ctx, err error)
- func ETagOf(v any) (string, error)
- func Fail(w http.ResponseWriter, r *http.Request, err error)
- func FormToMap(values url.Values) map[string]any
- func JSONWith[T any](c *Ctx, status int, schema z.Schema[T], v T) error
- func LimitBody(w http.ResponseWriter, r *http.Request, n int64) *http.Request
- func Parse[T any](c *Ctx, schema z.Schema[T]) (T, error)
- func ParseParams[T any](c *Ctx, schema z.Schema[T], names ...string) (T, error)
- func ParseQuery[T any](c *Ctx, schema z.Schema[T]) (T, error)
- func RequestID(ctx context.Context) string
- func SafeRedirect(r *http.Request, target, fallback string) string
- func SecurityEvent(r *http.Request, event string, attrs ...any)
- func WithRequestID(ctx context.Context, id string) context.Context
- func WithServices(ctx context.Context, s *Services) context.Context
- func WithSession(ctx context.Context, s Session) context.Context
- func WithValidationHook(ctx context.Context, hook ValidationHook) context.Context
- type Ctx
- func (c *Ctx) Accepted(v any) error
- func (c *Ctx) Back(fallback string) error
- func (c *Ctx) BindJSON(v any) error
- func (c *Ctx) Blob(status int, contentType string, b []byte) error
- func (c *Ctx) CheckIfMatch(current string) error
- func (c *Ctx) Context() context.Context
- func (c *Ctx) Cookie(name string) string
- func (c *Ctx) Created(location string, v any) error
- func (c *Ctx) Download(fsys fs.FS, name, filename string) error
- func (c *Ctx) Errors() map[string][]string
- func (c *Ctx) File(fsys fs.FS, name string) error
- func (c *Ctx) Flash(key string, value any) error
- func (c *Ctx) FlashValue(key string) any
- func (c *Ctx) FormFile(name string) (multipart.File, *multipart.FileHeader, error)
- func (c *Ctx) FormValue(name string) string
- func (c *Ctx) Fragment(name, block string, data any, opts ...RenderOption) error
- func (c *Ctx) HTML(status int, html string) error
- func (c *Ctx) HTMX() *htmx.Response
- func (c *Ctx) HandleError(err error)
- func (c *Ctx) Header(name string) string
- func (c *Ctx) IP() string
- func (c *Ctx) Input() (map[string]any, error)
- func (c *Ctx) IsHTMX() bool
- func (c *Ctx) IsJSON() bool
- func (c *Ctx) IsPartial() bool
- func (c *Ctx) JSON(status int, v any) error
- func (c *Ctx) Log() *slog.Logger
- func (c *Ctx) Method() string
- func (c *Ctx) MustURL(name string, params ...any) string
- func (c *Ctx) NoContent() error
- func (c *Ctx) NotModified(etag string, lastModified time.Time) bool
- func (c *Ctx) Old(field string) string
- func (c *Ctx) Param(name string) string
- func (c *Ctx) ParamInt(name string) (int64, error)
- func (c *Ctx) Path() string
- func (c *Ctx) Problem(status int, title, detail string) error
- func (c *Ctx) Query(name string) string
- func (c *Ctx) QueryBool(name string) bool
- func (c *Ctx) QueryInput() map[string]any
- func (c *Ctx) QueryInt(name string, def int) int
- func (c *Ctx) Redirect(target string) error
- func (c *Ctx) RedirectRoute(name string, params ...any) error
- func (c *Ctx) RedirectStatus(status int, target string) error
- func (c *Ctx) Render(name string, data any, opts ...RenderOption) error
- func (c *Ctx) Request() *http.Request
- func (c *Ctx) RouteName() string
- func (c *Ctx) Serve(h HandlerFunc)
- func (c *Ctx) Services() *Services
- func (c *Ctx) Session() Session
- func (c *Ctx) SetCookie(ck *http.Cookie)
- func (c *Ctx) SetHeader(name, value string)
- func (c *Ctx) SetRequest(r *http.Request)
- func (c *Ctx) SetRouteName(name string)
- func (c *Ctx) Status(code int) *Ctx
- func (c *Ctx) Stream(status int, contentType string, r io.Reader) error
- func (c *Ctx) String(status int, s string) error
- func (c *Ctx) URL(name string, params ...any) (string, error)
- func (c *Ctx) View(v View, opts ...RenderOption) error
- func (c *Ctx) WantsJSON() bool
- func (c *Ctx) Writer() *ResponseWriter
- type ErrorHandler
- type HandlerFunc
- type Map
- type Problem
- type RenderOption
- type Renderer
- type ResponseWriter
- type Services
- type Session
- type URLGenerator
- type ValidationHook
- type View
Constants ¶
const ( OldInputKey = "_old" ErrorsKey = "_errors" TokenKey = "_token" )
Reserved session keys used by flash, old input and validation errors.
const DefaultMaxBody = 10 << 20
DefaultMaxBody is the request body limit applied by Input and BindJSON when no smaller limit was set by middleware (A06: resource exhaustion).
Variables ¶
This section is empty.
Functions ¶
func Bind ¶
Bind parses the request input into T using the schema derived from T's `validate` struct tags (z.For).
func DefaultErrorHandler ¶
DefaultErrorHandler renders err according to the client:
- JSON clients get RFC 9457 problem+json (422 includes field errors);
- classic form posts with a validation error go to the validation hook (flash errors and redirect back) when one is installed;
- htmx requests get an HTML fragment with the status (htmx 4 swaps 4xx);
- browsers get an HTML status page (views "errors/<status>" if present).
5xx details are never sent to clients unless Services.Debug is set (docs/security.md, A10).
func ETagOf ¶
ETagOf returns a strong ETag ("\"...\"") for v: the SHA-256 of its JSON encoding (or of the bytes when v is []byte or string).
func Fail ¶
func Fail(w http.ResponseWriter, r *http.Request, err error)
Fail renders err through the error handler. Middleware uses it to reject a request (401, 413, 415, 429...) with the same format as handlers.
func FormToMap ¶
FormToMap converts url.Values into nested maps and lists: "a=1&a=2" → {"a": ["1","2"]}, "tags[]=x" → {"tags": ["x"]}, "author[email]=e" → {"author": {"email": "e"}}, "items[0][name]=n" → {"items": [{"name": "n"}]}.
func JSONWith ¶
JSONWith writes v as JSON after checking it against schema when Services.CheckResponses is enabled (development and test by default). A mismatch is a server bug: it is logged and answered with 500 instead of sending data that breaks the API contract.
func LimitBody ¶
LimitBody caps the request body at n bytes and marks the request so handlers do not apply DefaultMaxBody on top. Body-limit middleware uses it.
func Parse ¶
Parse reads the request input (JSON, form or multipart body merged over the query string) and parses it with schema. Validation issues become an *errs.ValidationError, so returning the error from a handler yields a 422 problem+json, an HTMX fragment or a redirect back with flashed errors.
in, err := web.Parse(c, requests.StorePostSchema)
if err != nil {
return err
}
func ParseParams ¶
ParseParams parses the named path parameters ({id}, {slug}...).
func ParseQuery ¶
ParseQuery parses only the query string.
func SafeRedirect ¶
SafeRedirect returns target when it is a relative path or has the same host as r, and fallback otherwise.
func SecurityEvent ¶
SecurityEvent logs a security-relevant event at WARN with a stable "event" attribute (docs/security.md, A09).
func WithRequestID ¶
WithRequestID stores the request id in ctx.
func WithServices ¶
WithServices stores s in ctx so middleware can render errors consistently.
func WithSession ¶
WithSession stores s in ctx (session middleware does this).
func WithValidationHook ¶
func WithValidationHook(ctx context.Context, hook ValidationHook) context.Context
WithValidationHook installs hook for requests carrying ctx.
Types ¶
type Ctx ¶
type Ctx struct {
// contains filtered or unexported fields
}
Ctx is the per-request context. It must not be used after the handler returns or from other goroutines.
func (*Ctx) Back ¶
Back redirects to the Referer when it is same-origin, else to fallback. Off-site referers are ignored (open-redirect protection, A01).
func (*Ctx) BindJSON ¶
BindJSON decodes the JSON body into v, rejecting unknown fields, trailing data and bodies over the size limit.
func (*Ctx) CheckIfMatch ¶
CheckIfMatch enforces optimistic concurrency for writes: when the client sent If-Match and it does not match the current ETag, it returns a 412 error. Without If-Match it returns nil.
func (*Ctx) FlashValue ¶
FlashValue returns a flashed value from the previous request, or nil.
func (*Ctx) Fragment ¶
func (c *Ctx) Fragment(name, block string, data any, opts ...RenderOption) error
Fragment renders a single block of a view.
func (*Ctx) HTML ¶
HTML writes a text/html response from a trusted string. Never pass user input: use views (html/template) for escaping.
func (*Ctx) HandleError ¶
HandleError passes err to the error handler.
func (*Ctx) IP ¶
IP returns the client IP. X-Forwarded-For is honoured only when the direct peer is a trusted proxy (docs/security.md, A01/A02).
func (*Ctx) Input ¶
Input returns the request body (JSON object, urlencoded or multipart form) merged over the query string as a generic map ready for schema parsing: JSON keeps its types (numbers as json.Number); form values are strings, repeated or "[]" keys become lists and "a[b]" keys nest.
func (*Ctx) MustURL ¶
MustURL is URL for templates and code paths where the route is known to exist; it returns "#" and logs on error instead of panicking.
func (*Ctx) NotModified ¶
NotModified sets the ETag (and Last-Modified when non-zero) headers and reports whether the client's cached copy is current. When it returns true a 304 was written and the handler must return without a body:
if c.NotModified(etag, post.UpdatedAt) { return nil }
func (*Ctx) Old ¶
Old returns the input submitted by the previous (failed) form post for field, so forms can be re-filled. Nested fields use dots ("author.email").
func (*Ctx) ParamInt ¶
ParamInt returns a path parameter as int64, or a 404 error when it is not a valid integer (a non-numeric id names no resource).
func (*Ctx) QueryInput ¶
QueryInput returns the query string as a generic map (see Input).
func (*Ctx) Redirect ¶
Redirect redirects to target. Unsafe methods get 303 See Other so the browser follows with GET; GET/HEAD get 302 Found. Use RedirectStatus for other codes. htmx requests receive HX-Location instead (navigation without a full reload).
func (*Ctx) RedirectRoute ¶
RedirectRoute redirects to a named route.
func (*Ctx) RedirectStatus ¶
RedirectStatus redirects with an explicit 3xx status.
func (*Ctx) Render ¶
func (c *Ctx) Render(name string, data any, opts ...RenderOption) error
Render renders a view into the response. Output is buffered, so a template error becomes a clean 500 instead of a half-written page.
func (*Ctx) Serve ¶
func (c *Ctx) Serve(h HandlerFunc)
Serve runs h with c and routes a returned error to the error handler.
func (*Ctx) SetRequest ¶
SetRequest replaces the request (e.g. after adding context values).
func (*Ctx) SetRouteName ¶
SetRouteName is used by routers to record the matched route.
func (*Ctx) Status ¶
Status sets the status to send with the next body write. Chain it: c.Status(201).JSON(v) is equivalent to c.JSON(201, v).
func (*Ctx) View ¶
func (c *Ctx) View(v View, opts ...RenderOption) error
View renders a component such as a templ.Component.
type ErrorHandler ¶
ErrorHandler writes the response for an error returned by a handler.
type HandlerFunc ¶
HandlerFunc handles a request. Returned errors go to the error handler.
type Problem ¶
type Problem struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
RequestID string `json:"request_id,omitempty"`
Errors map[string][]string `json:"errors,omitempty"`
}
Problem is an RFC 9457 problem details document.
type RenderOption ¶
type RenderOption func(*renderOpts)
RenderOption configures Render.
func Block ¶
func Block(name string) RenderOption
Block renders only the named block for htmx partial requests and the full page otherwise (template fragments):
return c.Render("posts/index", data, web.Block("post-list"))
func WithStatus ¶
func WithStatus(code int) RenderOption
WithStatus sets the response status (default 200).
type Renderer ¶
type Renderer interface {
Render(ctx context.Context, w io.Writer, name string, data any) error
RenderBlock(ctx context.Context, w io.Writer, name, block string, data any) error
Has(name string) bool
}
Renderer renders named views. The view package implements it; ctx gives templates access to request-scoped data (session, CSRF token, CSP nonce).
type ResponseWriter ¶
type ResponseWriter struct {
http.ResponseWriter
// contains filtered or unexported fields
}
ResponseWriter wraps http.ResponseWriter to record the status and whether the response was written. It supports http.ResponseController.
func (*ResponseWriter) Size ¶
func (w *ResponseWriter) Size() int64
Size returns the number of body bytes written.
func (*ResponseWriter) Status ¶
func (w *ResponseWriter) Status() int
Status returns the written status (0 when nothing was written).
func (*ResponseWriter) Unwrap ¶
func (w *ResponseWriter) Unwrap() http.ResponseWriter
Unwrap supports http.ResponseController.
func (*ResponseWriter) Write ¶
func (w *ResponseWriter) Write(b []byte) (int, error)
Write writes the body, sending a 200 status first when needed.
func (*ResponseWriter) WriteHeader ¶
func (w *ResponseWriter) WriteHeader(code int)
WriteHeader records and writes the status.
func (*ResponseWriter) Written ¶
func (w *ResponseWriter) Written() bool
Written reports whether the status line was sent.
type Services ¶
type Services struct {
URLs URLGenerator
Views Renderer
Errors ErrorHandler
Log *slog.Logger
// Debug enables detailed error pages (never in production).
Debug bool
// TrustedProxies lists CIDRs whose X-Forwarded-For is honoured by IP().
TrustedProxies []*net.IPNet
// ProblemBaseURL prefixes RFC 9457 problem "type" URIs; empty means
// "about:blank".
ProblemBaseURL string
// CheckResponses makes JSONWith validate outgoing data against its
// schema (enabled outside production by the app).
CheckResponses bool
}
Services are the shared dependencies every Ctx can reach. The router owns one Services value; fields may be nil when the feature is not installed.
func ServicesFrom ¶
ServicesFrom returns the services stored in ctx, or an empty value.
type Session ¶
type Session interface {
// Get returns a value, or nil.
Get(key string) any
// Put stores a value (JSON-compatible) for this and later requests.
Put(key string, value any)
// Forget removes keys.
Forget(keys ...string)
// Pull returns and removes a value.
Pull(key string) any
// Flash stores a value available only on the next request.
Flash(key string, value any)
// Regenerate issues a new session id keeping the data (call on login
// and privilege changes to prevent session fixation).
Regenerate()
// Invalidate clears all data and issues a new id (call on logout).
Invalidate()
// ID returns the session id (meaningful for server-side stores).
ID() string
}
Session is the request session as seen by handlers. The session package implements it; web only depends on this interface.
func SessionFrom ¶
SessionFrom returns the session stored in ctx, or nil.
type URLGenerator ¶
URLGenerator builds URLs for named routes.
type ValidationHook ¶
type ValidationHook func(c *Ctx, verr *errs.ValidationError) bool
ValidationHook lets session-aware code (flash + redirect back) handle validation errors for classic HTML form posts. It reports whether it wrote a response.