httpx

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package httpx is the wire shape of the routes rig owns.

Four small things, each of which three packages had a copy of: how JSON goes out, how an error goes out, how a body comes in, and how the caller is established before any of it. The value is not the lines saved — it is that a client parsing a failure from /auth/login and a failure from /notifications is parsing one shape, which had stopped being true. `notifyhttp` and `presencehttp` answered a nested `{"error":{code,message}}` that neither of rig's own client libraries can read, and `authhttp` answered a flat envelope missing the per-field detail a validation failure is worth.

Why a package of its own

The error writer needs throttle.RefusalOf to put a Retry-After on a 429, and `runtime/throttle` already imports `runtime/rigerr`. So these cannot live in `rigerr`, which is the other obvious home: it would be a cycle. They are not in `runtime/serve` either, which is the process's lifecycle rather than the wire.

The keys are camelCase, always

A project's `api.json_case` renames the fields of its *own* resources, and the generated server's error envelope goes through it. This one does not. These routes are identical in every project and the browser packages are compiled against them once, which is the same argument presence's `Beat` already makes for its own fields. So the generated `DefaultErrorMapper` and WriteError write the same shape under the same names for a camelCase project and deliberately diverge for any other — and the generated one stays generated, because one struct cannot be both.

Index

Constants

View Source
const MaxBodyBytes = 1 << 20

MaxBodyBytes bounds a request body when a route does not say otherwise.

A limit at all, because without one a single client can exhaust the server's memory by streaming forever into a handler that is waiting for a small JSON object. A mebibyte, because it is far past any body these routes take and far short of anything that matters.

Variables

This section is empty.

Functions

func Decode

func Decode(r *http.Request, limit int64, into any) error

Decode reads a JSON request body, refusing a field the route does not declare.

DisallowUnknownFields for the reason the generated decoder uses it: a client that sent `accountId` believing it meant something should be told so, not silently ignored — the field it was reaching for is often the one the route exists to make unreachable.

A limit of zero or less is MaxBodyBytes. An unauthenticated route should pass something smaller, because there the limit is the only thing between a stranger and the server's memory.

An empty body is its own message. It is the most common client mistake by a wide margin, and "unexpected end of JSON input" is not what the person reading it needs to know.

func Fail

func Fail(w http.ResponseWriter, _ *http.Request, err error)

Fail is WriteError in the shape a route's error-writer option takes.

It is the fallback for a handler mounted on its own. A project with a generated server passes that server's writer instead, so a failure from one of these routes carries the request id and lands in the same log line as every other route's.

func ParseBool

func ParseBool(name, raw string) (bool, error)

ParseBool reads a boolean.

strconv.ParseBool's spelling, so `1`, `t` and `TRUE` are all true. Wider than the `true`/`false` the message names, and deliberately so: the message tells a client what to send, and the parser accepts what a client is likely to have sent anyway.

func ParseFloat

func ParseFloat(name, raw string) (float64, error)

ParseFloat reads a number.

func ParseInt

func ParseInt(name, raw string) (int, error)

ParseInt reads a whole number.

func ParseInt64

func ParseInt64(name, raw string) (int64, error)

ParseInt64 reads a whole number that may not fit in an int.

func ParseTime

func ParseTime(name, raw string) (time.Time, error)

ParseTime reads an RFC 3339 timestamp.

func ParseUUID

func ParseUUID(name, raw string) (uuid.UUID, error)

ParseUUID reads an identifier.

The message says "identifier" rather than "UUID" because that is what it is to whoever is reading it: a client that got one out of a previous response and sent it back does not need to be told what shape rig keeps them in.

func PathInt

func PathInt(r *http.Request, name string) (int, error)

PathInt reads a whole number out of the path.

func PathString

func PathString(r *http.Request, name string) (string, error)

PathString reads a path segment that must be there.

It can be missing in exactly one way — a handler asking for a segment the pattern it was registered under does not have — so the refusal is a 400 rather than a 500 on the theory that a router rig did not write may have got there first.

func PathUUID

func PathUUID(r *http.Request, name string) (uuid.UUID, error)

PathUUID reads an identifier out of the path.

func QueryBool

func QueryBool(r *http.Request, name string, fallback bool) (bool, error)

QueryBool reads a boolean query parameter, falling back when absent.

func QueryInt

func QueryInt(r *http.Request, name string, fallback int) (int, error)

QueryInt reads a whole-number query parameter, falling back when absent.

func QueryOptional

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

QueryOptional reads a query parameter and says whether it was there.

func QueryRequired

func QueryRequired(r *http.Request, name string) (string, error)

QueryRequired reads a query parameter that must be present.

A shape whose scoping depends on a value is a shape that must not stream without it, which is the one place rig insists on a query parameter.

func QueryString

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

QueryString reads a query parameter, falling back when absent.

func QueryTime

func QueryTime(r *http.Request, name string) (time.Time, error)

QueryTime reads a timestamp out of the query string.

Absent is the zero time and no error, for the reason QueryUUID gives.

func QueryUUID

func QueryUUID(r *http.Request, name string) (uuid.UUID, error)

QueryUUID reads an identifier out of the query string.

Absent is uuid.Nil and no error, because every route that reads one treats it as a filter that was not asked for.

func WriteError

func WriteError(w http.ResponseWriter, requestID string, err error)

WriteError writes err as an Error. Pass an empty requestID where there is none to read.

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, body any)

WriteJSON writes a JSON response. A nil body is a status and no bytes.

Types

type Answer

type Answer struct {
	// Code and Status are the same fact twice, because a caller assembling a
	// response wants both and deriving one from the other at four call sites is
	// how they drift.
	Code   rigerr.Code
	Status int
	// Message is already redacted.
	Message string
	// Fields is nil unless the failure carried per-field detail.
	Fields any
}

Answer is what an error means on the wire, before anything is encoded.

Separate from Error so a caller with an envelope of its own — the generated server, whose field names go through `api.json_case` — can share the decision without sharing the shape. That is the whole seam between the two: the classification is one implementation, the encoding is two.

func AnswerFor

func AnswerFor(w http.ResponseWriter, err error) Answer

AnswerFor classifies err, and sets on w any header the error carries.

Two things it does that a caller must not have to remember. **An internal failure's detail never reaches the client**: it is exactly the kind of thing that leaks a table name, a constraint, or a connection string, so the message becomes a fixed sentence and the request id becomes the only way to find out more. And **a 429 leaves with its Retry-After**, because a client told to slow down without being told for how long has nothing to do but guess, and clients that guess retry immediately.

type Caller

type Caller struct {
	// Of identifies the caller. Required.
	//
	// Taken rather than assumed for the reason the generated server takes it: a
	// project authenticates its own way, and a route that established the caller
	// differently from every other route in the same application would be a
	// second answer to the one question a tenant boundary rests on.
	Of func(*http.Request) (tenancy.Claims, error)

	// Fail writes the refusal when Of cannot answer. Nil means [Fail].
	Fail func(http.ResponseWriter, *http.Request, error)
}

Caller establishes who is asking, once, so that every route under it narrows to them structurally rather than as something each handler remembers to do.

func (Caller) Wrap

func (c Caller) Wrap(next http.HandlerFunc) http.HandlerFunc

Wrap resolves the caller, puts them on the request's context, and hands it on.

The claims reach the handler through the context rather than as a parameter, because the context is where every layer beneath the handler already reads them from. A handler taking them twice is two answers to one question, and the interesting case is when they disagree.

type Error

type Error struct {
	// Code is the machine-readable one. A client branches on this, never on the
	// message.
	Code rigerr.Code `json:"code"`
	// Message is for a person. For an internal failure it says nothing about the
	// internals — see [AnswerFor].
	Message string `json:"message"`
	// RequestID is what finds the detail in the logs, and for an internal failure
	// it is the only thing a client is told. Omitted where there is none: the
	// hand-written routes have no request context to read one from.
	RequestID string `json:"requestId,omitempty"`
	// Fields is one member per field of the input, for a validation failure. It
	// is the difference between a client highlighting the field somebody got
	// wrong and a client parsing prose to work out which one that was.
	Fields any `json:"fields,omitempty"`
}

Error is the envelope every route rig owns answers a failure with.

Flat — `code` and `message` at the top level, not nested under an `error` key — because that is what the generated server ships and what both of rig's client libraries parse. A nested envelope decodes into an all-zero struct there: the code comes out empty, so every `IsNotFound` and `IsForbidden` answers false and the caller is left with the status and nothing else.

Jump to

Keyboard shortcuts

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