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

httpx

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 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

View Source
var ErrTooLarge = errors.New("the request body is over the limit")

ErrTooLarge is what a Bounded reader fails with past its limit. It is matched with errors.Is rather than returned to a caller, which reads it back off the decoder that was reading through one.

Functions

func Bounded added in v0.6.0

func Bounded(r io.Reader, limit int64) io.Reader

Bounded is io.LimitReader that refuses rather than truncates.

The difference is the whole point. A LimitReader stops early and says nothing, so a body one byte over the limit reaches the JSON decoder as a document that ends in the middle — and the caller is told its request was malformed, which is both untrue and unactionable. This one fails with ErrTooLarge instead, so the answer is a 413 naming the limit.

Not net/http.MaxBytesReader, which does the same job and better — it also stops the connection being held open by a client still sending. That one needs the net/http.ResponseWriter, and neither Decode nor the decoders in runtime/apibase has one to give it. Changing their signatures would reach every generated server.

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, or writes nothing at all when the caller has gone. Pass an empty requestID where there is none to read.

rigerr.Aborted is answered with silence because there is nobody to answer. The body would go into a socket that is already closed, and the status it set would make an abandoned request indistinguishable from a failed one for anything reading the response after the fact.

This is not the only copy of that rule, and cannot be: the generated DefaultErrorMapper reaches AnswerFor directly rather than coming through here, so runtime/apibase's own Fail carries it too. Here it covers the routes rig mounts itself — the inbox, presence, and the authentication routes — which answer through Fail and have no generated mapper.

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 = rigerr.Answer

Answer is rigerr.Answer, the classification every envelope rig writes shares.

An alias rather than a type of its own: the decision is one implementation and naming it twice is how two of them start. It lives in rigerr because it needs nothing from net/http, which is what lets a package that only wants to classify an error — auth/oauth, answering text/plain — reach it without taking on everything this one depends on.

func AnswerFor

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

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

The classification is rigerr.AnswerFor's, including the part a caller must not have to remember: **an internal failure's detail never reaches the client**. What is added here is the header, and there is one — **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.

So this is the call to make from a route that has a net/http.ResponseWriter in hand, and rigerr.AnswerFor the one to make from anywhere that does not.

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.

type Router added in v0.6.0

type Router interface {
	// Handle registers handler for pattern.
	Handle(pattern string, handler http.Handler)

	// HandleFunc registers handler for pattern.
	HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
}

Router is where a route is registered: the two methods of net/http.ServeMux that mounting one needs, and nothing else.

It is an interface rather than the mux itself so that a generated server can put something between the pattern and the handler — opening a span named by the route is the one thing that does so today — and have it cover the routes rig mounts as well as the ones it generates. A *http.ServeMux satisfies it, so a caller that has nothing to interpose passes the mux.

It lives here because the packages that mount rig's own routes — authhttp, oauth, notifyhttp, presencehttp, apidoc — already share this package's error writer, and an interface declared in one of them would be a dependency the others have no other reason for.

Jump to

Keyboard shortcuts

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