rigerr

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package rigerr is the error vocabulary shared by generated code.

Every failure carries a machine-readable code as well as a message. Status codes alone are too coarse — three unrelated failures all return 400 — and a message alone leaves a client parsing prose to decide whether to retry.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func FieldsOf

func FieldsOf(err error) (any, bool)

FieldsOf returns the per-field detail an error carries, if it carries any.

func Is

func Is(err error, code Code) bool

Is reports whether an error carries the given code.

func StatusOf

func StatusOf(err error) int

StatusOf returns the HTTP status for an error.

func Wrap

func Wrap(err error, format string, args ...any) error

Wrap adds context to an error, keeping whatever code it already carried.

An error with no code becomes Internal, which is the honest answer for one that never passed through this package: nobody decided what it means to a client, so it is a server problem until somebody does. An error that does carry a code keeps it — a rule that refused with a Conflict is still a conflict once the layer above has said which rule it was.

Types

type Code

type Code string

Code is the machine-readable reason a request failed. The set is closed: a handler cannot invent a status a client has not been told about.

const (
	CodeBadRequest          Code = "BadRequest"
	CodeUnauthorized        Code = "Unauthorized"
	CodeForbidden           Code = "Forbidden"
	CodeNotFound            Code = "NotFound"
	CodeConflict            Code = "Conflict"
	CodeUnprocessableEntity Code = "UnprocessableEntity"
	CodeRateLimited         Code = "RateLimited"
	// CodeTooLarge reports a request body past the limit the endpoint accepts.
	//
	// It is separate from a bad request because the caller's request was
	// well-formed and the answer is a number: send fewer bytes. A 400 that also
	// means a malformed body cannot say that.
	CodeTooLarge Code = "TooLarge"
	// CodeUnsupportedMediaType reports a body whose content type the endpoint
	// does not accept — a JSON-only endpoint sent a form, or an upload of a type
	// the project does not allow.
	CodeUnsupportedMediaType Code = "UnsupportedMediaType"
	// CodeUpgradeRequired reports that the caller was built against an API
	// revision the server no longer serves.
	//
	// It is its own code rather than a bad request because it is the one failure
	// a client can actually do something about, and "regenerate your client" is
	// not advice anybody can take from a 400 that also means a malformed body.
	CodeUpgradeRequired Code = "UpgradeRequired"
	CodeInternal        Code = "Internal"
)

The codes. Each one maps to a status through Code.HTTPStatus, and adding one here without a case there is what the default arm is for — an unmapped code is a 500 rather than a compile error, so the two are kept in step by reading them together.

func CodeOf

func CodeOf(err error) Code

CodeOf returns the code an error carries, or Internal when it carries none.

An error that never passed through this package is a bug rather than a client mistake, and reporting it as anything other than a server error would be telling the caller to fix something that is not theirs.

Example

The code survives wrapping, which is what makes it safe for a service to add context on the way out. A handler reads the code off whatever it was handed, however many layers added to it.

package main

import (
	"errors"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	err := rigerr.Conflict("that slug is taken")
	wrapped := fmt.Errorf("create the project: %w", err)

	fmt.Println(rigerr.CodeOf(wrapped), rigerr.StatusOf(wrapped))
	fmt.Println(rigerr.Is(wrapped, rigerr.CodeConflict))
	fmt.Println(errors.Is(wrapped, err))

}
Output:
Conflict 409
true
true
Example (Uncoded)

An error that never passed through this package is a server problem until somebody decides otherwise, so it reads as Internal rather than as something the caller could fix.

package main

import (
	"errors"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	fmt.Println(rigerr.CodeOf(errors.New("the connection went away")))
	fmt.Println(rigerr.StatusOf(errors.New("the connection went away")))

}
Output:
Internal
500

func (Code) HTTPStatus

func (c Code) HTTPStatus() int

HTTPStatus maps a code to its status.

type Coder

type Coder interface {
	error
	ErrorCode() Code
}

Coder is an error that names its own code.

It exists for a failure that is more than a message — the generated per-input validation errors implement it — so such an error can be returned as itself rather than flattened into an Error carrying prose.

type Error

type Error struct {
	Code    Code
	Message string
	// Err is the underlying cause, kept for logs and errors.Is. It is never
	// shown to a client: an internal failure's detail is exactly the kind of
	// thing that leaks a table name or a connection string.
	Err error
}

Error is a failure with a code.

func BadRequest

func BadRequest(format string, args ...any) *Error

BadRequest reports a malformed request or an unparseable parameter.

func Conflict

func Conflict(format string, args ...any) *Error

Conflict reports that the request contradicts the current state.

func Forbidden

func Forbidden(format string, args ...any) *Error

Forbidden reports that the caller is known but not permitted.

func Internal

func Internal(err error, format string, args ...any) *Error

Internal reports a server-side failure, keeping the cause for the logs.

Example

Internal keeps the cause for the log and shows the caller only the message. The detail of an internal failure is exactly the kind of thing that leaks a table name or a connection string.

package main

import (
	"errors"
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	cause := errors.New("dial tcp 10.0.0.4:5432: connect: connection refused")
	err := rigerr.Internal(cause, "load the todo list")

	fmt.Println(err)                   // what a log gets
	fmt.Println(err.Code, err.Message) // what a client gets
	fmt.Println(errors.Is(err, cause)) // the cause is still reachable

}
Output:
Internal: load the todo list: dial tcp 10.0.0.4:5432: connect: connection refused
Internal load the todo list
true

func Invalid

func Invalid(format string, args ...any) *Error

Invalid reports a well-formed request that failed validation.

func NotFound

func NotFound(format string, args ...any) *Error

NotFound reports that no such resource exists, or that it belongs to another tenant. The two are deliberately indistinguishable: answering 403 for a row in someone else's tenant confirms the row exists.

Example

A service returns a coded error; the generated handler asks it for a status and a code without knowing which rule produced it.

package main

import (
	"fmt"

	"github.com/simonjanss/rig/runtime/rigerr"
)

func main() {
	err := rigerr.NotFound("no todo with id %q", "t-42")

	fmt.Println(err)
	fmt.Println(rigerr.CodeOf(err), rigerr.StatusOf(err))

}
Output:
NotFound: no todo with id "t-42"
NotFound 404

func RateLimited

func RateLimited(format string, args ...any) *Error

RateLimited reports that the caller should slow down.

func TooLarge

func TooLarge(format string, args ...any) *Error

TooLarge reports a body past the limit the endpoint accepts.

func Unauthorized

func Unauthorized(format string, args ...any) *Error

Unauthorized reports that no valid session or key was presented.

func UnsupportedMediaType

func UnsupportedMediaType(format string, args ...any) *Error

UnsupportedMediaType reports a body whose content type the endpoint refuses.

func UpgradeRequired

func UpgradeRequired(format string, args ...any) *Error

UpgradeRequired reports that the caller is older than the server will serve.

func (*Error) Error

func (e *Error) Error() string

Error is the code, the message, and the cause — everything, because this is for a log. What a client sees is assembled from Error.Code and Error.Message alone.

func (*Error) HTTPStatus

func (e *Error) HTTPStatus() int

HTTPStatus is the status this error should be returned as.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause, so errors.Is and errors.As reach it.

func (*Error) Wrap

func (e *Error) Wrap(err error) *Error

Wrap attaches a cause to an error built by one of the constructors above.

type FieldCode

type FieldCode string

FieldCode says what kind of thing was wrong with one field, for a client that has to decide something rather than display something.

The set is fixed and shared by every project, so a client switches on it once. What varies goes in the message.

const (
	// FieldCodeCannotBeEmpty means the field is required and was left blank.
	FieldCodeCannotBeEmpty FieldCode = "CannotBeEmpty"
	// FieldCodeCannotBeNull means the field was cleared and the column cannot
	// hold null.
	FieldCodeCannotBeNull FieldCode = "CannotBeNull"
	// FieldCodeTooLong means longer than the column allows.
	FieldCodeTooLong FieldCode = "TooLong"
	// FieldCodeTooShort means shorter than the rule allows.
	FieldCodeTooShort FieldCode = "TooShort"
	// FieldCodeOutOfRange means outside the allowed range.
	FieldCodeOutOfRange FieldCode = "OutOfRange"
	// FieldCodeInvalidValue means not one of the values this field accepts.
	FieldCodeInvalidValue FieldCode = "InvalidValue"
	// FieldCodeAlreadyExists means another row has this value and it has to be
	// unique.
	FieldCodeAlreadyExists FieldCode = "AlreadyExists"
	// FieldCodeNotFound means it names something that does not exist.
	FieldCodeNotFound FieldCode = "NotFound"
	// FieldCodeNotAllowed means a legal value, but not one this caller may
	// set, or not from the state the row is in.
	FieldCodeNotAllowed FieldCode = "NotAllowed"
)

The kinds of wrong a field can be.

type FieldError

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

FieldError is one thing wrong with one field.

It does not name the field. It is reached through the member of an input's error struct that stands for that field, so the name is where the value is, and there is no second copy of it to disagree.

It lives here rather than being generated per project because none of it is per project: the same nine codes and the same two members, so one definition serves every model and every client.

func AsFieldError

func AsFieldError(err error) (*FieldError, bool)

AsFieldError returns the field error an error carries, and whether it was one at all.

It is what separates a rule that failed from a rule that could not be run: the first is about the caller's input and belongs in a 422 under the field it names, the second is about the server and belongs nowhere near it.

Named As rather than Is because it hands back the value, the way errors.As does.

func NewFieldError

func NewFieldError(code FieldCode, format string, args ...any) *FieldError

NewFieldError builds one. A rule with nothing better to say than "no" uses FieldCodeInvalidValue.

func (*FieldError) Error

func (e *FieldError) Error() string

Error implements error, so a validation rule can return one directly.

func (*FieldError) ErrorCode

func (e *FieldError) ErrorCode() Code

ErrorCode implements Coder, and says Internal on purpose.

A field error means nothing on its own: it says what was wrong without saying what it was wrong about. Reaching the HTTP layer as the whole answer means one was returned from somewhere that has no field to attach it to — a service method rather than a validation rule — and the client cannot act on "CannotBeEmpty" with nothing to hang it off.

So it is a 500 with the generic message, which is what any other bug is. The alternative, a 422 with an empty body, would blame the caller for a mistake in the service.

type FieldReporter

type FieldReporter interface {
	error
	ErrorFields() any
}

FieldReporter is an error that can say what was wrong with each field.

ErrorFields returns a value shaped like the input it describes: one member per field of the request, each holding the problem with that field or nothing. A client can then attach every message to the control the person is looking at, instead of parsing one sentence for field names.

Jump to

Keyboard shortcuts

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