apierror

package
v1.4.5 Latest Latest
Warning

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

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package apierror defines the structured error types used across all services.

Every user-facing error is represented as an APIError, which carries both a public message (safe to return in HTTP responses) and an internal message (for logs and traces only). Errors are categorized by an ErrorCode (machine-readable, e.g. "validation_failed") and an ErrorType (broad category, e.g. "invalid_request_error").

The package provides constructor functions for common error scenarios (validation, auth, not-found, etc.) and handles mapping error codes to HTTP status codes via GetHTTPStatusCode. For gRPC transport, errors can be serialized to/from JSON using ToJSON/APIErrorFromJSON.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetHTTPStatusCode

func GetHTTPStatusCode(code ErrorCode) int

GetHTTPStatusCode maps an ErrorCode to the appropriate HTTP status code. Used by the API gateway to set the response status when writing error responses.

func Is5XXErrorCode

func Is5XXErrorCode(code ErrorCode) bool

Is5XXErrorCode returns true if the error code maps to a 5xx HTTP status.

func IsNotFound

func IsNotFound(err *APIError) bool

IsNotFound is a convenience check for whether an APIError is a resource_not_found error.

func IsTransientError

func IsTransientError(code ErrorCode, errorType ErrorType) bool

IsTransientError returns true if the error code and type combination represents a temporary condition that may resolve on retry. The error type provides context that the code alone cannot — for example, a resource_conflict with ErrorTypeInvalidRequest (e.g. "username already taken") is not retryable, while server-side errors generally are.

Transient combinations:

  • ErrorTypeAPI: all server errors except client_closed_request
  • ErrorTypeIdempotency: only idempotency_in_progress (concurrent duplicate request)
  • ErrorTypeInvalidRequest: only rate_limit_exceeded (back off and retry)

Types

type APIError

type APIError struct {
	// Code is the machine-readable error code included in API responses.
	Code ErrorCode `json:"code"`
	// Type is the broad error category included in API responses.
	Type ErrorType `json:"type"`
	// PublicMessage is the human-readable message returned to the client.
	PublicMessage string `json:"message"`
	// Param is the request parameter that caused the error (e.g. "email"), if applicable.
	Param string `json:"param,omitempty"`
	// DocURL links to documentation about this specific error, if available.
	DocURL string `json:"doc_url,omitempty"`
	// IsTransient indicates whether the client should retry the request. Automatically set by NewAPIError based on the error code and type.
	IsTransient bool `json:"is_transient"`
	// Quota provides machine-readable details about a plan-imposed resource limit. Only populated for limit_exceeded errors. Included in API responses when non-nil.
	Quota *QuotaInfo `json:"quota,omitempty"`
	// InternalMessage is a developer-facing message for logs and traces. Never sent to clients.
	InternalMessage string `json:"-"`
	// Internal is the underlying error, if any. Never sent to clients. Accessible via Unwrap().
	Internal error `json:"-"`
	// Stack is the goroutine stack captured at the point a 5xx error is created, so the recorded trace points at the failing code rather than the response-writing layer. Empty for non-5xx errors. Never sent to clients. Propagated across gRPC via ToJSON.
	Stack string `json:"-"`
}

APIError is the canonical error type used throughout the platform. It implements the error interface and separates public-facing information (Code, Type, PublicMessage) from internal diagnostics (InternalMessage, Internal) that are never exposed to clients.

Create instances using the constructor functions (NewValidationError, NewInternalError, etc.) rather than building APIError literals directly.

func APIErrorFromJSON

func APIErrorFromJSON(jsonData []byte) (*APIError, error)

APIErrorFromJSON reconstructs an APIError from JSON bytes produced by ToJSON. Used on the receiving side of gRPC calls to restore the full error with internal details.

func NewAPIError

func NewAPIError(code ErrorCode, errorType ErrorType, publicMessage string, internalMessage string, opts ...APIErrorOption) *APIError

NewAPIError is the base constructor for all API errors. It sets IsTransient automatically based on the error code. Prefer the domain-specific constructors (NewValidationError, NewInternalError, etc.) for common cases; use this directly only when none of those fit.

func NewAPIVersionInvalidError

func NewAPIVersionInvalidError(version string, supported []string) *APIError

NewAPIVersionInvalidError creates a 400 error when the requested API version is not recognized.

func NewAPIVersionRequiredError

func NewAPIVersionRequiredError() *APIError

NewAPIVersionRequiredError creates a 400 error when the OpenMRP-Version header is missing.

func NewAPIVersionTooOldError

func NewAPIVersionTooOldError(requested, minimum string) *APIError

NewAPIVersionTooOldError creates a 400 error when the requested API version is below the minimum version required by the endpoint.

func NewAgentSpendingCapReachedError

func NewAgentSpendingCapReachedError(publicMessage string) *APIError

NewAgentSpendingCapReachedError creates a 402 Payment Required error when an agent run is refused because the account has reached its monthly agent spending cap. Clients should prompt the user to raise or remove the cap.

func NewAlreadyDeletedError

func NewAlreadyDeletedError(publicMessage string) *APIError

NewAlreadyDeletedError creates a 410 Gone error for resources that were already deleted.

func NewAuthenticationError

func NewAuthenticationError(publicMessage string) *APIError

NewAuthenticationError creates a 401 Unauthorized error for invalid credentials.

func NewAuthorizationError

func NewAuthorizationError(publicMessage string) *APIError

NewAuthorizationError creates a 403 Forbidden error when the caller lacks required permissions.

func NewClientClosedRequestError

func NewClientClosedRequestError(publicMessage string) *APIError

NewClientClosedRequestError creates a 499 error (nginx convention) when the client disconnects before the server finishes processing.

func NewConflictErrorWithParam

func NewConflictErrorWithParam(publicMessage string, param string) *APIError

NewConflictErrorWithParam creates a 409 Conflict error tied to a specific parameter (e.g. a duplicate email address).

func NewExpiredAPIKeyError

func NewExpiredAPIKeyError(publicMessage string) *APIError

NewExpiredAPIKeyError creates a 401 error when an API key has passed its expiration date.

func NewExpiredTokenError

func NewExpiredTokenError(publicMessage string) *APIError

NewExpiredTokenError creates a 401 error when a JWT access token or refresh token has passed its expiration date. Distinct from NewAuthenticationError so that routine token-rotation traffic (a short-lived access token expiring, the client silently refreshing) can be told apart from genuine bad credentials.

func NewIdempotencyHashMismatchError

func NewIdempotencyHashMismatchError(idempotencyKey string) *APIError

NewIdempotencyHashMismatchError creates a validation error when an idempotency key is reused with different request parameters than the original request.

func NewIdempotencyInProgressError

func NewIdempotencyInProgressError(idempotencyKey string) *APIError

NewIdempotencyInProgressError creates a 409 Conflict error when a request with the same idempotency key is already being processed concurrently.

func NewInternalError

func NewInternalError(internal error, internalMessage string) *APIError

NewInternalError creates a 500 Internal Server Error with a generic public message ("Something went wrong.") and wraps the underlying error for internal logging. If the underlying error is itself an APIError, their internal messages are chained.

func NewInvalidFormatError

func NewInvalidFormatError(publicMessage string, param string) *APIError

NewInvalidFormatError creates a 400 error when a field value does not match the expected format.

func NewInvariantViolationError

func NewInvariantViolationError(internalMessage string) *APIError

NewInvariantViolationError creates a 500 error for conditions that should never occur in correct code (e.g. missing required data after a successful query). The public message is generic; the internal message captures what invariant was violated.

func NewLimitExceededError

func NewLimitExceededError(publicMessage string) *APIError

NewLimitExceededError creates a 403 Forbidden error when an account has reached a plan-imposed resource limit (e.g. maximum sandbox accounts).

func NewMethodNotAllowedError

func NewMethodNotAllowedError(publicMessage string) *APIError

NewMethodNotAllowedError creates a 405 Method Not Allowed error.

func NewMissingFieldError

func NewMissingFieldError(publicMessage string, param string) *APIError

NewMissingFieldError creates a 400 error when a required field is not provided in the request body.

func NewParameterInvalidError

func NewParameterInvalidError(publicMessage string, param string) *APIError

NewParameterInvalidError creates a 400 error when a query or path parameter value is not valid.

func NewParameterMissingError

func NewParameterMissingError(publicMessage string, param string) *APIError

NewParameterMissingError creates a 400 error when a required query or path parameter is not provided.

func NewParameterUnknownError

func NewParameterUnknownError(publicMessage string, param string) *APIError

NewParameterUnknownError creates a 400 error when an unrecognized parameter is sent in the request.

func NewPaymentRequiredError

func NewPaymentRequiredError(publicMessage string) *APIError

NewPaymentRequiredError creates a 402 Payment Required error when the account's subscription is in a non-active state and must be resolved before continuing.

func NewRateLimitExceededError

func NewRateLimitExceededError(publicMessage string) *APIError

NewRateLimitExceededError creates a 429 Too Many Requests error. Marked as transient.

func NewRegistrationClosedError

func NewRegistrationClosedError(publicMessage string) *APIError

NewRegistrationClosedError creates a 403 Forbidden error when public registration for a plan code has reached its capacity.

func NewRequestTimeoutError

func NewRequestTimeoutError(internalMessage string) *APIError

NewRequestTimeoutError creates a 504 Gateway Timeout error. The deadline was exceeded on our side, not the client's, so this is a server failure rather than a 408. Marked as transient.

func NewResourceConflictError

func NewResourceConflictError(publicMessage string) *APIError

NewResourceConflictError creates a 409 Conflict error for state conflicts (e.g. concurrent updates).

func NewResourceExistsError

func NewResourceExistsError(publicMessage string) *APIError

NewResourceExistsError creates a 409 Conflict error for duplicate resource creation (e.g. unique constraint violation). Uses ErrorCodeResourceExists rather than ErrorCodeResourceConflict because the semantics are "this resource already exists" rather than a generic state conflict.

func NewResourceNotFoundError

func NewResourceNotFoundError(publicMessage string) *APIError

NewResourceNotFoundError creates a 404 Not Found error for missing resources.

func NewRevokedAPIKeyError

func NewRevokedAPIKeyError(publicMessage string) *APIError

NewRevokedAPIKeyError creates a 401 error when an API key has been explicitly revoked.

func NewValidationError

func NewValidationError(publicMessage string) *APIError

NewValidationError creates a 400 Bad Request error for general input validation failures.

func NewValidationErrorWithParam

func NewValidationErrorWithParam(publicMessage string, param string) *APIError

NewValidationErrorWithParam creates a 400 Bad Request error tied to a specific request parameter.

func (*APIError) Error

func (e *APIError) Error() string

Error returns the internal (non-public) error string for logging. If an underlying error is wrapped, it is appended to the internal message.

func (*APIError) ToJSON

func (e *APIError) ToJSON() ([]byte, error)

ToJSON serializes the full APIError (including internal fields) to JSON for transport over gRPC. Use APIErrorFromJSON on the receiving side to reconstruct it.

func (*APIError) ToResponseError

func (e *APIError) ToResponseError() ResponseError

renders the public fields as the canonical client-facing error object, so one embedded in another payload (e.g. a job's per-row failures) reads the same as any error envelope

func (*APIError) ToResponseMap

func (e *APIError) ToResponseMap() any

ToResponseMap converts the APIError into an APIErrorResponse suitable for JSON serialization in HTTP responses. Only public fields are included.

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

Unwrap returns the underlying error for use with errors.Is/errors.As.

func (*APIError) WithDocURL

func (e *APIError) WithDocURL(url string) *APIError

WithDocURL sets the documentation URL on the error and returns the same pointer for chaining.

func (*APIError) WithInternal

func (e *APIError) WithInternal(err error) *APIError

WithInternal wraps an underlying error and returns the same pointer for chaining.

func (*APIError) WithParam

func (e *APIError) WithParam(param string) *APIError

WithParam sets the parameter name on the error and returns the same pointer for chaining.

func (*APIError) WithQuota

func (e *APIError) WithQuota(limit, used int32, resetAt *time.Time) *APIError

WithQuota attaches quota details to the error and returns the same pointer for chaining. Intended for limit_exceeded errors so clients receive the plan limit, current usage, and optional reset time in the response envelope.

type APIErrorOption

type APIErrorOption func(*APIError)

APIErrorOption is a functional option applied during NewAPIError construction. Use the package-level WithParam, WithDocURL, and WithInternal functions to create options.

func WithDocURL

func WithDocURL(url string) APIErrorOption

WithDocURL returns an option that sets the documentation URL on the error.

func WithInternal

func WithInternal(err error) APIErrorOption

WithInternal returns an option that wraps an underlying error for logging/tracing.

func WithParam

func WithParam(param string) APIErrorOption

WithParam returns an option that sets the offending parameter name on the error.

type APIErrorResponse

type APIErrorResponse struct {
	// The error object containing details about what went wrong.
	Error ResponseError `json:"error"`
}

APIErrorResponse is the top-level envelope wrapping a ResponseError. All API error responses use this shape: { "error": { ... } }.

func (APIErrorResponse) SchemaExample

func (r APIErrorResponse) SchemaExample() any

SchemaExample returns a representative instance for OpenAPI documentation generation.

type ErrorCode

type ErrorCode string

ErrorCode is a machine-readable identifier for a specific error condition. These codes are returned in API responses and used by clients to programmatically handle errors without parsing human-readable messages.

const (

	// ErrorCodeExpiredToken indicates a JWT access or refresh token has expired.
	ErrorCodeExpiredToken ErrorCode = "expired_token"
	// ErrorCodeExpiredAPIKey indicates an API key has passed its expiration date.
	ErrorCodeExpiredAPIKey ErrorCode = "api_key_expired" // #nosec G101 - This is an error code constant, not a hardcoded credential
	// ErrorCodeRevokedAPIKey indicates an API key was explicitly revoked by the owner or OpenMRP.
	ErrorCodeRevokedAPIKey ErrorCode = "api_key_revoked" // #nosec G101 - This is an error code constant, not a hardcoded credential
	// ErrorCodeInvalidCredentials indicates the provided credentials are wrong.
	ErrorCodeInvalidCredentials ErrorCode = "invalid_credentials" // #nosec G101 - This is an error code constant, not a hardcoded credential
	// ErrorCodeInsufficientPerms indicates the caller is authenticated but lacks the required role or permission.
	ErrorCodeInsufficientPerms ErrorCode = "insufficient_permissions"
	// ErrorCodePaymentRequired indicates the account's subscription is in a non-active state (past_due, canceled, unpaid) and must be resolved before the account can continue using the platform.
	ErrorCodePaymentRequired ErrorCode = "payment_required"

	// ErrorCodeAgentSpendingCapReached indicates the account has reached its configured monthly agent (LLM) spending cap, so the run was refused. The caller must raise or remove the cap to continue running agents.
	ErrorCodeAgentSpendingCapReached ErrorCode = "agent_spending_cap_reached"

	// ErrorCodeValidationFailed is a general validation failure indicating that the request is invalid.
	ErrorCodeValidationFailed ErrorCode = "validation_failed"
	// ErrorCodeMissingField indicates a required field was not provided in the request body.
	ErrorCodeMissingField ErrorCode = "missing_field"
	// ErrorCodeInvalidFormat indicates a field value does not match the expected format (e.g. invalid email).
	ErrorCodeInvalidFormat ErrorCode = "invalid_format"
	// ErrorCodeMethodNotAllowed indicates the HTTP method is not supported for this endpoint.
	ErrorCodeMethodNotAllowed ErrorCode = "method_not_allowed"

	// ErrorCodeResourceNotFound indicates the requested resource does not exist.
	ErrorCodeResourceNotFound ErrorCode = "resource_not_found"
	// ErrorCodeResourceExists indicates a resource with the same unique constraint already exists.
	ErrorCodeResourceExists ErrorCode = "resource_exists"
	// ErrorCodeResourceConflict indicates a state conflict that prevents the operation (e.g. duplicate username, optimistic locking failure).
	ErrorCodeResourceConflict ErrorCode = "resource_conflict"
	// ErrorCodeResourceGone indicates the resource existed but has been permanently deleted.
	ErrorCodeResourceGone ErrorCode = "resource_gone"

	// ErrorCodeIdempotencyInProgress indicates a request with the same idempotency key is currently being processed. The client should retry after a short delay.
	ErrorCodeIdempotencyInProgress ErrorCode = "idempotency_in_progress"

	// ErrorCodeLimitExceeded indicates the account has reached a plan-imposed resource limit (e.g. maximum sandbox accounts). The caller must upgrade their plan to increase the limit.
	ErrorCodeLimitExceeded ErrorCode = "limit_exceeded"
	// ErrorCodeRegistrationClosed indicates that public registration for the requested plan code has reached its capacity. Distinct from limit_exceeded, which applies to per-account resource quotas (e.g. sandbox count).
	ErrorCodeRegistrationClosed ErrorCode = "registration_closed"

	// ErrorCodeRateLimitExceeded indicates the caller has exceeded the allowed request rate. The client should back off and retry.
	ErrorCodeRateLimitExceeded ErrorCode = "rate_limit_exceeded"

	// ErrorCodeParameterMissing indicates a required query or path parameter was not provided.
	ErrorCodeParameterMissing ErrorCode = "parameter_missing"
	// ErrorCodeParameterInvalid indicates a parameter value is not valid (e.g. negative limit).
	ErrorCodeParameterInvalid ErrorCode = "parameter_invalid"
	// ErrorCodeParameterUnknown indicates an unrecognized parameter was sent in the request.
	ErrorCodeParameterUnknown ErrorCode = "parameter_unknown"
	// ErrorCodeParametersExclusive indicates two mutually exclusive parameters were both provided.
	ErrorCodeParametersExclusive ErrorCode = "parameters_exclusive"

	// ErrorCodeInternalError is an unexpected server-side failure. Transient; safe to retry.
	ErrorCodeInternalError ErrorCode = "internal_error"
	// ErrorCodeSvcUnavailable indicates a downstream service is temporarily unreachable. Transient.
	ErrorCodeSvcUnavailable ErrorCode = "service_unavailable"
	// ErrorCodeExternalSvcError indicates a third-party dependency returned an error. Transient.
	ErrorCodeExternalSvcError ErrorCode = "external_service_error"
	// ErrorCodeTimeout indicates the operation exceeded its deadline. Transient.
	ErrorCodeTimeout ErrorCode = "timeout"
	// ErrorCodeConnectionError indicates a network-level failure to a downstream service. Transient.
	ErrorCodeConnectionError ErrorCode = "connection_error"
	// ErrorCodeRequestTimeout indicates the overall request exceeded its deadline. Transient.
	ErrorCodeRequestTimeout ErrorCode = "request_timeout"
	// ErrorCodeClientClosedRequest indicates the client disconnected before the server finished. Not transient — the server cannot meaningfully retry on behalf of a disconnected client.
	ErrorCodeClientClosedRequest ErrorCode = "client_closed_request"

	// ErrorCodeAPIVersionRequired indicates the OpenMRP-Version header was missing from the request.
	ErrorCodeAPIVersionRequired ErrorCode = "api_version_required"
	// ErrorCodeAPIVersionInvalid indicates the requested API version string is not recognized.
	ErrorCodeAPIVersionInvalid ErrorCode = "api_version_invalid"
	// ErrorCodeAPIVersionTooOld indicates the requested API version is below the minimum supported by the endpoint.
	ErrorCodeAPIVersionTooOld ErrorCode = "api_version_too_old"
)

func (ErrorCode) EnumValues

func (c ErrorCode) EnumValues() []string

EnumValues returns all valid ErrorCode values for use in schema generation.

func (ErrorCode) IsValid

func (c ErrorCode) IsValid() bool

IsValid reports whether the ErrorCode is a recognized value.

type ErrorType

type ErrorType string

ErrorType categorizes errors into broad classes for client-side handling.

  • ErrorTypeAPI: server-side failures (5xx-class), safe to retry if transient.
  • ErrorTypeIdempotency: idempotency key conflicts (concurrent or mismatched requests).
  • ErrorTypeInvalidRequest: client-side mistakes (bad input, missing auth, not found).
const (
	// ErrorTypeAPI covers server-side failures (5xx). Generally transient and safe to retry.
	ErrorTypeAPI ErrorType = "api_error"
	// ErrorTypeIdempotency covers idempotency key conflicts. May be transient (concurrent duplicate request) or permanent (reused key with different parameters).
	ErrorTypeIdempotency ErrorType = "idempotency_error"
	// ErrorTypeInvalidRequest covers client-side mistakes (bad input, missing auth, not found). Generally not retryable — the client must fix the request before resending.
	ErrorTypeInvalidRequest ErrorType = "invalid_request_error"
)

func (ErrorType) EnumValues

func (t ErrorType) EnumValues() []string

EnumValues returns all valid ErrorType values for use in schema generation.

func (ErrorType) IsValid

func (t ErrorType) IsValid() bool

IsValid reports whether the ErrorType is a recognized value.

type QuotaInfo

type QuotaInfo struct {
	// Limit is the maximum number of resources allowed by the current plan.
	Limit int32 `json:"limit"`
	// Used is the number of resources currently consumed.
	Used int32 `json:"used"`
	// ResetAt is the time when the quota resets, if applicable. Nil for static (non-metered) limits.
	ResetAt *time.Time `json:"reset_at"`
}

QuotaInfo provides machine-readable details about a plan-imposed resource limit. Included in limit_exceeded errors so clients can display upgrade prompts, usage bars, or implement programmatic retry/backoff logic.

type ResponseError

type ResponseError struct {
	// A machine-readable code for the error.
	Code ErrorCode `json:"code"`
	// The type of error.
	Type ErrorType `json:"type"`
	// A human-readable message providing more details about the error.
	Message string `json:"message"`
	// The parameter that caused the error, if applicable.
	Param *string `json:"param"`
	// A URL to documentation about the error.
	DocURL *string `json:"doc_url"`
	// Whether this error is transient and the request can be retried.
	IsTransient bool `json:"is_transient"`
	// Quota provides plan limit details when the error is limit_exceeded. Nil otherwise.
	Quota *QuotaInfo `json:"quota"`
	// RequestLogURL is a link to the dashboard page for this request's log entry. Nil when no request log is available.
	RequestLogURL *string `json:"request_log_url"`
}

ResponseError is the JSON-serializable error body returned to API clients. It contains only public information. This struct is used by the OpenAPI schema generator to produce documentation.

func (ResponseError) SchemaExample

func (r ResponseError) SchemaExample() any

SchemaExample returns a representative instance for OpenAPI documentation generation.

type RowError

type RowError struct {
	// Zero-based row of the request this failure names.
	Index int
	// What went wrong — the same error object a synchronous error response carries.
	Error ResponseError
}

pairs one row of a bulk request with the failure it produced. Internal to the services that run bulk work: the API reports a row's failure on the job result for that row, so this never reaches the wire.

func NewRowError

func NewRowError(index int, apiErr *APIError) RowError

records a row that failed, carrying the canonical client-facing error object

type RowErrors

type RowErrors struct {
	// contains filtered or unexported fields
}

collects the failures found across a bulk request's rows, keeping each one whole

func (*RowErrors) Add

func (e *RowErrors) Add(index int, apiErr *APIError)

records the failure one row produced

func (*RowErrors) AddValidation

func (e *RowErrors) AddValidation(index int, param, message string)

records a row's field-level validation failure against a row-indexed param

func (*RowErrors) Any

func (e *RowErrors) Any() bool

reports whether any row failed

func (*RowErrors) Entries

func (e *RowErrors) Entries() []RowError

hands back the collected failures

func (*RowErrors) Summary

func (e *RowErrors) Summary(entityPlural string) *APIError

renders the collected failures as the one error a synchronous response carries, or nil when there are none. Param holds a single field, so it names the first offending row.

Jump to

Keyboard shortcuts

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