http

package
v10.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

Documentation

Overview

Package http translates errors into HTTP responses, in both directions.

The forward direction is a Go error to the response a client sees: ToAPIError resolves it to an ErrorCode and a safe message, HTTPStatusForCode turns that code into a status, and ToAPIResponse does both and builds the envelope. The reverse direction is ErrorForCode, which a typed client uses to turn a code it received back into the platform sentinel it stands for — so a caller of a remote service matches ratelimiting.ErrRateLimited with errors.Is exactly as it would inside the serving process. Only the codes that came from exactly one sentinel invert; everything else reports nil, because guessing would hand a client an error to branch on that nobody promised.

Which direction the imports run

This package imports the packages whose sentinels it maps — circuitbreaking, database, idempotency, links, ratelimiting, sessions, and the rest. That is what lets the mapping live in one place instead of being restated at every handler. It also fixes the dependency direction: nothing in those packages may import errors/http back. A package that finds itself wanting an ErrorCode wants a sentinel of its own, mapped here.

Domains outside this module register their own mappers with RegisterHTTPErrorMapper, usually from an init function. The platform mapper is consulted first, registered mappers after, in registration order.

Messages are deliberately uninformative

An unmatched error resolves to the neutral code and "an error occurred" — never to a domain-specific code, so a failure inside a payment path cannot surface as one blaming the database. Matched errors get a fixed message that names no limit, no permission, and no quota: those are useful to an operator and equally useful to someone probing an endpoint for the shape of the policy behind it. What the handler already knows it can render itself, next to an upgrade link or in a log, rather than putting it on the wire.

Unmapped codes resolve to 500. That is the direction to fail in: a code nobody gave a status keeps its server-side failure looking like one.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ErrorForCode

func ErrorForCode(code ErrorCode) error

ErrorForCode returns the platform sentinel an ErrorCode was mapped from, or nil when the code has no single sentinel behind it.

This is the direction a client reads. A service answers 429 with code E116; its typed client parses the envelope and, rather than handing its caller an HTTP status to switch on, hands back ratelimiting.ErrRateLimited — the same sentinel a caller inside the serving process would have gotten, matched the same way with errors.Is. That is what makes a remote call substitutable for a local one at the error boundary.

The returned error is the sentinel itself, unwrapped and unannotated. Callers that want the remote context on it should wrap it with their own.

A nil result means only that this package cannot name the error; it does not mean the response was a success. Fall back to the status code.

func HTTPStatusForCode

func HTTPStatusForCode(code ErrorCode) int

HTTPStatusForCode returns the HTTP status code that corresponds to an ErrorCode. Unmapped codes (including ErrNothingSpecific and any server-side failure such as ErrTalkingToDatabase, ErrTalkingToSearchProvider, ErrSecretGeneration, or ErrEncryptionIssue) resolve to http.StatusInternalServerError.

func RegisterHTTPErrorMapper

func RegisterHTTPErrorMapper(m HTTPErrorMapper)

RegisterHTTPErrorMapper registers a domain-specific error mapper. Domains call this from init() to contribute their error mappings.

Types

type APIError

type APIError struct {
	Message string    `json:"message"`
	Code    ErrorCode `json:"code"`
	// contains filtered or unexported fields
}

APIError represents a response we might send to the User in the event of an error.

func (*APIError) AsError

func (e *APIError) AsError() error

AsError returns the APIError as an error, or nil when the receiver is nil.

It exists because a typed nil *APIError placed in an error interface is non-nil, so returning e.AsError() is how a caller gets a comparison against nil that means what it says.

func (*APIError) Error

func (e *APIError) Error() string

Error returns the error message.

type APIResponse

type APIResponse[T any] struct {
	Data       T                     `json:"data,omitempty"`
	Pagination *filtering.Pagination `json:"pagination,omitempty"`
	Error      *APIError             `json:"error,omitempty"`
	Details    ResponseDetails       `json:"details"`
	// contains filtered or unexported fields
}

APIResponse represents a response we might send to the user.

func NewAPIErrorResponse

func NewAPIErrorResponse(issue string, code ErrorCode, details ResponseDetails) *APIResponse[any]

NewAPIErrorResponse returns a new APIResponse with an error field.

func ToAPIResponse

func ToAPIResponse(err error) (int, *APIResponse[any])

ToAPIResponse maps a handler error to the HTTP status and response envelope that should be sent to the client. It combines ToAPIError (error -> code + safe message) with HTTPStatusForCode (code -> status), so callers get everything needed to write a consistent error response in one call. A nil error resolves to 200 with an empty envelope, though callers typically only invoke this on a non-nil error.

type ErrorCode

type ErrorCode string

ErrorCode is a string code identifying specific error conditions in API responses.

const (
	// ErrNothingSpecific is a catch-all error code for when we just need one.
	ErrNothingSpecific ErrorCode = "E100"
	// ErrFetchingSessionContextData is returned when we fail to fetch session context data.
	ErrFetchingSessionContextData ErrorCode = "E101"
	// ErrDecodingRequestInput is returned when we fail to decode request input.
	ErrDecodingRequestInput ErrorCode = "E102"
	// ErrValidatingRequestInput is returned when the user provides invalid input.
	ErrValidatingRequestInput ErrorCode = "E103"
	// ErrDataNotFound is returned when we fail to find data in the database.
	ErrDataNotFound ErrorCode = "E104"
	// ErrTalkingToDatabase is returned when we fail to interact with a database.
	ErrTalkingToDatabase ErrorCode = "E105"
	// ErrMisbehavingDependency is returned when we fail to interact with a third party.
	ErrMisbehavingDependency ErrorCode = "E106"
	// ErrTalkingToSearchProvider is returned when we fail to interact with the search provider.
	ErrTalkingToSearchProvider ErrorCode = "E107"
	// ErrSecretGeneration is returned when we fail to generate a secret.
	ErrSecretGeneration ErrorCode = "E108"
	// ErrUserIsBanned is returned when a user is banned.
	ErrUserIsBanned ErrorCode = "E109"
	// ErrUserIsNotAuthorized is returned when a user is not authorized.
	ErrUserIsNotAuthorized ErrorCode = "E110"
	// ErrEncryptionIssue is returned when encryption fails in the service.
	ErrEncryptionIssue ErrorCode = "E111"
	// ErrCircuitBroken is returned when a service is circuit broken.
	ErrCircuitBroken ErrorCode = "E112"
	// ErrIdempotencyKeyInFlight is returned when a request repeats an
	// idempotency key whose work is still running. Whether that work will
	// succeed is unknowable, so the repeat is refused rather than run again.
	ErrIdempotencyKeyInFlight ErrorCode = "E113"
	// ErrIdempotencyKeyReused is returned when an idempotency key is presented
	// with a different request than the one it was first used for. Replaying
	// the earlier response would hide a client bug.
	ErrIdempotencyKeyReused ErrorCode = "E114"
	// ErrResourceConflict is returned when a request conflicts with the current
	// state of the resource — deleting something another record still
	// references, for instance. It is the general conflict code:
	// ErrIdempotencyKeyInFlight is also a 409, but it says something specific
	// about idempotency rather than about the resource.
	ErrResourceConflict ErrorCode = "E115"
	// ErrTooManyRequests is returned when a rate limiter refused the request.
	// It says only that the request came too fast, never that a quota is
	// spent: "too much this month" is a different answer with a different
	// remedy, and conflating them tells a client to retry when it should stop.
	ErrTooManyRequests ErrorCode = "E116"
	// ErrInvalidRequestSignature is returned when a request's HMAC signature did
	// not verify, or verified against a timestamp outside the tolerance.
	//
	// One code for both, because the client's remedy is the same — sign it
	// properly, with a current clock — and because a code per failure mode is a
	// forgery oracle. The two are still distinguishable in the message, which is
	// where clock skew, the one benign cause, can be said out loud without
	// saying anything about the key.
	ErrInvalidRequestSignature ErrorCode = "E117"
	// ErrNotEntitled is returned when the account's plan does not include the
	// feature the request needs.
	//
	// It is not ErrUserIsNotAuthorized, and the difference is the remedy. A 403
	// tells a client it is the wrong principal for this action; this tells it the
	// action is not part of what the account bought. The first is answered by an
	// administrator granting a role, the second by somebody entering a card, and
	// a client shown the wrong one asks the wrong person.
	ErrNotEntitled ErrorCode = "E118"
	// ErrQuotaExhausted is returned when the account is entitled to the feature
	// and has consumed all of it for the current billing period.
	//
	// It is deliberately not ErrTooManyRequests, whose own documentation draws
	// the same line from the other side: "too fast" resolves by waiting a moment
	// and "too much this month" does not resolve by waiting at all. Retry-After
	// is meaningful for one of them and a month long for the other.
	ErrQuotaExhausted ErrorCode = "E119"
	// ErrActionLinkUnusable is returned when a magic-login, verification,
	// reset, or unsubscribe link cannot be redeemed: it was already used, it
	// expired, it was revoked, or there is no such link.
	//
	// One code for all four, because a client has the same thing to do with
	// every one of them — ask for a new link — and because the four are not a
	// branch a client should be writing. The distinction that matters is the
	// one a person reads, and that is in the message, which can be specific
	// here without disclosing anything: a link token is 256 random bits, so
	// nobody is ever holding one they were not sent, and there is no guess for
	// the difference between "expired" and "no such link" to be an oracle for.
	ErrActionLinkUnusable ErrorCode = "E120"
	// ErrInvalidSearchCursor is returned when a pagination cursor was not issued
	// by the index being asked to resume it — it did not decode, or it came from
	// a different backend.
	//
	// It is not ErrValidatingRequestInput, though it is also a 400, because the
	// remedy is different and a client can act on it without a person reading
	// the message. Bad input means the request was wrong and needs correcting;
	// this means the request was fine and the *position* is unusable, and the
	// answer is to drop the cursor and start the pagination again. A client that
	// cannot tell the two apart either retries a cursor that will never work or
	// abandons a query that would have.
	ErrInvalidSearchCursor ErrorCode = "E121"
	// ErrSearchWindowExceeded is returned when pagination reached the depth the
	// index will serve. The cursor was valid; the page it names is past the end
	// of what can be paged to.
	//
	// Distinct from ErrInvalidSearchCursor for the same reason that one is
	// distinct from ErrValidatingRequestInput: same status, different remedy.
	// Restarting pagination does not help here — every page up to the ceiling is
	// still fine, and the only thing that reaches the rest of the result set is a
	// narrower query. It is emphatically not a 200 with an empty page, which
	// would tell the client it had seen everything.
	ErrSearchWindowExceeded ErrorCode = "E122"
)

func ToAPIError

func ToAPIError(err error) (code ErrorCode, msg string)

ToAPIError maps known sentinel errors to ErrorCode and a safe user-facing message. It tries PlatformMapper first, then each registered domain mapper. Returns (code, message). Unknown errors fall back to the neutral ErrNothingSpecific and "an error occurred" — never a domain-specific code (e.g. a payment panic must not report "database").

type HTTPErrorMapper

type HTTPErrorMapper interface {
	Map(err error) (code ErrorCode, msg string, ok bool)
}

HTTPErrorMapper maps domain errors to (ErrorCode, message). ok=false means no match.

var PlatformMapper HTTPErrorMapper = platformMapper{}

PlatformMapper maps platform-level errors to HTTP error codes and messages. It does not depend on any domain.

type ResponseDetails

type ResponseDetails struct {
	CurrentAccountID string `json:"currentAccountID"`
	TraceID          string `json:"traceID"`
	// contains filtered or unexported fields
}

ResponseDetails represents details about the response.

Jump to

Keyboard shortcuts

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