req

package
v0.0.0-...-dd06ff7 Latest Latest
Warning

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

Go to latest
Published: Mar 4, 2026 License: MIT Imports: 16 Imported by: 0

README

Handle

This package provides a couple of convenient HTTP handlers as well as a mechanism for simpler handler definitions that include extracting input strings from the URL path, query params, cookies, and headers. The system is extensible, so eg. an app can define special extractors for extracting from session state or Datastar signals. See TestCustomExtractor for an example of a session extractor.


Handle is a generic adapter for net/http handlers that separates core handler logic from the repetitive work of input parsing, error reporting, and response formatting.

A handler is a plain function, where Req wraps a response writer and request for handler convenience:

func(*Req, T) error

T is a data struct whose fields declare where each input comes from via struct tags — path, query, cookie, or header. When multiple source tags are present, extraction priority follows the extractor list order. Non-pointer fields are required: if no extractor finds a value, decode reports an error like query "name" is required.

Pointer fields are optional: missing values stay nil.

Error keys in FieldErrors use external tag values (e.g. "name" from `query:"name"`), not Go field names, based on the first extractor tag. A field may have multiple extractors; they will be attempted in the order listed.

Fields may also carry validate tags for declarative validation (notblank, email, min=N, max=N):

mux.HandleFunc("GET /add", Handle(func(req *Req, in struct {
    A float64 `query:"a"`
    B float64 `query:"b"`
}) error {
    return req.JSON(map[string]float64{"sum": in.A + in.B})
}))

Boolean helper functions (NotBlank, IsEmail, Between, In, etc.) are available for composing custom validations with Check and CheckField:

req.CheckField(NotBlank(in.Name), "name", "is required")
req.CheckField(Between(in.Age, 18, 120), "age", "must be between 18 and 120")

Options can be passed to append additional extractors and validators:

	extractSession := func(r *http.Request, name string) (string, bool) {
		v := r.Header.Get("X-Session-" + name)
		return v, v != ""
	}

mux.HandleFunc("GET /dashboard", Handle(handleDashboard,
    WithExtractors(
        NewExtractor("session", extractSession),
    ),
))

Handle always calls the handler. Decode errors and validate-tag errors are pre-populated into req.FieldErrors, letting the handler add more via Check/CheckField and decide how to respond (e.g. re-render a form with inline errors):

mux.HandleFunc("POST /signup", Handle(func(req *Req, in struct {
    Email    string `query:"email" validate:"email"`
    Password string `query:"password"`
    Confirm  string `query:"confirm"`
}) error {
    req.CheckField(len(in.Password) >= 8, "password", "must be at least 8 characters")
    req.Check(in.Password == in.Confirm, "passwords don't match")
    if req.HasErrors() {
        return req.HTML(renderForm(req.Errors, req.FieldErrors))
    }
    createUser(in.Email, in.Password)
    return req.Redirect("/welcome")
}))

For a strict adapter that auto-400s on any errors, wrap Handle:

func Strict[T any](fn func(*Req, T) error) http.HandlerFunc {
    return Handle(func(req *Req, in T) error {
        if req.HasErrors() {
            return HTTPError(400, req.Error())
        }
        if err := fn(req, in); err != nil {
            return err
        }
        if req.HasErrors() {
            return HTTPError(400, req.Error())
        }
        return nil
    })
}

Pointer fields distinguish "missing" (nil) from "present but empty" (non-nil). Decode recurses into struct-typed fields that have no extraction tags, so nested structs without source tags are populated from their own fields' tags.

Body parsing (JSON, form data, etc.) is intentionally left to handlers. Extractors return (string, bool), so structured data doesn't fit the pattern, and the implicit "is required" check can't see inside a decoded struct — after json.Unmarshal, a missing key is indistinguishable from a zero value. Deserialization also has too many knobs (unknown fields, number precision, custom decoders) for a single struct tag to capture. Use json.NewDecoder in your handler and call CheckField or req.Validate(&myStruct) to validate the result.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AllIn

func AllIn[T comparable](values []T, safelist ...T) bool

AllIn reports whether every element of values is in the safelist.

func Between

func Between[T cmp.Ordered](value, min, max T) bool

Between reports whether value is in the range [min, max].

func HTTPError

func HTTPError(status int, msg string) error

HTTPError returns an error that the Handle adapter unwraps into an HTTP error response.

func Handle

func Handle[T any](fn func(*Req, T) error, opts ...handleOption) http.HandlerFunc

Handle is a generic adapter that decodes request input into T, then always calls fn. Per-field decode errors are written into Req.FieldErrors so the handler can inspect them alongside its own Check/CheckField calls.

func HotReloadHandler

func HotReloadHandler(w http.ResponseWriter, r *http.Request)

hotreloadHandler is designed for the use case of a developer with a single browser tab open. That tab should have the following code (assuming this handler is running at /hot-reload):

<div data-init="@get('/hot-reload', {retryMaxCount: 1000, retryInterval: 20, retryMaxWaitMs: 200})" id="hotreload"></div>

When the server is shut down, this will attempt to reconnect until the server is restarted. That first successful reconnection will trigger the new server to send the reload script. After that initial reload, this handler will simply send an empty SSE connection thanks to the use of sync.Once.

Todo: Make a version that will reconnect *all* clients – maybe by

func In

func In[T comparable](value T, safelist ...T) bool

In reports whether value is in the safelist.

func IsEmail

func IsEmail(value string) bool

IsEmail reports whether value looks like a valid email address.

func IsURL

func IsURL(value string) bool

IsURL reports whether value is a valid URL with a scheme and host.

func Matches

func Matches(value string, rx *regexp.Regexp) bool

Matches reports whether value matches the regular expression rx.

func MaxRunes

func MaxRunes(value string, n int) bool

MaxRunes reports whether value has at most n runes.

func MinRunes

func MinRunes(value string, n int) bool

MinRunes reports whether value has at least n runes.

func NewExtractor

func NewExtractor(tag string, fn func(*http.Request, string) (string, bool)) extractor

NewExtractor creates an extractor that reads the given struct tag and calls fn to extract a value from the request.

func NewValidator

func NewValidator[V any](name, msg string, fn func(V) bool) validator

NewValidator adapts a boolean helper with no tag argument to a struct tag validator.

func NewValidatorWithArg

func NewValidatorWithArg[V any, A Parseable](name, msg string, fn func(V, A) bool) validator

NewValidatorWithArg adapts a boolean helper with one parsed tag argument to a struct tag validator. msg may contain one %s verb for the tag argument value.

func NoDuplicates

func NoDuplicates[T comparable](values []T) bool

NoDuplicates reports whether all elements of values are unique.

func NonZero

func NonZero[T comparable](value T) bool

func NotBlank

func NotBlank(value string) bool

NotBlank reports whether value contains non-whitespace characters.

func NotIn

func NotIn[T comparable](value T, blocklist ...T) bool

NotIn reports whether value is not in the blocklist.

func StaticHandler

func StaticHandler(dir, prefix string, files embed.FS) http.Handler

StaticHandler serves an embedded static directory from within an embed.FS. For example, if you want to embed the `static` subdirectory, create an embed.FS:

//go:embed static/* var staticFiles embed.FS

Then call StaticHandler("static", "/static/", staticFiles)

To serve the files from the "static" subdirectory at the route prefix "/static/". The slashes in the prefix ensure that files are routed to the correct path within the embed.FS.

func WithExtractors

func WithExtractors(extractors ...extractor) handleOption

WithExtractors returns a handle option that appends additional extractors.

func WithValidators

func WithValidators(validators ...validator) handleOption

WithValidators returns a handle option that appends additional validators.

Types

type Decoder

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

Decoder extracts struct fields from an HTTP request using struct tags.

func (*Decoder) Decode

func (d *Decoder) Decode(r *http.Request, dst any) (map[string]string, error)

Decode populates dst (must be *struct) from the request using struct tags. It returns per-field parse errors keyed by external tag values (first matching extractor tag), not Go field names.

type Parseable

type Parseable interface {
	~int | ~float64 | ~string
}

Parseable is the set of types that can be parsed from a struct tag argument.

type Req

type Req struct {
	Validator
	W http.ResponseWriter
	R *http.Request
}

Req wraps a response writer and request for handler convenience.

func (*Req) HTML

func (req *Req) HTML(s string) error

HTML writes s as text/html with status 200.

func (*Req) JSON

func (req *Req) JSON(v any) error

JSON writes v as JSON with status 200.

func (*Req) JSONStatus

func (req *Req) JSONStatus(status int, v any) error

JSONStatus writes v as JSON with the given status code.

func (*Req) NoContent

func (req *Req) NoContent() error

NoContent sends a 204 No Content response.

func (*Req) Redirect

func (req *Req) Redirect(url string) error

Redirect sends a 302 redirect to the given URL.

func (*Req) Text

func (req *Req) Text(s string) error

Text writes s as text/plain with status 200.

func (*Req) Validate

func (req *Req) Validate(dst any)

Validate runs struct-tag validation (email, min, max, etc.) on dst, writing any errors into req.FieldErrors. Useful after manually decoding a JSON body into a struct with validate tags.

type Validator

type Validator struct {
	Errors      []string          // non-field errors ("passwords don't match")
	FieldErrors map[string]string // field -> error message; first error per field wins
	// contains filtered or unexported fields
}

Validator collects non-field errors and per-field errors into a single place. FieldErrors keys are the external tag values (first matching extractor tag), not Go field names. For example, a field `Name string `query:"name"“ uses "name" as the key.

func (*Validator) AddError

func (v *Validator) AddError(message string)

AddError appends a non-field error, and is usually called by Validator.Check.

func (*Validator) AddFieldError

func (v *Validator) AddFieldError(field, message string)

AddFieldError records an error for field. The first error per field wins; subsequent errors for the same field are ignored.

func (*Validator) Check

func (v *Validator) Check(ok bool, message string)

Check adds a non-field error if ok is false.

func (*Validator) CheckField

func (v *Validator) CheckField(ok bool, field, message string)

CheckField records an error for field if ok is false. The first error per field wins; subsequent errors for the same field are ignored.

func (*Validator) Error

func (v *Validator) Error() string

Error formats all errors as a single string.

func (*Validator) HasErrors

func (v *Validator) HasErrors() bool

HasErrors reports whether any errors have been recorded.

Jump to

Keyboard shortcuts

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