Documentation
¶
Overview ¶
Package httpx is the HTTP scaffolding every gogo app shares: the archetype gate that turns a route's Access set into the right allow/deny, the JSON error contract, content negotiation, security headers with a composable CSP, and the principal request-context plumbing. It sits above auth and store; the web package sits above it. See docs/design.md, section 2 "The route contract".
Index ¶
- Constants
- Variables
- func BaselineOf(ctx context.Context) *auth.Principal
- func ClientIPOf(ctx context.Context) string
- func Decode[T any](w http.ResponseWriter, r *http.Request) (T, bool)
- func DecodeBody[T any](r *http.Request) (T, error)
- func Fail(w http.ResponseWriter, r *http.Request, code int, reason string)
- func PrincipalOf(ctx context.Context) *auth.Principal
- func Register[I, O any](a *API, op Op, handler func(context.Context, *I) (*O, error))
- func SecurityHeaders(csp string, next http.Handler) http.Handler
- func SetHTMLError(fn func(w http.ResponseWriter, r *http.Request, code int, reason string))
- func WriteJSON(w http.ResponseWriter, code int, body any)
- type API
- type APIInfo
- type CSP
- type Config
- type Errors
- type Kind
- type Limiter
- type NavItem
- type Op
- type Route
- type Router
- type Rule
Constants ¶
const MaxBodyBytes = 1 << 16
MaxBodyBytes bounds an API request body at 64 KiB: enormous for a JSON resource, and small enough that a hostile body costs nothing to refuse. A byte stream -- an upload -- is not a resource and does not come through here. See docs/design.md, FR-10.5.
Variables ¶
var ( Error400BadRequest = huma.Error400BadRequest Error403Forbidden = huma.Error403Forbidden Error404NotFound = huma.Error404NotFound Error409Conflict = huma.Error409Conflict Error413RequestEntityTooLarge = huma.Error413RequestEntityTooLarge Error429TooManyRequests = huma.Error429TooManyRequests Error500InternalServerError = huma.Error500InternalServerError )
The refusals a typed handler makes on its own account -- an ownership check, a policy, a field a caller may not set -- as distinct from one its store made, which goes through an Errors table. Re-exported so an app never imports huma to say "no".
var ErrBadJSON = errors.New("body is not valid JSON")
ErrBadJSON is what a body that is not JSON is refused with. It is the reason string a caller sees, so it says what is wrong with their request and nothing about ours.
Functions ¶
func BaselineOf ¶ added in v1.8.0
BaselineOf is the identity that would be left underneath this caller if their session went away: the certificate or trusted-subnet grant a browser presents automatically and cannot sign out of, or anonymous when there is none. The chrome derives its sign-out wording from it, and GET /api/v1/me reports it.
It is lazy -- answering costs a header parse and a database read, and one endpoint asks -- so the gate stores a closure over the request, letting a typed handler ask a question only the request can answer without handing it the request.
func ClientIPOf ¶ added in v1.6.0
ClientIPOf returns the caller's address as the trusted-proxy rules resolved it, or "" if no gate ran. Both gates put it there, from one resolver.
func Decode ¶ added in v1.6.0
Decode reads a bounded JSON body into T and, on a malformed one, answers the caller with a 400 through the same negotiation every other refusal uses. It reports whether the handler may go on, so a handler reads as:
body, ok := httpx.Decode[createUserRequest](w, r)
if !ok {
return
}
func DecodeBody ¶ added in v1.6.0
DecodeBody reads a bounded JSON body into T. It answers nobody: handlers that also accept a form post (/login, /reset, /verify-email) render their own refusal, on their own page. It deliberately does not reject unknown fields -- that belongs where the schema owns decoding and can say which field was wrong.
func Fail ¶
Fail renders an error as JSON for API callers, as an HTML page for a browser (when a renderer is installed), and as plain text for everything else. Handlers call it for their own denials, so their negotiation matches the gate's.
func PrincipalOf ¶
PrincipalOf returns the principal the gate attached, or the anonymous principal if none (a handler behind Public may see anonymous). It takes a context and not a request, so both halves of the route contract can use it: a typed handler is given a context and no request at all.
func Register ¶ added in v1.6.0
Register adds a typed operation. It is a free function rather than a method because Go has no generic methods.
It panics when the operation declares no Access, so a forgotten gate is a bug found at boot rather than in an access log. The gate fails closed anyway -- auth.Access is a bitset whose zero value permits no archetype -- so the panic is a convenience, not the only thing between a forgotten gate and an open door. Say Public if that is what you mean. See docs/design.md, FR-2.4.
func SecurityHeaders ¶
SecurityHeaders applies the standard headers to every response, with the given (already-rendered) Content-Security-Policy.
func SetHTMLError ¶
SetHTMLError installs the HTML error-page renderer. gogo.Run calls it with the web package's renderer.
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API is the typed half of the route contract: huma over the router's own mux, and not a second router. humago.New takes a Mux interface *http.ServeMux already satisfies, so a typed operation and a raw route are registered on the same mux, matched by the same matcher, and wrapped by the same logger and security headers.
type APIInfo ¶ added in v1.6.0
type APIInfo struct {
Title string
Version string
// SpecPath serves the OpenAPI document, without an extension: "/gogo/openapi"
// answers /gogo/openapi.json and /gogo/openapi.yaml. Empty serves neither.
SpecPath string
// DocsPath serves the human-readable API browser. Empty serves none.
DocsPath string
// SessionCookie is the name of the app's session cookie, so the document can
// name the credential a browser actually carries. Empty declares only the token.
SessionCookie string
}
APIInfo names the generated document and says where it is served.
type CSP ¶
CSP is a Content-Security-Policy as directive -> sources.
The default is strict, and script-src has no 'unsafe-inline' -- permanently: gogo and its apps serve no inline script, and confirmations go through the shared modal. An app may extend a directive additively; it must not weaken the base.
func DefaultCSP ¶
func DefaultCSP() CSP
DefaultCSP returns the strict base policy shared by every app.
type Config ¶
type Config struct {
Resolver *auth.Resolver
LoginPath string // where a Page 401 redirects, e.g. "/login"
// ClearSession discards the session cookie on a login redirect. A stale cookie that survives
// the redirect is what turns the login page -- gated like everything else -- into a redirect
// back to itself, nesting `next` deeper each hop. Clearing it makes the browser follow the
// redirect with no credential, resolve as anonymous, and get the form. Optional: nil leaves
// the cookie in place (the loop the app opts out of avoiding). Only the app knows the cookie's
// name and flags, so it supplies the closure.
ClearSession func(http.ResponseWriter)
}
Config carries what the router needs to resolve and gate every request.
type Errors ¶ added in v1.6.0
type Errors []Rule
Errors is an ordered table of rules: the first one that matches wins.
It is a slice and not a map, deliberately. errors.Is matches through wrapping, so two rules genuinely can both claim one error, and the tie must be broken by an order a reader can see. That order is also what makes With an override rather than an addition. See docs/design.md, FR-10.1.
func (Errors) Fail ¶ added in v1.6.0
Fail renders err through the table. An error no rule matches is a logged 500 and never a message to the caller: a sentinel nobody mapped is a bug in the table, which the caller cannot fix, and a store error's own text says more about the inside of the service than anybody outside it should learn. Do not add a catch-all rule to spare the log line.
func (Errors) Match ¶ added in v1.6.0
Match reports the status and the reason for err, and whether any rule claimed it. It matches through wrapping, so a store error a caller has annotated (fmt.Errorf("loading the target: %w", store.ErrNotFound)) still finds its rule.
func (Errors) Status ¶ added in v1.6.0
Status renders err as an error a typed handler returns, rather than one it writes:
if err != nil {
return nil, userErrors.Status(err)
}
It is Fail's other half, over the same table and under the same rule: an error no rule names is a logged 500, and the caller is told "internal error" and no more.
func (Errors) With ¶ added in v1.6.0
With derives a table that answers differently for one sentinel, leaving the table it derives from untouched. A sentinel can mean different things to different verbs: store.ErrReservedUser is "cannot be deleted" to a DELETE and "can only be enabled or disabled" to a PATCH. With prepends, and the first matching rule answers -- which is what makes it an override rather than an addition.
type Kind ¶
type Kind uint8
Kind selects how a denial is rendered for a raw route.
const ( // JSON renders denials as the error envelope: unauthenticated -> 401, // authenticated-but-wrong-tier -> 403. It means "refuse as JSON rather than as a // page", not "this is the API": a raw route may answer JSON without being a typed // operation. JSON Kind = iota // Page renders denials for a browser: unauthenticated -> 303 to the login // path with a next parameter, authenticated-but-wrong-tier -> 403 HTML page. Page )
type Limiter ¶ added in v1.2.0
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is a fixed-window counter, keyed by whatever the caller decides the subject of the limit is: a client IP for a login form, an account for a mail that account can ask to have sent to itself.
It is deliberately not part of the Route contract: a route declares who may enter, and how often they may knock is the handler's business (docs/design.md, FR-5.6). It composes with, and does not replace, the per-account lockout in the store -- one stops a thousand guesses at one account, the other one guess at a thousand.
func NewLimiter ¶ added in v1.2.0
NewLimiter allows limit events per key per window. A limit of zero or less disables it -- every call to Allow succeeds -- which is what an unconfigured brake should do.
func (*Limiter) Allow ¶ added in v1.2.0
Allow records one event against key and reports whether it is within the limit. A refused event is still counted, so a caller that keeps hammering keeps the window open rather than sneaking through the moment it lapses.
func (*Limiter) Len ¶ added in v1.2.0
Len is how many keys the limiter is currently tracking, for the tests and a future gauge.
func (*Limiter) Limited ¶ added in v1.2.0
Limited reports whether key is currently over its limit, without counting an event. It is what a caller uses when only failures should count: a login form checks Limited before it does any work, calls Allow when the attempt fails, and Reset when it succeeds. Counting successes too would rate-limit a busy office behind one NAT.
func (*Limiter) Reset ¶ added in v1.2.0
Reset forgets a key. A successful sign-in calls it: the limiter is there to bound guessing, and whoever just proved a credential was not guessing.
func (*Limiter) RetryAfter ¶ added in v1.2.0
RetryAfter is how long key must wait, rounded up to a second, and what the Retry-After header on a 429 carries. It is zero when the key is not limited.
func (*Limiter) Status ¶ added in v1.6.0
Status is the refusal a caller who has knocked too often gets from a typed handler: a 429 carrying Retry-After, returned rather than written. No handler should build one by hand -- only the limiter knows when the window lapses, and a forgotten Retry-After is a client that retries in a tight loop.
type NavItem ¶
type NavItem struct {
}
NavItem is a header-menu entry, shown only to callers whose archetype is in Access. The library filters the menu with the same gate it routes with.
type Op ¶ added in v1.6.0
type Op struct {
// ID is the operationId in the document, and the name of the method a generated
// client will call. Changing it renames somebody's generated function.
ID string
Method string
Path string
Summary string
Description string
Tags []string
// Access is the set of archetypes admitted. There is no default, and the zero
// value admits nobody -- see Register.
Access auth.Access
// Status is the success status. Zero means huma's default (200, or 204 when the
// operation has no body).
Status int
// Errors are the statuses this operation can answer with, so they appear in the
// document. The gate's own 401 and 403 are added for you.
Errors []int
}
Op is one typed operation: what huma needs to describe it, plus the one thing the route contract adds -- who may enter.
type Route ¶
type Route struct {
Method string
Pattern string
Handler http.HandlerFunc
Access auth.Access
Kind Kind
}
Route is one method+pattern, its handler, which archetypes may enter, and how a denial is rendered. It is the unit an application declares.
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router builds the ServeMux, wrapping each route in the archetype gate and a per-pattern metric label.
func (*Router) HandlePublic ¶
HandlePublic registers a public, instrumented handler with no archetype gate and no credential resolution -- for /healthz and /assets, which must answer every caller (a bad token must not turn /healthz into a 401) yet still be counted.