httputil

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 19, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package httputil provides HTTP helpers for JSON APIs and chi-based servers.

Error handling and responses

HandleError writes a JSON error response; if err implements HTTPStatus and GetCode (e.g. httperr.HTTPError), that status and code are used, otherwise 500 INTERNAL_ERROR. ErrorHandler wraps logging (Info for 4xx, Error for 5xx) and HandleError. ErrorResponse and ValidationErrorResponse are the JSON shapes. ValidationHTTPError combines *HTTPError with a slice of field errors. RenderJSON, RenderOK, RenderCreated, RenderAccepted, RenderNoContent, RenderError, RenderErrorWithCode, RenderInvalidID, and RenderText send typed responses. RenderText writes the body as-is with no escaping; use Content-Type text/plain and do not pass user-controlled HTML to avoid XSS. For 5xx, RenderError and RenderErrorWithCode replace the message with "Internal server error".

Request decoding and validation

DecodeAndValidate reads JSON from the body (limit MaxRequestBodySize), disallows unknown fields and trailing data, then validates with Validator; on failure it writes the response and returns false. DecodeAndValidateE returns an error instead of writing. DecodeJSON decodes without validation. ParseMultipartFormLimit parses multipart form with body and memory limits.

Pagination

ClampPage, ClampPerPage, ClampLimit, ParseIntQuery, TotalPages, NewPaginationMeta, and PaginationMeta support page/per-page handling. Paginated[T] holds a page of items and metadata; NewPaginated builds it; FetchPage runs fetch and count in parallel and returns Paginated. MaxPage is the maximum allowed page number.

Query parsing

ParseBoolQuery, ParseEnumQuery, ParseSortQuery, and ParseTimeQuery parse and validate query parameters.

Client IP

ParseTrustedProxyCIDRs parses CIDR strings for trusted proxies. GetClientIPWithNets returns the client IP using X-Real-IP and X-Forwarded-For when the connection is from a trusted net. Restrict trustedNets to your actual proxy/load-balancer CIDRs; overly broad ranges (e.g. 0.0.0.0/0) allow clients to spoof X-Forwarded-For. GetClientIPE parses CIDRs from strings and returns an error on invalid input. GetClientIP is deprecated; use GetClientIPE.

Path and context

ChiPathFromRequest returns the chi route pattern. UserIDKey is the context key for the authenticated user ID. GetUserID, ParseUUID, ParseUUIDField, and ParseAuthUserID read or parse IDs and optionally write error responses.

Search and download

ValidateSearchQ checks length and control characters. EscapeILIKE and SanitizeSearchQ escape strings for PostgreSQL ILIKE. RenderJSONAttachment, RenderStream, RenderStreamLimited, and RenderBytes send file downloads with sanitized Content-Disposition filenames. RenderStreamLimited returns ErrStreamTruncated when maxBytes > 0 and the source exceeds the limit (response is already committed). RenderStream and RenderStreamLimited return ErrInvalidContentType for disallowed content types.

SSE and health

NewSSEWriter and NewSSEWriterWithLimit return an SSEWriter for Server-Sent Events; Send and SendJSON are concurrent-safe. Close marks the writer done. NewSSEWriterWithLimit accepts MaxEventBytes(n) to cap event payload size (default 64KB); larger payloads return ErrSSEPayloadTooLarge. HealthHandler runs Checker implementations in parallel with a timeout and returns JSON status and checks. Use HealthOnEncodeError to log or handle encode failures; HealthHideDetails omits check details from the response.

Index

Constants

View Source
const DefaultSearchMaxLen = 100

DefaultSearchMaxLen is the default maximum rune length for search input in ValidateSearchQ and EscapeILIKE.

View Source
const MaxPage = 10000

MaxPage is the maximum allowed page number for ClampPage.

View Source
const MaxRequestBodySize = 1 << 20

MaxRequestBodySize is the default body size limit (1 MiB) for DecodeAndValidate, DecodeAndValidateE, and DecodeJSON.

View Source
const UserIDKey contextKey = "user_id"

UserIDKey is the context key for the authenticated user ID (string, e.g. UUID). Set by auth middleware; read with GetUserID.

Variables

View Source
var ErrInvalidContentType = errors.New("content-type contains invalid characters")

ErrInvalidContentType is returned when contentType contains CR/LF or disallowed characters (header injection).

View Source
var ErrRequestBodyTooLarge = errors.New("request body too large")

ErrRequestBodyTooLarge is returned by DecodeJSON when the request body exceeds the configured limit.

View Source
var ErrSSEClosed = errors.New("SSE writer closed")

ErrSSEClosed is returned by Send and SendJSON after the writer has been closed.

View Source
var ErrSSEPayloadTooLarge = errors.New("SSE event payload exceeds size limit")

ErrSSEPayloadTooLarge is returned by Send and SendJSON when the event payload would exceed MaxEventBytes.

View Source
var ErrStreamTruncated = errors.New("response stream truncated: source exceeded max bytes")

ErrStreamTruncated is returned by RenderStreamLimited when the source exceeds maxBytes and the response was truncated.

Functions

func ChiPathFromRequest added in v0.1.1

func ChiPathFromRequest(r *http.Request) string

ChiPathFromRequest returns the chi route pattern (e.g. "/users/{id}") from the request context, or "" if not set.

func ClampLimit

func ClampLimit(p *int, defaultVal, maxVal int) int

ClampLimit returns limit clamped to [defaultVal, maxVal], or defaultVal if nil/<=0.

func ClampPage

func ClampPage(p *int) int

ClampPage returns the page number clamped to [1, MaxPage]; uses 1 if p is nil or *p < 1.

func ClampPerPage

func ClampPerPage(p *int, defaultVal, maxVal int) int

ClampPerPage returns perPage clamped to [defaultVal, maxVal], or defaultVal if nil/<=0. If defaultVal > maxVal, the effective default is maxVal so the result is never above maxVal.

func DecodeAndValidate

func DecodeAndValidate[T any](w http.ResponseWriter, r *http.Request, v Validator, opts ...DecodeOption) (T, bool)

DecodeAndValidate reads JSON from the request body (limit from WithMaxBodySize or MaxRequestBodySize, no unknown fields, no trailing data), then validates with v. On error it writes the appropriate JSON response and returns (zero, false).

func DecodeAndValidateE

func DecodeAndValidateE[T any](r *http.Request, v Validator, opts ...DecodeOption) (T, error)

DecodeAndValidateE reads and validates JSON from the request body and returns an error without writing a response. Returns *httperr.HTTPError for invalid JSON, trailing data, body too large, or validation failure.

func DecodeJSON

func DecodeJSON[T any](r *http.Request, v *T, opts ...DecodeOption) error

DecodeJSON decodes JSON from the request body (limit from WithMaxBodySize or MaxRequestBodySize, no unknown fields, no trailing data) into v.

func EscapeILIKE

func EscapeILIKE(s string, maxLen int) string

EscapeILIKE escapes %, _, and \ for safe use in PostgreSQL ILIKE. Output is truncated to maxLen runes (or DefaultSearchMaxLen if maxLen <= 0).

func GetClientIP deprecated

func GetClientIP(r *http.Request, trustedProxyCIDRs []string) string

GetClientIP returns the client IP. When RemoteAddr is in a trusted proxy CIDR, X-Real-IP and X-Forwarded-For are used. Parse errors from trustedProxyCIDRs are ignored; if all are invalid, only RemoteAddr is used.

Deprecated: Use GetClientIPE so invalid CIDR configuration is not silently ignored.

func GetClientIPE added in v0.1.4

func GetClientIPE(r *http.Request, trustedProxyCIDRs []string) (string, error)

GetClientIPE returns the client IP like GetClientIP but returns an error when ParseTrustedProxyCIDRs fails (e.g. all CIDRs invalid). When only some CIDRs are invalid, returns the IP using valid nets and the parse error so the caller can log it.

func GetClientIPWithNets added in v0.1.4

func GetClientIPWithNets(r *http.Request, trustedNets []*net.IPNet) string

GetClientIPWithNets returns the client IP, using X-Real-IP and X-Forwarded-For when RemoteAddr is in trustedNets.

func GetUserID

func GetUserID(ctx context.Context) string

GetUserID returns the authenticated user ID from context, or "" if not set.

func HandleError

func HandleError(w http.ResponseWriter, r *http.Request, err error)

HandleError writes a JSON error response. If err implements HTTPStatus and GetCode (e.g. *httperr.HTTPError), that status and code are used; otherwise 500 and INTERNAL_ERROR. For 5xx the message is replaced with "Internal server error". ValidationHTTPError is rendered as ValidationErrorResponse with per-field errors.

func HealthHandler added in v0.1.3

func HealthHandler(checkers map[string]Checker, opts ...HealthHandlerOption) http.HandlerFunc

HealthHandler returns an HTTP handler that runs all checkers in parallel with a timeout and returns JSON with status and optional check results.

func ParseAuthUserID

func ParseAuthUserID(w http.ResponseWriter, r *http.Request) (uuid.UUID, bool)

ParseAuthUserID returns the authenticated user's UUID from context. Writes 401 and returns (uuid.Nil, false) if not authenticated or invalid.

func ParseBoolQuery added in v0.1.3

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

ParseBoolQuery parses the query parameter key as a boolean. Accepts "1", "true", "yes" for true and "0", "false", "no" for false. Returns (value, true) when valid, (false, false) when missing or invalid.

func ParseEnumQuery added in v0.1.3

func ParseEnumQuery[T ~string](r *http.Request, key string, allowed []T) (T, bool)

ParseEnumQuery parses the query parameter key and returns it as T if it is in allowed; otherwise returns ("", false).

func ParseIntQuery

func ParseIntQuery(r *http.Request, key string) *int

ParseIntQuery parses the first query parameter key as positive int; returns nil if missing or invalid.

func ParseMultipartFormLimit

func ParseMultipartFormLimit(w http.ResponseWriter, r *http.Request, maxBodySize, maxMemory int64) bool

ParseMultipartFormLimit parses the multipart form with the given body size and memory limits. On error writes an error response and returns false.

func ParseSortQuery added in v0.1.3

func ParseSortQuery(r *http.Request, allowedFields []string) (field, dir string, ok bool)

ParseSortQuery parses the "sort" query parameter. Supports "field", "-field", or "field:asc"/"field:desc". Returns (field, dir, true) when field is in allowedFields and dir is "asc" or "desc"; otherwise ("", "", false).

func ParseTimeQuery added in v0.1.3

func ParseTimeQuery(r *http.Request, key, layout string) (time.Time, bool)

ParseTimeQuery parses the query parameter key with the given time layout (e.g. time.RFC3339). Returns (zero time.Time, false) on missing or invalid value.

func ParseTrustedProxyCIDRs added in v0.1.4

func ParseTrustedProxyCIDRs(cidrs []string) ([]*net.IPNet, error)

ParseTrustedProxyCIDRs parses CIDR strings into networks. Invalid or empty entries are skipped. Returns (nil, error) if all entries are invalid. If only some are invalid, returns (valid nets, error) so the caller can log the invalid list.

func ParseUUID

func ParseUUID(w http.ResponseWriter, r *http.Request, id string) (uuid.UUID, bool)

ParseUUID parses id as a UUID. On failure writes an error response and returns (uuid.Nil, false).

func ParseUUIDField

func ParseUUIDField(w http.ResponseWriter, r *http.Request, value, field string) (uuid.UUID, bool)

ParseUUIDField parses value as a UUID. On failure writes a validation error for the given field name and returns (uuid.Nil, false). field is used in the error message sent to the client; it must be a constant or sanitized (e.g. alphanumeric, underscore). Do not pass user-controlled input as field.

func Ptr

func Ptr[T any](v T) *T

Ptr returns a pointer to v. Useful for optional query params.

func RenderAccepted added in v0.1.1

func RenderAccepted[T any](w http.ResponseWriter, r *http.Request, data T)

RenderAccepted sends 202 Accepted with data as JSON body.

func RenderBytes

func RenderBytes(w http.ResponseWriter, contentType, filename string, data []byte) error

RenderBytes writes raw bytes with Content-Type and Content-Disposition attachment (filename sanitized).

func RenderCreated

func RenderCreated[T any](w http.ResponseWriter, r *http.Request, data T)

RenderCreated sends 201 Created with data as JSON body.

func RenderError

func RenderError(w http.ResponseWriter, r *http.Request, status int, message string)

RenderError sends JSON error with status and message; code is derived from status. For 5xx status, message is replaced with "Internal server error" to avoid leaking details.

func RenderErrorWithCode

func RenderErrorWithCode(w http.ResponseWriter, r *http.Request, status int, message, code string)

RenderErrorWithCode sends JSON error with explicit code. For 5xx status, message is replaced with "Internal server error" to avoid leaking details.

func RenderInvalidID

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

RenderInvalidID sends 400 Bad Request with code INVALID_ID.

func RenderJSON

func RenderJSON[T any](w http.ResponseWriter, r *http.Request, status int, data T)

RenderJSON writes data as JSON with the given HTTP status.

func RenderJSONAttachment

func RenderJSONAttachment[T any](w http.ResponseWriter, data T, filename string) error

RenderJSONAttachment encodes data as JSON and writes it with Content-Disposition attachment and sanitized filename.

func RenderNoContent

func RenderNoContent(w http.ResponseWriter, _ *http.Request)

RenderNoContent sends 204 No Content with no body.

func RenderOK

func RenderOK[T any](w http.ResponseWriter, r *http.Request, data T)

RenderOK sends 200 OK with data as JSON body.

func RenderStream

func RenderStream(w http.ResponseWriter, contentType, filename string, rc io.Reader) error

RenderStream streams the response with Content-Disposition attachment. Caller is responsible for closing rc after the function returns (e.g. if rc implements io.Closer, call rc.Close() in defer). For untrusted or unbounded sources use RenderStreamLimited to cap the response size. contentType is sent as-is in the Content-Type header; it MUST NOT contain user-controlled input—validate or use a fixed allowlist to avoid header injection (e.g. XSS via attachment).

func RenderStreamLimited added in v0.1.4

func RenderStreamLimited(w http.ResponseWriter, contentType, filename string, rc io.Reader, maxBytes int64) error

RenderStreamLimited is like RenderStream but limits the number of bytes copied from rc to maxBytes. If maxBytes <= 0, no limit is applied. When maxBytes > 0 and the source exceeds the limit, the response is already committed (headers and up to maxBytes sent) and the function returns ErrStreamTruncated so the caller can log or handle the truncation. Use a pre-buffered reader or Content-Length if you need to reject before sending.

func RenderText

func RenderText(w http.ResponseWriter, _ *http.Request, status int, contentType, body string)

RenderText writes a plain text response with the given status, Content-Type, and body. Only text/plain, application/json, and application/octet-stream are allowed; any other Content-Type is forced to "text/plain; charset=utf-8" to avoid reflected XSS when body contains user input. Do not pass user-controlled content that could be interpreted as HTML; ensure clients do not render the response as HTML.

func SanitizeSearchQ

func SanitizeSearchQ(q string, maxLen int) string

SanitizeSearchQ is EscapeILIKE with default max length (DefaultSearchMaxLen when maxLen <= 0).

func TotalPages added in v0.1.2

func TotalPages(total int64, perPage int) int

TotalPages calculates the total number of pages for a given total and perPage.

func ValidateSearchQ

func ValidateSearchQ(q string) bool

ValidateSearchQ returns true if q is valid for search (within DefaultSearchMaxLen runes and no control characters).

Types

type Checker added in v0.1.3

type Checker interface {
	Check(ctx context.Context) error
}

Checker performs a single health check. Implementations should respect ctx cancellation.

type DecodeOption added in v0.2.0

type DecodeOption func(*decodeConfig)

DecodeOption configures decode behaviour (e.g. body size limit).

func WithMaxBodySize added in v0.2.0

func WithMaxBodySize(n int64) DecodeOption

WithMaxBodySize sets the request body size limit for decode. Values <= 0 use MaxRequestBodySize.

type ErrorHandler added in v0.1.2

type ErrorHandler struct {
	// Logger is optional; when set, Handle logs 4xx at Info and 5xx at Error.
	Logger logger.Logger
}

ErrorHandler handles errors by optionally logging and writing a JSON error response via HandleError.

func (*ErrorHandler) Handle added in v0.1.2

func (h *ErrorHandler) Handle(w http.ResponseWriter, r *http.Request, err error, msg string) bool

Handle logs err (if Logger is set) and writes a JSON error response. Returns true if err was non-nil and handled. 4xx errors are logged at Info level, everything else at Error level.

type ErrorResponse

type ErrorResponse struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

ErrorResponse is the JSON body for a single error (code and message).

type HealthHandlerOption added in v0.1.4

type HealthHandlerOption func(*healthOpts)

HealthHandlerOption configures HealthHandler behaviour.

func HealthHideDetails added in v0.1.4

func HealthHideDetails() HealthHandlerOption

HealthHideDetails omits per-check results from the JSON response; only status (ok/degraded) is returned.

func HealthOnEncodeError added in v0.1.4

func HealthOnEncodeError(f func(error)) HealthHandlerOption

HealthOnEncodeError sets a callback invoked when JSON encoding of the health response fails (e.g. for logging).

type Paginated added in v0.1.4

type Paginated[T any] struct {
	Data       []T   `json:"data"`
	Total      int64 `json:"total"`
	Page       int   `json:"page"`
	PerPage    int   `json:"per_page"`
	TotalPages int   `json:"total_pages"`
}

Paginated holds a page of items and pagination metadata for JSON responses.

func FetchPage added in v0.1.4

func FetchPage[T any](
	ctx context.Context,
	page, perPage int,
	fetchFn func(ctx context.Context, limit, offset int) ([]T, error),
	countFn func(ctx context.Context) (int64, error),
) (*Paginated[T], error)

FetchPage runs fetchFn and countFn in parallel and returns a Paginated result. Page and perPage are clamped to at least 1.

func NewPaginated added in v0.1.4

func NewPaginated[T any](data []T, total int64, page, perPage int) *Paginated[T]

NewPaginated builds a Paginated with TotalPages computed from total and perPage. Data is never nil in the result (empty slice if nil).

type PaginationMeta added in v0.1.2

type PaginationMeta struct {
	Page       int `json:"page"`
	PerPage    int `json:"per_page"`
	Total      int `json:"total"`
	TotalPages int `json:"total_pages"`
}

PaginationMeta holds pagination metadata for a response.

func NewPaginationMeta added in v0.1.2

func NewPaginationMeta(page, perPage int, total int64) PaginationMeta

NewPaginationMeta builds PaginationMeta with Total and TotalPages derived from total and perPage.

type SSEOption added in v0.1.4

type SSEOption func(*SSEWriter)

SSEOption configures NewSSEWriterWithLimit.

func MaxEventBytes added in v0.1.4

func MaxEventBytes(n int64) SSEOption

MaxEventBytes limits the size of a single event (event + data lines); 0 means use default (64KB).

type SSEWriter added in v0.1.3

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

SSEWriter sends Server-Sent Events. Create with NewSSEWriter or NewSSEWriterWithLimit. Send and SendJSON are safe for concurrent use. Call Close when done. The 200 response is committed on the first successful Send or SendJSON, not in NewSSEWriter.

func NewSSEWriter added in v0.1.3

func NewSSEWriter(w http.ResponseWriter) (*SSEWriter, bool)

NewSSEWriter sets SSE headers on w and returns an SSEWriter. The 200 response is sent on the first Send or SendJSON. The second return value is false if w does not implement http.Flusher.

func NewSSEWriterWithLimit added in v0.1.4

func NewSSEWriterWithLimit(w http.ResponseWriter, opts ...SSEOption) (*SSEWriter, bool)

NewSSEWriterWithLimit is like NewSSEWriter but accepts options (e.g. MaxEventBytes). Does not write 200 until the first Send or SendJSON. If MaxEventBytes is not set, a default of 64KB is used to limit memory for untrusted event sizes.

func (*SSEWriter) Close added in v0.1.3

func (s *SSEWriter) Close()

Close marks the writer as done; subsequent Send or SendJSON calls return ErrSSEClosed.

func (*SSEWriter) Send added in v0.1.3

func (s *SSEWriter) Send(event, data string) error

Send writes one SSE message (optional event line, then data lines, then newline) and flushes. Event and data are sanitized (newlines in event replaced). Returns ErrSSEPayloadTooLarge if the message would exceed MaxEventBytes.

func (*SSEWriter) SendJSON added in v0.1.3

func (s *SSEWriter) SendJSON(event string, v any) error

SendJSON marshals v to JSON and sends it as the data payload using Send.

type ValidationErrorItem

type ValidationErrorItem struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

ValidationErrorItem is one field-level validation error.

type ValidationErrorResponse

type ValidationErrorResponse struct {
	Code    string                `json:"code"`
	Message string                `json:"message"`
	Errors  []ValidationErrorItem `json:"errors,omitempty"`
}

ValidationErrorResponse is the JSON body for validation errors (code, message, and optional per-field errors).

type ValidationHTTPError added in v0.1.4

type ValidationHTTPError struct {
	*httperr.HTTPError
	Errors []ValidationErrorItem
}

ValidationHTTPError is an HTTPError that carries per-field validation errors for JSON responses.

func (*ValidationHTTPError) Error added in v0.1.4

func (e *ValidationHTTPError) Error() string

func (*ValidationHTTPError) GetCode added in v0.1.4

func (e *ValidationHTTPError) GetCode() string

func (*ValidationHTTPError) HTTPStatus added in v0.1.4

func (e *ValidationHTTPError) HTTPStatus() int

func (*ValidationHTTPError) Unwrap added in v0.1.4

func (e *ValidationHTTPError) Unwrap() error

type Validator

type Validator interface {
	Validate(any) error
}

Validator validates a value (e.g. go-playground/validator). Used by DecodeAndValidate and DecodeAndValidateE.

Directories

Path Synopsis
Package middleware provides HTTP middleware for use with go-httpkit and chi.
Package middleware provides HTTP middleware for use with go-httpkit and chi.

Jump to

Keyboard shortcuts

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