Documentation
¶
Overview ¶
Package svcerr provides custom error types for consistent error handling.
Error types: ValidationError, DatabaseError, ExternalAPIError, AuthenticationError, NotFoundError, ConflictError, RateLimitError, InternalError.
All types implement ErrorWithCode interface and support error wrapping. A custom error type doesn't need to embed BaseError to participate - implementing just Coder, StackTracer, or PublicMessager independently is enough for the corresponding functions (GetErrorCode/HTTPStatusCode, GetStackTrace, and the public-message overrides) to recognize it.
This package's own code imports no logging library: WriteHTTPError, WriteHTTPErrorHTML, and RecoveryMiddleware log through the Logger interface instead - pass an adapter for whatever logger the caller uses. (The zerologadapter subpackage, which does depend on zerolog, is a separate Go module nested in this repo; only importing it pulls zerolog into a caller's build, not this package - this module has zero dependencies of its own.)
Errors are not safe for concurrent mutation. SetPublicMessage, SetPublicDetail, RemovePublicDetail, SetProblemType, SetProblemInstance, SetProblemTitle, SetAuthenticateChallenge, and RecaptureStackTrace all mutate the receiver in place with no locking, and the exported struct fields on ValidationError, DatabaseError, and the other semantic types are plain fields, not synchronized accessors. This is fine for the normal pattern of constructing and configuring an error locally before returning it, but don't call these once an error might be read or mutated from another goroutine (e.g. after handing it to a shared error-collection type).
Index ¶
- func GetStackTrace(err error) []string
- func HTTPStatusCode(code ErrorCode) int
- func RecaptureStackTrace(err error, extraSkip int)
- func RecoveryMiddleware(logger Logger) func(http.Handler) http.Handler
- func RegisterStatusCode(code ErrorCode, status int) error
- func UserMessage(err error) string
- func WriteHTML(w http.ResponseWriter, err error) int
- func WriteHTTPError(w http.ResponseWriter, err error, logger Logger)
- func WriteHTTPErrorHTML(w http.ResponseWriter, err error, logger Logger)
- func WriteHTTPProblem(w http.ResponseWriter, err error, logger Logger)
- func WriteJSON(w http.ResponseWriter, err error) int
- func WriteProblem(w http.ResponseWriter, err error) int
- type AuthenticationError
- type Authenticator
- type BaseError
- func (e *BaseError) AuthenticateChallenge() (string, bool)
- func (e *BaseError) Code() ErrorCode
- func (e *BaseError) Context() map[string]interface{}
- func (e *BaseError) Error() string
- func (e *BaseError) ProblemInstance() (string, bool)
- func (e *BaseError) ProblemTitle() (string, bool)
- func (e *BaseError) ProblemType() (string, bool)
- func (e *BaseError) PublicDetails() (add map[string]interface{}, remove map[string]struct{})
- func (e *BaseError) PublicMessage() (string, bool)
- func (e *BaseError) RemovePublicDetail(key string)
- func (e *BaseError) SetAuthenticateChallenge(challenge string)
- func (e *BaseError) SetProblemInstance(uri string)
- func (e *BaseError) SetProblemTitle(title string)
- func (e *BaseError) SetProblemType(uri string)
- func (e *BaseError) SetPublicDetail(key string, value interface{})
- func (e *BaseError) SetPublicMessage(msg string)
- func (e *BaseError) StackTrace() []string
- func (e *BaseError) Unwrap() error
- type Coder
- type ConflictError
- type DatabaseError
- type ErrorCode
- type ErrorDetail
- type ErrorWithCode
- type ExternalAPIError
- type HTTPErrorResponse
- type InternalError
- type Level
- type Logger
- type NotFoundError
- type ProblemDetails
- type ProblemInstancer
- type ProblemTitler
- type ProblemTyper
- type PublicDetailer
- type PublicMessager
- type RateLimitError
- type StackTracer
- type ValidationError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func GetStackTrace ¶
GetStackTrace extracts the stack trace from an error. It only requires StackTracer, not the full ErrorWithCode.
func HTTPStatusCode ¶
HTTPStatusCode maps error codes to HTTP status codes
func RecaptureStackTrace ¶ added in v0.2.1
RecaptureStackTrace re-captures err's stack trace starting extraSkip frames higher than the normal New*/Wrap* capture point. Every constructor in this package assumes it's called directly from the site the trace should point at; if you wrap a constructor in your own helper function, the trace ends up pointing at that helper instead of its caller. Call RecaptureStackTrace(err, 1) from inside such a helper, immediately after constructing err, to fix that - err must be one of this package's error types (or wrap one); otherwise this is a no-op.
func RecoveryMiddleware ¶
Middleware for error recovery and logging
func RegisterStatusCode ¶ added in v0.3.0
RegisterStatusCode adds or overrides the HTTP status HTTPStatusCode returns for code. Use it to extend the built-in mapping with an application's own ErrorCode values (constructed via New/Wrap), or to override a built-in mapping for a deployment that wants different semantics. Safe for concurrent use.
status must be a valid error status (400-599, since this package only ever maps error codes to error responses) - an out-of-range value (0, 200, 999, ...) is rejected here rather than surfacing later as a WriteHeader panic from inside an error handler, which is a far harder place to diagnose it.
func UserMessage ¶ added in v0.1.1
UserMessage returns the safe, user-facing message for an error - the same sanitized text WriteHTTPError/WriteHTTPErrorHTML send (e.g. a wrapped database error's raw cause is never included), for callers that need to embed it in a custom response fragment instead of one of those two standard bodies.
func WriteHTML ¶ added in v0.4.0
func WriteHTML(w http.ResponseWriter, err error) int
WriteHTML mirrors WriteJSON for the HTML rendering WriteHTTPErrorHTML writes.
func WriteHTTPError ¶
func WriteHTTPError(w http.ResponseWriter, err error, logger Logger)
WriteHTTPError writes a standardized error response to the HTTP response writer
func WriteHTTPErrorHTML ¶
func WriteHTTPErrorHTML(w http.ResponseWriter, err error, logger Logger)
WriteHTTPErrorHTML writes an HTML error response (for non-API endpoints)
func WriteHTTPProblem ¶ added in v0.3.0
func WriteHTTPProblem(w http.ResponseWriter, err error, logger Logger)
WriteHTTPProblem writes an RFC 9457 "application/problem+json" error response - an alternative body shape to WriteHTTPError's own {"error": {...}} for callers whose clients expect the standard problem-details format. Status mapping, message safety (Detail never includes a wrapped cause's text without an explicit SetPublicMessage), and logging behave identically to WriteHTTPError.
func WriteJSON ¶ added in v0.4.0
func WriteJSON(w http.ResponseWriter, err error) int
WriteJSON writes err's standardized JSON error response to w and returns the HTTP status code used - the same body WriteHTTPError writes, minus the logging call, for a caller that wants to own reporting separately (its own Reporter, a nil Logger via WriteHTTPError, or none at all) instead of participating in this package's Logger contract just to render a response.
func WriteProblem ¶ added in v0.4.0
func WriteProblem(w http.ResponseWriter, err error) int
WriteProblem mirrors WriteJSON for the RFC 9457 rendering WriteHTTPProblem writes.
Types ¶
type AuthenticationError ¶
type AuthenticationError struct {
BaseError
SessionID string
Reason string // "token_expired", "token_invalid", "permission_denied"
}
AuthenticationError represents authentication and authorization errors
func NewAuthenticationError ¶
func NewAuthenticationError(reason, message string) *AuthenticationError
NewAuthenticationError creates a new authentication error
func WrapAuthenticationError ¶ added in v0.6.0
func WrapAuthenticationError(err error, reason, message string) *AuthenticationError
WrapAuthenticationError wraps an existing error as an authentication error. ErrCodeUnauthorized/ErrCodeTokenExpired/ErrCodeTokenInvalid/ ErrCodePermissionDenied are all in mayExposeOwnMessage's safe category, so message is shown to the client the same as NewAuthenticationError's - it's still an explicit caller argument, never derived from err.
type Authenticator ¶ added in v0.6.0
Authenticator is implemented by any error that specifies its own WWW-Authenticate challenge for a 401 response. RFC 7235 §3.1 requires a server generating a 401 to include at least one WWW-Authenticate challenge; this package has no way to know an application's authentication scheme or realm on its own, so none of the response writers set one unless the error provides it. BaseError implements it via SetAuthenticateChallenge/AuthenticateChallenge.
type BaseError ¶
type BaseError struct {
// contains filtered or unexported fields
}
BaseError provides common error functionality
func New ¶ added in v0.3.0
New creates a generic error with the given code and message. Prefer the semantic constructors below (NewValidationError, NewNotFoundError, ...) when one exists for what you're representing; use New directly for codes that have no dedicated constructor, e.g. ErrCodeMissingRequired, ErrCodeDatabaseConnection, ErrCodeDatabaseTransaction, ErrCodeDatabaseMigration, ErrCodeResourceConflict, or ErrCodeQuotaExceeded.
func Wrap ¶ added in v0.3.0
Wrap wraps err as a generic error with the given code and message. As with the semantic Wrap* constructors, err's text is never shown to clients unless SetPublicMessage is called explicitly.
func (*BaseError) AuthenticateChallenge ¶ added in v0.6.0
AuthenticateChallenge returns the challenge set by SetAuthenticateChallenge, and whether one was set at all.
func (*BaseError) Context ¶
Context returns a shallow copy of the error context: callers can't add, remove, or replace top-level keys in the error's internal map through the returned one, but a value that's itself a map, slice, or pointer is shared, not copied - mutating that value's contents still reaches into the error.
func (*BaseError) ProblemInstance ¶ added in v0.4.1
ProblemInstance returns the URI set by SetProblemInstance, and whether one was set at all.
func (*BaseError) ProblemTitle ¶ added in v0.5.0
ProblemTitle returns the title set by SetProblemTitle, and whether one was set at all.
func (*BaseError) ProblemType ¶ added in v0.4.1
ProblemType returns the URI set by SetProblemType, and whether one was set at all.
func (*BaseError) PublicDetails ¶ added in v0.4.1
PublicDetails returns the keys SetPublicDetail added or overrode, and the keys RemovePublicDetail suppressed - the capability extractErrorDetails needs to apply both on top of a built-in type's automatic extraction.
func (*BaseError) PublicMessage ¶ added in v0.2.1
PublicMessage returns the message set by SetPublicMessage, and whether one was set at all.
func (*BaseError) RemovePublicDetail ¶ added in v0.4.1
RemovePublicDetail suppresses key from the structured "details" map, even if this error's built-in type would otherwise include it - e.g. hiding NotFoundError's resource_id when the identifier itself is sensitive (an email address, say). Whichever of SetPublicDetail/ RemovePublicDetail was called most recently for a given key wins - calling this after SetPublicDetail(key, ...) un-does that addition.
func (*BaseError) SetAuthenticateChallenge ¶ added in v0.6.0
SetAuthenticateChallenge sets the WWW-Authenticate header WriteHTTPError/WriteHTTPErrorHTML/WriteHTTPProblem send alongside a 401 response - e.g. `Bearer realm="api"`. Only applied when the error's code maps to 401; set on an error whose code maps elsewhere and it's silently unused.
func (*BaseError) SetProblemInstance ¶ added in v0.4.1
SetProblemInstance sets the RFC 9457 "instance" URI WriteHTTPProblem sends for this specific occurrence - e.g. a request ID or trace URL. Unset by default, in which case the field is omitted entirely.
func (*BaseError) SetProblemTitle ¶ added in v0.5.0
SetProblemTitle overrides the RFC 9457 "title" WriteHTTPProblem sends for this error, in place of the default http.StatusText(status). See SetProblemType.
func (*BaseError) SetProblemType ¶ added in v0.4.1
SetProblemType overrides the RFC 9457 "type" URI WriteHTTPProblem sends for this error, in place of the default "about:blank". Per RFC 9457, pair this with SetProblemTitle too - a stable, occurrence-invariant summary of the custom type, since http.StatusText(status) (the default Title, correct only for "about:blank") describes the HTTP status in general, not this specific problem type.
func (*BaseError) SetPublicDetail ¶ added in v0.4.1
SetPublicDetail adds or overrides a key in the structured "details" map WriteHTTPError/WriteHTTPProblem send to the client, alongside whatever this error's built-in type already contributes (e.g. NotFoundError's resource_type/resource_id) - or, for a code with no dedicated type (New/ Wrap), the only source of details at all. Unlike SetPublicMessage, this can be called more than once to add several keys. Whichever of SetPublicDetail/RemovePublicDetail was called most recently for a given key wins - calling this after RemovePublicDetail(key) un-suppresses it.
func (*BaseError) SetPublicMessage ¶ added in v0.2.1
SetPublicMessage overrides the message WriteHTTPError, WriteHTTPErrorHTML, and UserMessage show the client for this error instance, so the logged Error() text (which may carry internal detail) and the client-facing text can differ. Unset by default, in which case those functions fall back to their normal behavior (the error's own message, or a default per-code message).
func (*BaseError) StackTrace ¶
StackTrace resolves and formats the stack trace captured when this error was constructed (or last RecaptureStackTrace'd). Each call builds a fresh []string, so there's no shared internal state for a caller to mutate through the result - but also no caching, so calling it repeatedly re-resolves the frames each time.
type Coder ¶ added in v0.3.0
type Coder interface {
Code() ErrorCode
}
Coder is implemented by any error that carries an application-specific ErrorCode - the minimal capability GetErrorCode and HTTPStatusCode need. A custom error type only needs this one method to participate in status mapping; unlike ErrorWithCode, it doesn't also require Unwrap or StackTrace.
type ConflictError ¶
ConflictError represents resource conflict errors
func NewConflictError ¶
func NewConflictError(resourceType, conflictKey, message string) *ConflictError
NewConflictError creates a new conflict error
func WrapConflictError ¶ added in v0.6.0
func WrapConflictError(err error, resourceType, conflictKey, message string) *ConflictError
WrapConflictError wraps an existing error as a conflict error. ErrCodeAlreadyExists is in mayExposeOwnMessage's safe category, so message is shown to the client the same as NewConflictError's - it's still an explicit caller argument, never derived from err.
type DatabaseError ¶
type DatabaseError struct {
BaseError
Operation string // "query", "insert", "update", "delete", "transaction", "migration"
Query string
}
DatabaseError represents database operation errors
func NewDatabaseError ¶
func NewDatabaseError(operation, message string) *DatabaseError
NewDatabaseError creates a new database error
func WrapDatabaseError ¶
func WrapDatabaseError(err error, operation, query string) *DatabaseError
WrapDatabaseError wraps an existing error as a database error
type ErrorCode ¶
type ErrorCode string
ErrorCode represents application-specific error codes
const ( // Validation errors (1xxx) ErrCodeInvalidInput ErrorCode = "INVALID_INPUT" ErrCodeMissingRequired ErrorCode = "MISSING_REQUIRED" ErrCodeInvalidFormat ErrorCode = "INVALID_FORMAT" ErrCodeConstraintViolation ErrorCode = "CONSTRAINT_VIOLATION" // Database errors (2xxx) ErrCodeDatabaseConnection ErrorCode = "DB_CONNECTION" ErrCodeDatabaseQuery ErrorCode = "DB_QUERY" ErrCodeDatabaseTransaction ErrorCode = "DB_TRANSACTION" ErrCodeDatabaseMigration ErrorCode = "DB_MIGRATION" // External API errors (3xxx). The specific service is carried in // ExternalAPIError.Service, not encoded as a separate code per service. ErrCodeExternalAPI ErrorCode = "EXTERNAL_API_ERROR" // Authentication errors (4xxx) ErrCodeTokenExpired ErrorCode = "TOKEN_EXPIRED" ErrCodeTokenInvalid ErrorCode = "TOKEN_INVALID" ErrCodePermissionDenied ErrorCode = "PERMISSION_DENIED" // Resource errors (5xxx) ErrCodeNotFound ErrorCode = "NOT_FOUND" ErrCodeAlreadyExists ErrorCode = "ALREADY_EXISTS" ErrCodeResourceConflict ErrorCode = "RESOURCE_CONFLICT" // Rate limiting (6xxx) ErrCodeRateLimitExceeded ErrorCode = "RATE_LIMIT_EXCEEDED" ErrCodeQuotaExceeded ErrorCode = "QUOTA_EXCEEDED" // Internal errors (9xxx). RecoveryMiddleware reports recovered panics // as ErrCodeInternal too - there's no separate panic-specific code. ErrCodeInternal ErrorCode = "INTERNAL_ERROR" ErrCodeNotImplemented ErrorCode = "NOT_IMPLEMENTED" )
func GetErrorCode ¶
GetErrorCode extracts the error code from an error. It only requires Coder, not the full ErrorWithCode - a custom error type that implements just Code() ErrorCode is picked up here (and so by HTTPStatusCode) even if it doesn't also implement Unwrap or StackTrace.
type ErrorDetail ¶
type ErrorDetail struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
}
ErrorDetail contains detailed error information for API responses
type ErrorWithCode ¶
type ErrorWithCode interface {
error
Coder
Unwrap() error
StackTracer
}
ErrorWithCode is the full capability set every type in this package implements via BaseError: Coder and StackTracer, plus error and Unwrap. Prefer the narrower Coder, StackTracer, or PublicMessager when writing a custom error type that doesn't want to embed BaseError - GetErrorCode, GetStackTrace, and getUserFriendlyMessage each check the capability they need independently, not this combined interface.
type ExternalAPIError ¶
type ExternalAPIError struct {
BaseError
Service string // caller-defined service name, e.g. "yahoo", "nba_stats"
StatusCode int
URL string
RetryAfter *int // seconds to retry after; not set by the constructors, assign it directly when known
}
ExternalAPIError represents errors from external APIs
func NewExternalAPIError ¶
func NewExternalAPIError(service, message string, statusCode int, url string) *ExternalAPIError
NewExternalAPIError creates a new external API error
func WrapExternalAPIError ¶
func WrapExternalAPIError(err error, service, url string, statusCode int) *ExternalAPIError
WrapExternalAPIError wraps an existing error as an external API error
type HTTPErrorResponse ¶
type HTTPErrorResponse struct {
Error ErrorDetail `json:"error"`
}
HTTPErrorResponse represents a standardized HTTP error response
type InternalError ¶
InternalError represents unexpected internal errors
func NewInternalError ¶
func NewInternalError(component, message string) *InternalError
NewInternalError creates a new internal error
func WrapInternalError ¶
func WrapInternalError(err error, component, message string) *InternalError
WrapInternalError wraps an existing error as an internal error
type Logger ¶
type Logger interface {
// Log records msg at the given level. err may be nil (e.g. the panic
// path logs a message with no associated error). fields carries
// structured context (error_code, http_status, stack_trace, and
// error-type-specific keys like "field" or "resource_id").
Log(level Level, err error, fields map[string]interface{}, msg string)
}
Logger is the minimal structured-logging surface this package needs. Nothing in this file imports a logging library - wrap your logger to satisfy this interface. See the zerologadapter subpackage for a zerolog adapter (that subpackage, unlike this one, does depend on zerolog).
type NotFoundError ¶
NotFoundError represents resource not found errors
func NewNotFoundError ¶
func NewNotFoundError(resourceType, resourceID string) *NotFoundError
NewNotFoundError creates a new not found error
func WrapNotFoundError ¶ added in v0.6.0
func WrapNotFoundError(err error, resourceType, resourceID string) *NotFoundError
WrapNotFoundError wraps an existing error (e.g. sql.ErrNoRows) as a not found error, preserving it in the chain for errors.Is/errors.As while still getting NotFoundError's type, automatic resource_type/resource_id details, and client-facing message - which is generated the same way NewNotFoundError's is, never derived from err's own text.
type ProblemDetails ¶ added in v0.3.0
type ProblemDetails struct {
Type string // a URI reference identifying the problem type; "about:blank" when none is registered
Title string // a short, occurrence-invariant summary of the problem type
Status int // the HTTP status code for this occurrence
Detail string // a human-readable explanation specific to this occurrence
Instance string // a URI reference identifying this specific occurrence, if known
Code ErrorCode // this package's own error code, as an extension member
Extensions map[string]interface{} // additional extension members (e.g. resource_id, field)
}
ProblemDetails is the RFC 9457 (https://www.rfc-editor.org/rfc/rfc9457) "application/problem+json" response body written by WriteHTTPProblem. Code and any extractErrorDetails fields are extension members - RFC 9457 says extension members live at the top level alongside the registered ones, which is what MarshalJSON does instead of nesting them.
func (ProblemDetails) MarshalJSON ¶ added in v0.3.0
func (p ProblemDetails) MarshalJSON() ([]byte, error)
MarshalJSON flattens Extensions into the top-level object rather than nesting them under a sub-object, per RFC 9457's extension-member model.
type ProblemInstancer ¶ added in v0.4.1
ProblemInstancer is implemented by any error that specifies its own RFC 9457 "instance" URI for WriteHTTPProblem - e.g. a request ID or trace URL for this specific occurrence. BaseError implements it via SetProblemInstance/ProblemInstance.
type ProblemTitler ¶ added in v0.5.0
ProblemTitler is implemented by any error that specifies its own RFC 9457 "title" for WriteHTTPProblem, in place of the default http.StatusText(status) - useful alongside a custom SetProblemType, since RFC 9457 defines title as a short, occurrence-invariant summary of that specific problem type, not of the HTTP status in general. BaseError implements it via SetProblemTitle/ProblemTitle.
type ProblemTyper ¶ added in v0.4.1
ProblemTyper is implemented by any error that specifies its own RFC 9457 "type" URI for WriteHTTPProblem, in place of the default "about:blank". BaseError implements it via SetProblemType/ProblemType.
type PublicDetailer ¶ added in v0.4.1
type PublicDetailer interface {
PublicDetails() (add map[string]interface{}, remove map[string]struct{})
}
PublicDetailer is implemented by any error that customizes the structured "details" map WriteHTTPError/WriteHTTPProblem send - keys to add or override beyond whatever a built-in type's automatic extraction already contributes (or, for a code with no dedicated type, the only source of details at all), and keys to suppress from that automatic extraction (e.g. hiding NotFoundError's resource_id when the identifier is sensitive). BaseError implements it via SetPublicDetail/ RemovePublicDetail.
type PublicMessager ¶ added in v0.3.0
PublicMessager is implemented by any error with a client-facing message distinct from its logged Error() text - the minimal capability getUserFriendlyMessage needs to honor SetPublicMessage-style overrides. BaseError (and so every type in this package) implements it via SetPublicMessage/PublicMessage.
type RateLimitError ¶
RateLimitError represents rate limiting errors
func NewRateLimitError ¶
func NewRateLimitError(service string, limit, retryAfter int) *RateLimitError
NewRateLimitError creates a new rate limit error
func WrapRateLimitError ¶ added in v0.6.0
func WrapRateLimitError(err error, service string, limit, retryAfter int) *RateLimitError
WrapRateLimitError wraps an existing error as a rate limit error. ErrCodeRateLimitExceeded is in mayExposeOwnMessage's safe category, so message is generated the same way NewRateLimitError's is, never derived from err's own text.
type StackTracer ¶ added in v0.3.0
type StackTracer interface {
StackTrace() []string
}
StackTracer is implemented by any error that can report a captured stack trace - the minimal capability GetStackTrace needs.
type ValidationError ¶
ValidationError represents input validation errors
func NewValidationError ¶
func NewValidationError(message string, field string, value interface{}) *ValidationError
NewValidationError creates a new validation error
func WrapValidationError ¶
func WrapValidationError(err error, message string, field string) *ValidationError
WrapValidationError wraps an existing error as a validation error