apierr

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package apierr is PayCLI's error vocabulary (§11): a closed set of stable machine-readable codes, a total code-to-exit-status map, and the normaliser that folds Payload's six different error body shapes into one.

Nothing in this package reads a human-readable server message to decide a code. Payload runs followingFieldsInvalid, noFilesUploaded, notAllowedToPerformAction and deletedCountSuccessfully through req.t, so their text changes with the project's i18n config, a payload-lng cookie or a proxy-injected Accept-Language. Only errors[].name, the JSON structure and the HTTP status are trusted. The two strings Payload hardcodes in English — `Route not found "…"` and `Cannot <METHOD> …` — are the sole exceptions and are named explicitly at each use.

Index

Constants

View Source
const (
	IDTypeNumber  = "number"
	IDTypeString  = "string"
	IDTypeUnknown = "unknown"
)

ID types a collection can have (§7.6). "unknown" is a real, common value and it disables every local check that depends on the id type: a client-side rejection built on a fact PayCLI never learned is a fabricated error.

View Source
const (
	CauseHigh   = "high"
	CauseMedium = "medium"
	CauseLow    = "low"
)

Cause confidence levels.

View Source
const (
	ExitOK                   = 0
	ExitInternal             = 1
	ExitAuth                 = 2
	ExitThrottled            = 3
	ExitNotFound             = 4
	ExitValidation           = 5
	ExitNetwork              = 6
	ExitPartial              = 7
	ExitAccessDenied         = 8
	ExitConfig               = 9
	ExitCapability           = 10
	ExitConfirmationRequired = 11
)

Process exit statuses (§11.4). 125–128 and 130 are shell/signal territory and are never used.

View Source
const (
	AuthModeAPIKey    = "api-key"
	AuthModeJWT       = "jwt"
	AuthModeAnonymous = "anonymous"
)

Auth modes (§5.0). Duplicated as plain strings rather than imported so that apierr stays free of dependencies on config/secret.

View Source
const DefaultDocs = "pay explain --section exit_codes"

DefaultDocs is error.docs on every error PayCLI produces.

MaxExit is the largest status PayCLI ever returns.

Variables

This section is empty.

Functions

func AnySent

func AnySent(fields []Field) bool

AnySent reports whether any field was part of the request body PayCLI sent.

func CodesByExit

func CodesByExit() map[int][]Code

CodesByExit groups the vocabulary by exit status, for help and explain output.

func DefaultHint

func DefaultHint(code Code) string

DefaultHint returns the starting hint for a code.

func DidYouMean

func DidYouMean(input string, candidates []string) []string

DidYouMean ranks candidates by closeness to input for error.did_you_mean. A prefix or substring match always beats a pure edit-distance match, because "page" -> "pages" is the mistake agents actually make.

func ExitCode

func ExitCode(err error) int

ExitCode extracts the process exit status from any error: 0 for nil, the mapped status for an *Error anywhere in the chain, and ExitInternal for an error that never passed through this package.

func HasCode

func HasCode(err error, code Code) bool

HasCode reports whether err carries the given code.

func HumanLine

func HumanLine(e *Error) string

HumanLine renders the one-line stderr summary (§11.1):

pay: validation_failed (exit 5): 3 fields are invalid on "pages" — hint: …

Types

type Cause

type Cause struct {
	Cause      string `json:"cause"`
	Confidence string `json:"confidence"`
	Detail     string `json:"detail,omitempty"`
	Fix        string `json:"fix,omitempty"`
}

Cause is one entry of error.likely_causes, used to diagnose opaque 500s (§11.3). Confidence is "high" | "medium" | "low".

func Causes500

func Causes500(method, path string, rels []Relationship) []Cause

Causes500 ranks the likely causes of an opaque 500 (§11.3). The first two rows of that table are rejected client-side before the call, so what remains is the relationship diagnosis plus the standing advice that the real message is one config flag away.

type Code

type Code string

Code is a stable, machine-readable error identifier. Agents branch on it; it never changes for a given failure mode.

const (
	CodeInternal                 Code = "internal"
	CodeUnknown                  Code = "unknown"
	CodeCacheCorrupt             Code = "cache_corrupt"
	CodeUpdateVerificationFailed Code = "update_verification_failed"
	CodeAuditWriteFailed         Code = "audit_write_failed"
)

Exit 1 — generic / internal.

const (
	CodeAuthMissing             Code = "auth_missing"
	CodeAuthInvalid             Code = "auth_invalid"
	CodeAuthRequired            Code = "auth_required"
	CodeAuthLocked              Code = "auth_locked"
	CodeAuthUnverifiedEmail     Code = "auth_unverified_email"
	CodeAuthInsecurePermissions Code = "auth_insecure_permissions"
	CodeAuthHelperFailed        Code = "auth_helper_failed"
)

Exit 2 — auth: *your credentials* are the problem.

const (
	CodeRateLimited Code = "rate_limited"
	CodeDocLocked   Code = "doc_locked"
	CodeServerBusy  Code = "server_busy"
)

Exit 3 — throttled / temporarily unavailable. Auto-retried.

const (
	CodeDocNotFound     Code = "doc_not_found"
	CodeRouteNotFound   Code = "route_not_found"
	CodeVersionNotFound Code = "version_not_found"
)

Exit 4 — not found.

const (
	CodeValidationFailed    Code = "validation_failed"
	CodeQueryPathInvalid    Code = "query_path_invalid"
	CodeInvalidArgs         Code = "invalid_args"
	CodeInvalidWhereSyntax  Code = "invalid_where_syntax"
	CodeInvalidSortField    Code = "invalid_sort_field"
	CodeUnknownField        Code = "unknown_field"
	CodeInvalidOption       Code = "invalid_option"
	CodeInvalidID           Code = "invalid_id"
	CodeBadRequestBody      Code = "bad_request_body"
	CodeWhereRequired       Code = "where_required"
	CodeFileMissing         Code = "file_missing"
	CodeNotUploadCollection Code = "not_upload_collection"
	CodeUnsupportedOperator Code = "unsupported_operator"
	CodeBulkLimitExceeded   Code = "bulk_limit_exceeded"
	CodeRequestTooLarge     Code = "request_too_large"
	CodeFormatUnsupported   Code = "format_unsupported"
	CodeInvalidPathExpr     Code = "invalid_path_expr"
)

Exit 5 — validation / bad input.

const (
	CodeNetworkUnreachable Code = "network_unreachable"
	CodeDNSFailure         Code = "dns_failure"
	CodeTLSError           Code = "tls_error"
	CodeTimeout            Code = "timeout"
	CodeServerError        Code = "server_error"
	CodeServerUnavailable  Code = "server_unavailable"
	CodeNonJSONResponse    Code = "non_json_response"
)

Exit 6 — network / server.

const (
	CodeAccessDenied      Code = "access_denied"
	CodeAdminAccessDenied Code = "admin_access_denied"
)

Exit 8 — access denied: identity valid, permission not.

const (
	CodeConfigMissing           Code = "config_missing"
	CodeConfigSecretInPlaintext Code = "config_secret_in_plaintext"
	CodeProfileUnknown          Code = "profile_unknown"
	CodeBaseURLInvalid          Code = "base_url_invalid"
	CodeEndpointNotPayload      Code = "endpoint_not_payload"
	CodeAuthCollectionUnknown   Code = "auth_collection_unknown"
)

Exit 9 — config / profile.

const (
	CodeCollectionUnknown    Code = "collection_unknown"
	CodeGlobalUnknown        Code = "global_unknown"
	CodeFeatureUnavailable   Code = "feature_unavailable"
	CodeOperationUnsupported Code = "operation_unsupported"
	CodeDiscoveryFailed      Code = "discovery_failed"
	CodeGraphQLDisabled      Code = "graphql_disabled"
	CodeSchemaStale          Code = "schema_stale"
)

Exit 10 — capability / discovery.

const (
	CodeConfirmationRequired Code = "confirmation_required"
)

Exit 11 — confirmation required.

const (
	CodePartialFailure Code = "partial_failure"
)

Exit 7 — partial failure. NEVER auto-retried: some documents committed.

func CodeOf

func CodeOf(err error) Code

CodeOf returns the code of the first *Error in err's chain, or CodeInternal.

func Codes

func Codes() []Code

Codes returns every declared code, sorted, for `pay explain --section exit_codes` and for tests.

func (Code) Exit

func (c Code) Exit() int

Exit returns the process exit status for a code. An unregistered code is a programming error rather than a runtime condition; it maps to ExitInternal and the totality test is what prevents it from ever happening.

func (Code) Known

func (c Code) Known() bool

Known reports whether the code is part of the declared vocabulary.

func (Code) Retriable

func (c Code) Retriable() bool

Retriable reports whether PayCLI may transparently retry a request that failed with this code. Writes are never retried regardless (§6.1); this answers only "is the failure itself transient".

func (Code) String

func (c Code) String() string

type Confidence

type Confidence string

Confidence says how the code was decided (§11.1). It is always present.

certain  — the classification rests on errors[].name, JSON structure or an
           HTTP status: signals the project's i18n config cannot change.
probable — it rests on a structural heuristic and the confirming English
           string was absent. §11.5's file_missing row is the only one.
const (
	ConfidenceCertain  Confidence = "certain"
	ConfidenceProbable Confidence = "probable"
)

type Error

type Error struct {
	Code         Code            `json:"code"`
	Exit         int             `json:"exit"`
	Message      string          `json:"message"`
	Hint         string          `json:"hint,omitempty"`
	Retriable    bool            `json:"retriable"`
	Confidence   Confidence      `json:"confidence"`
	Fields       []Field         `json:"fields"`
	Failures     []Failure       `json:"failures,omitempty"`
	LikelyCauses []Cause         `json:"likely_causes,omitempty"`
	DidYouMean   []string        `json:"did_you_mean"`
	Docs         string          `json:"docs,omitempty"`
	HTTP         *HTTP           `json:"http,omitempty"`
	Raw          json.RawMessage `json:"raw,omitempty"`

	// RawRedactedPaths is non-empty when redaction modified Raw. The CLI turns
	// it into the warnings[] entry {"code":"raw_redacted","paths":[…]} so the
	// agent knows the bytes differ from the wire (§11.1).
	RawRedactedPaths []string `json:"-"`
	// contains filtered or unexported fields
}

Error is the error half of the envelope (§11.1). Field order is the JSON key order: §11.1's listing, with failures and likely_causes inserted after fields (§12.5 and §11.3 add them to the same object and the two listings disagree on their position; fields-then-failures keeps all the per-item detail together).

func As

func As(err error) (*Error, bool)

As finds the first *Error in err's chain.

func CheckID

func CheckID(collection, id, idType string) *Error

CheckID pre-empts §11.3's first opaque-500 cause: an id that does not parse as the collection's id type produces HTTP 500 "Something went wrong.", and on /duplicate it can produce an unintended write. It returns nil — meaning "send the request" — whenever idType is unknown or empty, per §9.3's tri-state rule.

func CheckVersions

func CheckVersions(collection string, hasVersions *bool) *Error

CheckVersions pre-empts §11.3's second opaque-500 cause: /versions on a collection without versions enabled. hasVersions is a tri-state — nil means "not discovered", and PayCLI then sends the request rather than guessing.

func Classify

func Classify(r Response) *Error

Classify folds any Payload error response into one *Error (§11.2, §11.5). It never panics: a project author can make formatErrors return literally anything, and afterError hooks can rewrite both body and status.

func From

func From(err error) *Error

From coerces any error into an *Error so the envelope always has one. A nil error yields nil.

func Network

func Network(err error) *Error

Network classifies a transport-level failure (§11.4 exit 6). It is here rather than in internal/payload so the classification is testable without a socket and so every caller agrees on the code.

func New

func New(code Code, format string, args ...any) *Error

New builds an error from a code and a message. The exit status, retriability and the default hint all follow from the code, so no caller can invent a mapping. The message is scrubbed: server messages routinely embed URLs, and a URL can carry a credential.

func Wrap

func Wrap(err error, code Code, format string, args ...any) *Error

Wrap attaches a code and message to an existing error, preserving the chain for errors.Is / errors.As.

func (*Error) Error

func (e *Error) Error() string

Error renders the one-line human form used on stderr (§11.1).

func (*Error) Is

func (e *Error) Is(target error) bool

Is lets errors.Is(err, apierr.New(code, "")) match on the code alone.

func (*Error) Recode

func (e *Error) Recode(code Code) *Error

Recode changes the code, keeping everything else, and re-derives the exit status and retriability. It is how a layer with more context sharpens a classification — the normaliser cannot tell route_not_found from collection_unknown without the manifest, for example.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the wrapped cause.

func (*Error) WithCauses

func (e *Error) WithCauses(causes ...Cause) *Error

WithCauses sets error.likely_causes (§11.3).

func (*Error) WithConfidence

func (e *Error) WithConfidence(c Confidence) *Error

WithConfidence overrides the default ConfidenceCertain.

func (*Error) WithDidYouMean

func (e *Error) WithDidYouMean(suggestions ...string) *Error

WithDidYouMean sets error.did_you_mean.

func (*Error) WithFailures

func (e *Error) WithFailures(failures ...Failure) *Error

WithFailures sets error.failures for a partial_failure (§12.5).

func (*Error) WithFields

func (e *Error) WithFields(fields ...Field) *Error

WithFields sets error.fields, sorted sent-first (§11.1).

func (*Error) WithHTTP

func (e *Error) WithHTTP(h *HTTP) *Error

WithHTTP records the wire context, redacting the URL (§5.3 makes redaction of error.http.url mandatory).

func (*Error) WithHint

func (e *Error) WithHint(format string, args ...any) *Error

WithHint replaces the default hint. Every "you cannot X" must be followed by "do Y instead", so a hint is effectively mandatory.

func (*Error) WithRaw

func (e *Error) WithRaw(body []byte, noRedact bool) *Error

WithRaw attaches the server's body after redaction. When redaction changed the bytes, RawRedactedPaths names what moved so the caller can emit the raw_redacted warning. noRedact reproduces the true bytes for --no-redact.

func (*Error) WithWrapped

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

WithWrapped attaches an underlying Go error after construction.

type Failure

type Failure struct {
	ID      any     `json:"id"`
	Code    Code    `json:"code"`
	Message string  `json:"message"`
	Fields  []Field `json:"fields"`
}

Failure is one entry of error.failures on a partial_failure (§12.5).

type Field

type Field struct {
	Path    string `json:"path"`
	Label   string `json:"label,omitempty"`
	Message string `json:"message"`
	// Sent is true iff Path is a leaf of the request body PayCLI actually sent.
	// It exists because Payload validates the WHOLE document on PATCH and
	// therefore reports fields the caller never touched.
	Sent bool `json:"sent"`
}

Field is one entry of error.fields.

func SortFields

func SortFields(fields []Field) []Field

SortFields orders entries sent-first while preserving the server's order inside each group (§11.1).

type HTTP

type HTTP struct {
	Status int    `json:"status"`
	Method string `json:"method"`
	URL    string `json:"url"`
	// PayloadErrorName is errors[0].name verbatim — the signal the normaliser
	// classified on, exposed so an agent can audit the decision.
	PayloadErrorName string `json:"payload_error_name,omitempty"`
	Attempts         int    `json:"attempts"`
	RetryAfter       string `json:"retry_after,omitempty"`
	// BodyExcerpt is at most excerptLimit characters of a non-JSON body.
	BodyExcerpt string `json:"body_excerpt,omitempty"`
}

HTTP is error.http: what was on the wire when the failure happened.

type Relationship

type Relationship struct {
	Path       string // request-body leaf path, e.g. "heroImage"
	Collection string // the relationTo target slug
	ID         any    // the id that was sent
}

Relationship describes one relationship value in a request body, used to diagnose §11.3's third cause after the fact.

type Response

type Response struct {
	Status      int
	Method      string
	URL         string
	ContentType string
	Body        []byte
	Attempts    int
	RetryAfter  string

	// AuthMode and IdentityVerified drive the 403 split. The server cannot make
	// this call for us: a 403 body is byte-identical whether the caller is
	// unauthenticated or merely unpermitted (verified), and its message is
	// translated on top of that.
	AuthMode         string
	IdentityVerified bool

	// UploadCollection is the target collection's flags.upload. A 400 against an
	// upload collection with no errors[0].name is file_missing — a structural,
	// language-independent signal (the English "No files were uploaded." is
	// only a confirming signal that raises confidence).
	UploadCollection bool

	// Collection is the target slug, used in messages only.
	Collection string

	// SentPaths is the leaf-path set of the request body PayCLI actually sent,
	// which decides Field.Sent and therefore which validation hint is used.
	SentPaths map[string]bool

	// IncludeRaw is false for --no-raw; NoRedact is true for --no-redact.
	IncludeRaw bool
	NoRedact   bool
}

Response is everything the normaliser is allowed to look at. It is a plain struct rather than an *http.Response so classification is a pure function and every row of §11.2 and §11.5 is a table test.

Jump to

Keyboard shortcuts

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