Help us shape the future of Go! Take the 2026 Go Developer Survey

request

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 21 Imported by: 0

README

HTTP Request Helpers

Use arkoren.dev/core/http/request from error-returning HTTP handlers to read parameters and bodies. Return its input problems to the router; do not turn operational failures into field-validation feedback.

  • JSON uses Go's encoding/json/v2 defaults, including duplicate-name rejection and case-sensitive member matching. Strict additionally rejects unknown names.
  • XML decodes the first element only. Invalid numeric fields are input errors; trailing elements are deliberately not checked.
  • A nil r.Body is empty input. Size options limit reads, not decoded memory, and cannot remove an upstream reader's body-size ceiling.
  • Body decoding does not check Content-Type. Apply the content-type middleware when a route requires that policy.
  • Query helpers parse Go scalars, not JSON Schema constraints. Apply application validation when finite floats or stricter URL syntax are required.

Custom JSON decoder methods must explicitly identify intended input failures with an input problem or JSON syntax/type error. Ordinary callback, transport, and multipart temporary-file failures retain their operational causes. Resolve unsupported schema layouts before handling requests.

JSON validation diagnostics

ValidateJSON and ParseJSON report native semantic failures as decode violations under the existing 422 validation problem. They preserve JSON Pointer paths and native causes for server-side inspection. Strict unknown members keep unknown, and schema-generated codes, including type, are unchanged.

Native decoding feedback uses optional params.default_representation, not params.expected. This is a default-type hint, not a field's required wire kind: json:"count,string" accepts "42" and rejects 42, even though the hint is integer. Binary, timestamp, and text-decoded values retain a default string hint. Opaque custom JSON methods, unsupported Go kinds, and time.Duration have no hint. Update consumers of the former native type/expected diagnostic to use decode/default_representation without prescribing a field format.

The generic message does not expose native error text, submitted values, or Go type names. Add a decode entry to your validation catalog to translate it; without one, the message stays English. Schema type translations do not apply.

The Go 1.27 JSON-v2 API does not support format:* field-tag options, including format:array. time.Duration has no default JSON representation; use an application decoder or convert an explicitly represented DTO field.

See the package API and examples for error identities, body options, and schema-validation behavior.

Documentation

Overview

Package request reads and validates HTTP request data: parameters, headers, cookies, bodies, and the user.

The package focuses on safe extraction and decoding of request data: path/query parameters, headers/cookies, body parsing, lifecycle hooks, and the session and user (User) stored by middleware.

Behavior notes

Body helpers return typed errors for malformed payloads and limit body size: an explicit MaxSize, else the limit in the request context (the bodylimit middleware), else DefaultMaxBodySize. Unlimited adds no per-call cap but cannot remove an existing reader's ceiling. Hooks requires a request served by router.Handler and panics with ErrNoHooksMiddleware otherwise; there is no safe public lookup. A nil r.Body is empty input, including for requests constructed by http.NewRequest: raw readers succeed with no data and JSON/XML return their malformed-body problem. Decoders do not check Content-Type; use the contenttype middleware when a route requires a specific media type. Size options cannot remove a ceiling already enforced by r.Body's reader. Query scalar conversion follows strconv and does not execute OpenAPI schemas; use application validation for stricter numeric or URL requirements.

Session and MustSession select the default session unless per-call WithSessionHandle options select another bridge. Later handles replace earlier ones; zero restores default lookup. These options contain no cookie or persistence settings. MustSession panics with session.ErrNoSession when that selected bridge is absent or nil; it never falls back to the default.

Example:

admin := session.NewHandle()
current := request.MustSession(r, request.WithSessionHandle(admin))
if err := current.Put(session.NewField[string]("theme"), "dark"); err != nil {
	return err
}

Multipart

Multipart parses multipart/form-data once, with a size cap, and returns a Form of text fields and uploaded files. Combine it with validate.File and storage.Storage.PutFile for uploads.

Validation

The Validate* and Parse* helpers decode a body like their plain counterparts (JSON and XML) and check it against a validate.Schema; they take the same BodyOption values. T is inferred from the schema. Validate* returns the decoded value as is, Parse* returns the schema.Parse result with defaults applied. The locale and catalog come from the request context, see the locale package.

Malformed JSON syntax, including duplicate names, stays ErrInvalidJSON (400); oversized bodies return ErrBodyTooLarge (413). Native JSON semantic failures become "decode" violations; strict unknown members retain "unknown". Both preserve the native JSON Pointer and merge with schema violations under ErrValidationFailed (422). Schema codes, including "type", are unchanged. ValidationProblem puts the validate.Errors in the problem's "errors" member. Its cause retains both the violations and native semantic error, without retaining an intermediate 400 problem identity.

Native GoType metadata describes the output type, not the field's wire format. A "decode" violation therefore omits params.expected. Its optional params.default_representation is only a default hint, such as "integer" for an integer or "string" for binary, timestamp, and text-decoded values. For json:"count,string", "42" is valid and 42 is not; the integer hint is not advice to remove the quotes. Opaque custom JSON methods, unsupported Go kinds, and time.Duration have no hint. No JSON field resolver or tag parser is used to infer a field-specific expectation. The generic English message excludes native error text, submitted values, and Go type names; translate it with a "decode" entry in the validation catalog, not the schema's "type" entry.

This Go 1.27 JSON-v2 API does not support format:* field-tag options such as format:array. time.Duration has no default JSON representation; use an application decoder or a DTO field with an explicit application conversion. Native XML syntax and scalar conversion errors carry no JSON Pointer and stay 400; ordinary application decoder failures are not assumed to be bad input. Native integer decoding rejects fractions, exponents, and overflow. Ordinary errors from custom unmarshaler methods, body reads, and multipart temporary-file I/O are not converted into input problems or decoding violations. Their causes remain available through errors.Is/errors.As for server-side handling.

The router does not render a raw validate.Errors; it answers 500. Pass errors from schema.Validate or schema.Parse through ValidationProblem, or register it once with the errormap middleware.

encoding/json/v2 stops at its first semantic error and leaves the members after it undecoded, so at most one native decoding violation is reported. The schema can add its own violations on the partially decoded value, but "required" violations are dropped in that case because the missing members were never read. Fix the reported value and resubmit to see the rest.

Index

Examples

Constants

View Source
const DefaultMaxBodySize int64 = 10 << 20 // 10 MiB

DefaultMaxBodySize is the body limit when neither MaxSize nor the bodylimit middleware gives one, and the size a negative limit selects (10 MiB). It is a sensible ceiling for JSON APIs; raise it explicitly for uploads or opt out with Unlimited.

Example:

data, err := request.Bytes(r, request.MaxSize(request.DefaultMaxBodySize))
if err != nil {
	return err
}
_ = data

Variables

View Source
var ErrBodyTooLarge = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/request:body-too-large")
	details.Title("Payload Too Large")
	details.Detail("The request body exceeds the allowed size.")
	details.Status(http.StatusRequestEntityTooLarge)
})

ErrBodyTooLarge is returned when the request body exceeds the maximum allowed size. Arkoren exposes this condition as a problem response so handlers can return it directly.

Example:

failure := request.ErrBodyTooLarge.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()
View Source
var ErrInvalidJSON = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/request:invalid-json")
	details.Title("Invalid JSON request body")
	details.Detail("The request body must contain valid JSON.")
	details.Status(http.StatusBadRequest)
})

ErrInvalidJSON is returned when the request body cannot be decoded as JSON.

Example:

failure := request.ErrInvalidJSON.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()
View Source
var ErrInvalidMultipart = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/request:invalid-multipart")
	details.Title("Invalid multipart request body")
	details.Detail("The request body must contain valid multipart/form-data.")
	details.Status(http.StatusBadRequest)
})

ErrInvalidMultipart is returned by Multipart when the body is not valid multipart/form-data, such as a missing boundary or malformed part. Native multipart.ErrMessageTooLarge limits instead match ErrBodyTooLarge.

Example:

failure := request.ErrInvalidMultipart.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()
View Source
var ErrInvalidPathParameter = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/request:invalid-path-parameter")
	details.Title("Invalid path parameter")
	details.Detail("The path parameter has an invalid value.")
	details.Status(http.StatusBadRequest)
})

ErrInvalidPathParameter is returned when a path parameter is empty or cannot be parsed as the requested type. The problem carries the "parameter" name and the "expected_type" Go type.

Example:

failure := request.ErrInvalidPathParameter.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()
View Source
var ErrInvalidQueryParameter = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/request:invalid-query-parameter")
	details.Title("Invalid query parameter")
	details.Detail("The query parameter has an invalid value.")
	details.Status(http.StatusBadRequest)
})

ErrInvalidQueryParameter is returned when a query parameter is absent, empty, or cannot be parsed as the requested type. The problem carries the "parameter" name and the "expected_type" Go type.

Example:

failure := request.ErrInvalidQueryParameter.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()
View Source
var ErrInvalidXML = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/request:invalid-xml")
	details.Title("Invalid XML request body")
	details.Detail("The request body must contain valid XML.")
	details.Status(http.StatusBadRequest)
})

ErrInvalidXML is returned when the request body cannot be decoded as XML.

Example:

failure := request.ErrInvalidXML.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()
View Source
var ErrNoHooksMiddleware = hook.ErrNoHooks

ErrNoHooksMiddleware is the problem raised when hooks are accessed on a request that was not served through a router.Handler, which installs them. It is a construction/wiring mistake rather than optional hook absence.

Example:

failure := request.ErrNoHooksMiddleware.Derive(func(details *problem.Builder) {
	details.Detail("The request was not served through the router.")
})
_ = failure.Status()
View Source
var ErrStrictUnsupported = errors.New("request: Strict is not supported for XML, Bytes and String bodies")

ErrStrictUnsupported is returned by Bytes, String, XML, ParseXML and ValidateXML when they are given the Strict option. Raw bodies have no members to reject and Arkoren does not provide a strict XML decoder. The check runs before the body is read, so the body stays unconsumed.

Example:

_, err := request.XML[Feed](r, request.Strict())
if errors.Is(err, request.ErrStrictUnsupported) {
	return err // choose a supported decoder option rather than reporting bad input
}
return err
View Source
var ErrUnsupportedMediaType = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/middleware/contenttype:unsupported-media-type")
	details.Title("Unsupported Media Type")
	details.Detail("The request Content-Type is not supported by this endpoint.")
	details.Status(http.StatusUnsupportedMediaType)
})

ErrUnsupportedMediaType is returned by Multipart when the request is not multipart/form-data. It has the same type, status and title as contenttype.ErrUnsupportedMediaType, which this package cannot import, so errors.Is matches either one against the other.

Example:

failure := request.ErrUnsupportedMediaType.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()
View Source
var ErrValidationFailed = problem.New(func(details *problem.Builder) {
	details.Type("tag:arkoren.dev,2026-09:core:http/request:validation-failed")
	details.Title("Validation failed")
	details.Status(http.StatusUnprocessableEntity)
})

ErrValidationFailed is the problem for a body or value that fails schema validation. It is the base for ValidationProblem, which adds the violations as the "errors" member keyed by JSON Pointer. Match it with errors.Is.

Example:

failure := request.ErrValidationFailed.Derive(func(details *problem.Builder) {
	details.Detail("The request cannot be completed.")
})
_ = failure.Status()

Functions

func Bytes

func Bytes(request *http.Request, opts ...BodyOption) ([]byte, error)

Bytes reads the request body and returns it as a byte slice. The body is consumed and cannot be read again. A body over the MaxSize limit returns ErrBodyTooLarge. Strict returns ErrStrictUnsupported without reading. Operational failures and problems already supplied by the reader retain their identity rather than being reclassified from an underlying cause.

Example:

data, err := request.Bytes(r, request.MaxSize(1<<20))
if err != nil {
	return err
}
_ = data
func Cookie(r *http.Request, name string) *http.Cookie

Cookie retrieves the named cookie from the HTTP request using http.Request.Cookie. It returns nil if the cookie does not exist. The first parsed cookie with that name wins; malformed cookie entries are handled by net/http rather than surfaced as request input problems here.

Example:

if cookie := request.Cookie(r, "theme"); cookie != nil {
	return response.String(w, http.StatusOK, cookie.Value)
}
return response.Status(w, http.StatusNoContent)

func CookieValue

func CookieValue(r *http.Request, name string) string

CookieValue retrieves the value of the named cookie from the HTTP request. It returns an empty string if the cookie does not exist. An empty cookie value and a missing cookie therefore have the same result; use Cookie when the distinction matters.

Example:

return response.String(w, http.StatusOK, request.CookieValue(r, "theme"))

func CookieValueOr

func CookieValueOr(r *http.Request, name string, fallback string) string

CookieValueOr retrieves the value of the named cookie, falling back to the provided default value if the cookie is missing or empty. It follows Cookie's first-match rule and does not validate application values.

Example:

theme := request.CookieValueOr(r, "theme", "light")
return response.String(w, http.StatusOK, theme)

func Cursor

Cursor reads the cursor and per_page query parameters into cursor pagination parameters and normalizes them with pagination.CursorParams.Normalize and opts. A missing per_page uses the default, a present value below one is raised to one, and a value that is not an integer returns ErrInvalidQueryParameter. A maximum below one returns pagination.ErrInvalidConfig.

Example:

params, err := request.Cursor(r)
if err != nil {
	return err
}
_ = params.Cursor

func HasHeader

func HasHeader(r *http.Request, key string) bool

HasHeader reports whether the first value for key is nonempty. A present header with an empty first value reports false, matching HeaderOr's fallback rule rather than testing map-key presence.

Example:

if request.HasHeader(r, "Accept-Language") {
	language := request.Header(r, "Accept-Language")
	_ = language
}

func HasQuery

func HasQuery(r *http.Request, name string) bool

HasQuery reports whether the named parameter exists in net/url's parsed query, regardless of its value. Malformed pairs omitted by net/url are not present. It is a presence check, not validation of the complete raw query string.

Example:

if request.HasQuery(r, "verbose") {
	return response.String(w, http.StatusOK, "verbose mode")
}
return response.Status(w, http.StatusNoContent)
func Header(r *http.Request, key string) string

Header returns the first value associated with the given header key from the request. It returns an empty string if the header is not present. Header names use the standard http.Header lookup rules; values are not split into comma-separated tokens or otherwise normalized.

Example:

return response.String(w, http.StatusOK, request.Header(r, "Accept-Language"))

func HeaderOr

func HeaderOr(r *http.Request, key string, fallback string) string

HeaderOr returns the first value for the given header key, falling back to the provided default value if the header is missing or empty. Only the first value participates; later header entries do not replace an empty first value.

Example:

language := request.HeaderOr(r, "Accept-Language", "en")
return response.String(w, http.StatusOK, language)

func HeaderValues

func HeaderValues(r *http.Request, key string) []string

HeaderValues returns all values associated with the given header key from the request. It returns nil if the header is not present. Like http.Header.Values, the returned slice aliases the request's values; copy it before mutation if the original header must remain unchanged.

Example:

values := request.HeaderValues(r, "Accept-Language")
return response.JSON(w, http.StatusOK, values)

func JSON

func JSON[T any](request *http.Request, opts ...BodyOption) (value T, err error)

JSON decodes the request body as JSON into a value of type T. Unknown members are ignored unless Strict is given. A body over the MaxSize limit returns ErrBodyTooLarge; malformed JSON returns ErrInvalidJSON. Decoding follows encoding/json/v2: by default member names are case-sensitive, duplicates are rejected, and integers reject fractions and exponents. Content-Type is not checked; use contenttype middleware for that policy. Errors from application unmarshaler methods retain their cause and are not automatically classified as bad input. Such methods must return an explicit problem or JSON syntax/type error when client-facing feedback is intended.

Example:

input, err := request.JSON[CreateUser](r, request.Strict(), request.MaxSize(1<<20))
if err != nil {
	return err
}
_ = input
Example

ExampleJSON distinguishes absent input from operational decoder failures.

package main

import (
	"errors"
	"fmt"
	"net/http"

	"arkoren.dev/core/http/request"
)

func main() {
	incoming, err := http.NewRequest(http.MethodPost, "http://example.test/", nil)

	if err != nil {
		fmt.Println(err)

		return
	}

	_, err = request.JSON[struct{}](incoming)
	fmt.Println(errors.Is(err, request.ErrInvalidJSON))
}
Output:
true

func MaxBodySizeFromContext

func MaxBodySizeFromContext(ctx context.Context) (int64, bool)

MaxBodySizeFromContext returns the limit stored in ctx by the bodylimit middleware (arkoren.dev/core/http/middleware/bodylimit) and whether there is one. When ok is false, bodies are limited to DefaultMaxBodySize.

Example:

limit, ok := request.MaxBodySizeFromContext(r.Context())
if !ok {
	limit = request.DefaultMaxBodySize
}
_ = limit

func MustSession

func MustSession(r *http.Request, opts ...SessionOption) *session.Session

MustSession is like Session but panics with session.ErrNoSession when no session is found. It applies the per-call options once through Session.

Example:

current := request.MustSession(r)
if err := current.Put(session.NewField[string]("theme"), "dark"); err != nil {
	return err
}

func Page

Page reads the page and per_page query parameters into offset pagination parameters and normalizes them with pagination.PageParams.Normalize and opts. A missing parameter uses the default, a present value below one is raised to one, and a value that is not an integer returns ErrInvalidQueryParameter. A maximum below one returns pagination.ErrInvalidConfig.

Example:

params, err := request.Page(r)
if err != nil {
	return err
}
_ = params.PerPage

func Param

func Param[T Scalar](r *http.Request, name string) (T, error)

Param retrieves the named path parameter (see http.Request.PathValue) and parses it as T. Integers are range checked against T's size, so "300" is rejected for int8. An empty or missing value is an error for every T, including string; use http.Request.PathValue to read a raw string.

It returns ErrInvalidPathParameter with the parameter name and type attached, and the parse error as its cause when there is one.

Example:

id, err := request.Param[int64](r, "id")
if err != nil {
	return err
}
_ = id

func ParamOr

func ParamOr[T Scalar](r *http.Request, name string, fallback T) T

ParamOr is like Param but returns fallback when the parameter is missing or cannot be parsed as T.

Example:

page := request.ParamOr(r, "page", 1)

func ParseJSON

func ParseJSON[T any](request *http.Request, schema validate.Schema[T], opts ...BodyOption) (T, error)

ParseJSON decodes like ValidateJSON and returns the result of schema.Parse: a copy of the decoded value with defaults applied. It preserves the same input-error and operational-error classifications.

Example:

input, err := request.ParseJSON(r, userSchema)
if err != nil {
	return err
}
_ = input

func ParseXML

func ParseXML[T any](request *http.Request, schema validate.Schema[T], opts ...BodyOption) (T, error)

ParseXML decodes like ValidateXML and returns the result of schema.Parse. Native numeric conversion failures remain 400 input problems; operational failures are not converted to schema feedback.

Example:

input, err := request.ParseXML(r, feedSchema)
if err != nil {
	return err
}
_ = input

func Query

func Query[T Scalar](r *http.Request, name string) (T, error)

Query retrieves the first value of the named query parameter and parses it as T, with the same rules as Param. An absent or empty value is an error for every T, including string.

URL decoding follows net/url.URL.Query, including its omission of malformed query pairs. Scalar conversion follows strconv, not a JSON Schema: float types accept NaN and infinities, and bool accepts values such as "1". OpenAPI metadata does not add validation; enforce application constraints separately when the route requires a stricter value or URL grammar.

It returns ErrInvalidQueryParameter with the parameter name and type attached, and the parse error as its cause when there is one.

Example:

limit, err := request.Query[int](r, "limit")
if err != nil {
	return err
}
_ = limit

func QueryOr

func QueryOr[T Scalar](r *http.Request, name string, fallback T) T

QueryOr is like Query but returns fallback when the parameter is absent, empty, or cannot be parsed as T. A present but empty "?q=" therefore also yields the fallback.

Example:

search := request.QueryOr(r, "q", "")

func Session

func Session(r *http.Request, opts ...SessionOption) (*session.Session, bool)

Session returns the session stored on the request by session.Middleware. The boolean reports whether a session was found. Options only select a handle; zero selects default. A stored nil pointer is absent. No session is created, no fallback is attempted, and the request is not mutated.

Example:

current, ok := request.Session(r)
if !ok {
	return session.ErrNoSession
}
if err := current.Put(session.NewField[string]("theme"), "dark"); err != nil {
	return err
}

func String

func String(request *http.Request, opts ...BodyOption) (string, error)

String reads the request body and returns it as a string without UTF-8 validation. It follows the rules of Bytes, including the size limit and ErrStrictUnsupported.

Example:

text, err := request.String(r, request.MaxSize(4096))
if err != nil {
	return err
}
_ = text

func User

func User[U any](r *http.Request) (U, error)

User returns the user of type U stored on the request by auth.Middleware. It returns auth.ErrUnauthenticated when there is none.

Example:

user, err := request.User[User](r)
if err != nil {
	return err
}
_ = user.ID

func ValidateJSON

func ValidateJSON[T any](request *http.Request, schema validate.Schema[T], opts ...BodyOption) (T, error)

ValidateJSON decodes the request body as JSON like JSON and checks the value against schema. Defaults are not applied; use ParseJSON for that. Strict turns unknown members into "unknown" violations; MaxSize and Unlimited change the size limit (see DefaultMaxBodySize).

Malformed syntax, including duplicate names, returns ErrInvalidJSON (400). Native semantic failures become "decode" violations at the decoder's JSON Pointer, merged with schema violations under ErrValidationFailed (422). The native semantic cause remains available through errors.As/errors.Is in that 422 result. Schema infrastructure failures take precedence instead. Strict unknown members retain "unknown"; schema violation codes are unchanged. Ordinary application unmarshaler failures pass through without conversion.

A "decode" violation never supplies params.expected: the native Go type does not establish the field's wire format. Optional params.default_representation describes that type's default representation only. For example, an integer field tagged with ,string still has an "integer" default hint, but requires a quoted number. Opaque custom JSON methods and unsupported Go kinds have no hint; time.Duration has no default representation. The safe generic message excludes native error text, input values, and Go type names. Add a "decode" entry to the validation catalog to translate it; the built-in fallback is English. See the package documentation for partial-decoding limits.

Example:

input, err := request.ValidateJSON(r, userSchema, request.Strict(), request.MaxSize(1<<20))
if err != nil {
	return err
}
_ = input
Example

ExampleValidateJSON reads a qualified default-representation hint.

package main

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"arkoren.dev/core/http/request"
	"arkoren.dev/foundation/validate"
)

func main() {
	type input struct {
		// Data uses the default base64-encoded JSON string representation.
		Data []byte `json:"data"`
	}
	incoming := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"data":42}`))

	_, err := request.ValidateJSON(incoming, validate.Struct[input]())
	errs, ok := errors.AsType[validate.Errors](err)

	if !ok {
		fmt.Println(err)

		return
	}

	fmt.Println(errs["/data"][0].Params["default_representation"])
}
Output:
string

func ValidateXML

func ValidateXML[T any](request *http.Request, schema validate.Schema[T], opts ...BodyOption) (T, error)

ValidateXML decodes the request body as XML like XML and checks the value against schema. Defaults are not applied. Native XML input errors, including invalid numeric fields, stay ErrInvalidXML (400), because XML errors carry no JSON Pointer. Operational errors pass through unchanged. Strict returns ErrStrictUnsupported before reading. Limits work as for XML.

Example:

input, err := request.ValidateXML(r, feedSchema)
if err != nil {
	return err
}
_ = input

func ValidationProblem

func ValidationProblem(err error) error

ValidationProblem converts a validate.Errors found anywhere in err's chain to ErrValidationFailed (422) carrying the violations as the "errors" member, with err kept as the cause for logging and tracing. A nil err returns nil, and any other error is returned unchanged.

The Validate* and Parse* helpers already apply it. Call it for errors from schema.Validate or schema.Parse used directly. Its signature also fits the errormap package, so one registration covers every handler:

r.Use(errormap.Middleware(errormap.Func(request.ValidationProblem)))

Example:

if err := schema.Validate(ctx, input); err != nil {
	return request.ValidationProblem(err)
}

func XML

func XML[T any](request *http.Request, opts ...BodyOption) (value T, err error)

XML decodes the request body as XML into a value of type T with a streaming decoder. A body over the MaxSize limit returns ErrBodyTooLarge; malformed XML or invalid numeric fields return ErrInvalidXML. Operational read failures and ordinary application decoder errors retain their cause without becoming input problems. Strict returns ErrStrictUnsupported without reading the body.

Only the first XML element is decoded; trailing data is ignored. encoding/xml does not expand DTD entities, so entity expansion attacks such as "Billion Laughs" do not apply.

Example:

feed, err := request.XML[Feed](r, request.MaxSize(1<<20))
if err != nil {
	return err
}
_ = feed

Types

type BodyOption

type BodyOption func(*bodyConfig)

BodyOption changes how one call reads and decodes a request body. Options apply in order, so a later option overrides an earlier one of the same kind. Without options unknown JSON members are ignored and reads are limited to the size in the request context (bodylimit middleware), or DefaultMaxBodySize. Options do not remove a ceiling already enforced by the request body's reader.

Example:

options := []request.BodyOption{request.Strict(), request.MaxSize(1 << 20)}
input, err := request.JSON[CreateUser](r, options...)
if err != nil {
	return err
}
_ = input

func MaxSize

func MaxSize(n int64) BodyOption

MaxSize limits the body to n bytes for this call, overriding both the limit in the request context (the bodylimit middleware) and DefaultMaxBodySize. A larger read fails with ErrBodyTooLarge. A negative n selects DefaultMaxBodySize; zero allows only an empty body. The option cannot raise a ceiling already enforced by an upstream http.MaxBytesReader. It bounds input bytes read, not the total memory required by the decoded value.

Example:

input, err := request.JSON[CreateUser](r, request.MaxSize(1<<20))
if err != nil {
	return err
}
_ = input

func Strict

func Strict() BodyOption

Strict makes JSON decoding reject members the target type does not define. The violations surface as "unknown" violations from ValidateJSON and ParseJSON. It is not supported for XML, Bytes or String: those return ErrStrictUnsupported before reading the body.

Example:

input, err := request.JSON[CreateUser](r, request.Strict())

func Unlimited

func Unlimited() BodyOption

Unlimited adds no per-call read limit, ignoring the stored request-context limit and DefaultMaxBodySize. It cannot remove a ceiling already enforced on r.Body by middleware. Use it only where memory use is bounded some other way; a hostile client can otherwise exhaust server memory.

Example:

data, err := request.Bytes(r, request.Unlimited())
if err != nil {
	return err
}
_ = data

type Form

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

Form is a parsed multipart/form-data body: text fields plus uploaded files. It is a thin wrapper around multipart.Form; get one with Multipart. File headers can refer to memory or temporary files; the caller must follow Multipart's cleanup ownership rules before retaining them.

Example:

form, err := request.Multipart(r, 20<<20)
if err != nil {
	return err
}
return response.String(w, http.StatusOK, form.Value("title"))

func Multipart

func Multipart(r *http.Request, maxSize int64) (*Form, error)

Multipart parses the request body as multipart/form-data and returns its fields and files. Up to 8 MiB of file content is held in memory and larger files are spilled to temporary files on disk. The body is capped at maxSize bytes by replacing r.Body with an http.MaxBytesReader. A negative maxSize uses the limit in the request context (the bodylimit middleware), else DefaultMaxBodySize.

The body is parsed once. Later calls on the same request return the cached result and ignore their maxSize, so the first call's limit wins. After a call that returned an error the body is consumed, so do not call again.

Input errors are problem.Details the router renders directly, with the cause attached. Body transport and temporary-file I/O failures are returned without converting them into client validation feedback:

Temporary files: when the request was served through a router.Handler, Multipart registers an AfterResponse hook that calls Form.RemoveAll, once per request. Without hooks, such as in a plain http.Handler, the caller must defer Form.RemoveAll. A form parsed elsewhere keeps its existing cleanup owner; reusing r.MultipartForm does not transfer that ownership.

Example:

form, err := request.Multipart(r, 20<<20)
if err != nil {
	return err
}

title := form.Value("title")
avatar := form.File("avatar") // nil when not uploaded

func (*Form) File

func (form *Form) File(name string) *multipart.FileHeader

File returns the first file uploaded under name, or nil when none was. Reading a header cannot fail, so there is no error: absence is the only case, and it is not an error because most upload fields are optional. Use validate.File with a callback that calls FileBuilder.Required to demand one.

Example:

form, err := request.Multipart(r, 20<<20)
if err != nil {
	return err
}
if header := form.File("avatar"); header != nil {
	_ = header.Size // inspect metadata before opening or storing the upload
}

func (*Form) Files

func (form *Form) Files(name string) []*multipart.FileHeader

Files returns every file uploaded under name in the order sent. The result is a copy of the slice; it is nil when none was uploaded. The headers themselves remain shared and keep the lifetime described by Multipart and Form.RemoveAll.

Example:

form, err := request.Multipart(r, 20<<20)
if err != nil {
	return err
}
for _, header := range form.Files("attachments") {
	_ = header.Filename
}

func (*Form) RemoveAll

func (form *Form) RemoveAll() error

RemoveAll deletes the temporary files holding uploads that exceeded the in-memory threshold. It is safe to call more than once. When Multipart parses the form with hooks installed, it arranges cleanup after the response. Headers for successfully removed disk files can no longer be opened; in-memory file parts remain readable. A removal failure is returned and can leave files behind. Consume and close uploaded files before cleanup.

Example:

form, err := request.Multipart(r, 20<<20)
if err != nil {
	return err
}
// Consume uploaded files before explicitly releasing temporary storage.
if err := form.RemoveAll(); err != nil {
	return err
}

func (*Form) Value

func (form *Form) Value(name string) string

Value returns the first text field called name, or "" when it is absent. Later duplicate values do not replace the first; use Form.Values to keep every submitted value.

Example:

form, err := request.Multipart(r, 20<<20)
if err != nil {
	return err
}
return response.String(w, http.StatusOK, form.Value("title"))

func (*Form) Values

func (form *Form) Values(name string) []string

Values returns every text field called name in the order sent. The result is a copy; it is nil when the field is absent. Modifying the result does not change the parsed form's values.

Example:

form, err := request.Multipart(r, 20<<20)
if err != nil {
	return err
}
return response.JSON(w, http.StatusOK, form.Values("tag"))

type HookRegistry

type HookRegistry interface {
	// AfterResponse registers callbacks that run after the response is completed.
	// The argument is the handler's returned error, not proof of client delivery.
	//
	// Example:
	//
	//	request.Hooks(r).AfterResponse(func(err error) {
	//		if err != nil {
	//			slog.ErrorContext(r.Context(), "request failed", "error", err)
	//		}
	//	})
	AfterResponse(...func(error))
	// BeforeWrite registers callbacks that run before response bytes are written.
	// A callback observes an attempted write, which can still fail afterward.
	//
	// Example:
	//
	//	request.Hooks(r).BeforeWrite(func(_ http.ResponseWriter, data []byte) {
	//		slog.DebugContext(r.Context(), "response write", "bytes", len(data))
	//	})
	BeforeWrite(...func(http.ResponseWriter, []byte))
	// BeforeWriteHeader registers callbacks before final response commitment.
	// Use it to change headers before the writer accepts the final status.
	//
	// Example:
	//
	//	request.Hooks(r).BeforeWriteHeader(func(writer http.ResponseWriter, _ int) {
	//		writer.Header().Set("Cache-Control", "no-store")
	//	})
	BeforeWriteHeader(...func(http.ResponseWriter, int))
}

HookRegistry provides lifecycle hook registration for an Arkoren HTTP request. Obtain it with Hooks only inside a router handler. Callbacks remain attached to that request; registering one does not execute it immediately.

Example:

registry := request.Hooks(r)
registry.BeforeWriteHeader(func(writer http.ResponseWriter, _ int) {
	writer.Header().Set("Cache-Control", "no-store")
})

func Hooks

func Hooks(r *http.Request) HookRegistry

Hooks returns the hooks instance used to attach callbacks to lifecycle events. It panics with ErrNoHooksMiddleware when the request was not served through a router.Handler. There is no non-panicking public alternative; use it only where router wiring guarantees lifecycle-hook installation.

Example:

registry := request.Hooks(r)
registry.BeforeWriteHeader(func(writer http.ResponseWriter, _ int) {
	writer.Header().Set("Cache-Control", "no-store")
})
Example

ExampleHooks edits a header before final response commitment.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"arkoren.dev/core/http/request"
	"arkoren.dev/core/http/response"
	"arkoren.dev/core/http/router"
)

func main() {
	handler := router.Handler(func(writer http.ResponseWriter, incoming *http.Request) error {
		request.Hooks(incoming).BeforeWriteHeader(func(writer http.ResponseWriter, _ int) {
			writer.Header().Set("Cache-Control", "no-store")
		})

		return response.String(writer, http.StatusOK, "ready")
	})

	writer := httptest.NewRecorder()

	handler.ServeHTTP(writer, httptest.NewRequest(http.MethodGet, "/", nil))
	fmt.Println(writer.Code, writer.Header().Get("Cache-Control"), writer.Body.String())
}
Output:
200 no-store ready

type Scalar

type Scalar interface {
	~string | ~bool |
		~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
		~float32 | ~float64
}

Scalar is the set of types a path or query parameter can be parsed into. The tilde forms admit named types such as `type UserID int64`. Parsing uses the underlying Go scalar kind, not application schema rules.

Example:

type UserID int64
id, err := request.Param[UserID](r, "id")
if err != nil {
	return err
}
_ = id

type SessionOption

type SessionOption func(*sessionConfig)

SessionOption changes the context selection for one Session or MustSession call. Options run in order; a nil entry panics. An expanded nil slice supplies no options. Use WithSessionHandle rather than middleware construction settings when several stores share one request.

Example:

current, ok := request.Session(r, request.WithSessionHandle(applicationSession))
if !ok {
	return session.ErrNoSession
}
_ = current

func WithSessionHandle

func WithSessionHandle(handle session.Handle) SessionOption

WithSessionHandle selects the bridge used by session.Builder.Handle. Later options replace earlier selections; zero restores default lookup. Missing selected sessions never fall back to another handle or the default.

Example:

current := request.MustSession(r, request.WithSessionHandle(applicationSession))
if err := current.Put(session.NewField[string]("theme"), "dark"); err != nil {
	return err
}

Directories

Path Synopsis
internal
decode
Package decode converts native JSON semantics to safe validation feedback.
Package decode converts native JSON semantics to safe validation feedback.

Jump to

Keyboard shortcuts

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