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.)
Classification of a joined error (errors.Join, or any tree whose Unwrap returns []error) follows stdlib errors.As traversal order: pre-order, depth-first, so the first coded error wins - for errors.Join, the earliest coded argument. Join(notFoundErr, internalErr) therefore classifies as NOT_FOUND/404 while Join(internalErr, notFoundErr) classifies as INTERNAL_ERROR/500. When aggregating errors of different severities (e.g. a client-facing error with an operational cleanup failure), don't rely on argument order - classify the aggregate explicitly:
return svcerr.Wrap(errors.Join(notFoundErr, cleanupErr), svcerr.ErrCodeInternal, "request processing failed")
The semantic types' identity - a validation error's field, a not-found error's resource ID, a rate limit's retry delay - is fixed at construction and read through same-name accessor methods (ValidationError.Field(), NotFoundError.ResourceID(), RateLimitError.RetryAfter(), ...). Every projection of an error - response details, headers, log fields, Context() - derives from that one canonical state, so they can never disagree. The one identity-adjacent value legitimately learned after construction - an upstream retry hint - has a dedicated clamping setter, ExternalAPIError.SetRetryAfter. (Before v1 these were exported writable fields; the migration is mechanical - x.Field becomes x.Field(). See docs/v1-design-pass.md.)
Identity fixation is by reference, not deep copy: a mutable object a constructor is given (e.g. the map or slice passed as NewValidationError's value) stays shared, so mutating it afterward is visible through Value() and Context() - pass a snapshot if the original will change. This never affects wire output or logs (validation values are deliberately excluded from both); it is the same shallow-ownership rule PublicDetails documents for detail values.
Errors are not safe for concurrent mutation. SetPublicMessage, SetPublicDetail, RemovePublicDetail, SetProblemType, SetProblemInstance, SetProblemTitle, SetAuthenticateChallenge, SetRetryAfter, and RecaptureStackTrace all mutate the receiver in place with no locking. 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). Identity accessors (Field(), ResourceID(), ...) are safe to call concurrently once construction and configuration are done.
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 SetDefaultAuthenticateChallenge(challenge string)
- func SetHeaderPolicy(p HeaderPolicy)
- func SetRecoveryHeaderPolicy(p HeaderPolicy)
- 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]any
- 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]any, 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 any)
- 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 HeaderPolicy
- type InternalError
- type Level
- type Logger
- type NotFoundError
- type ProblemDetails
- type ProblemInstancer
- type ProblemTitler
- type ProblemTyper
- type PublicDetailer
- type PublicMessager
- type RateLimitError
- type Renderer
- type RendererConfig
- type StackTracer
- type ValidationError
- type WriteResult
Examples ¶
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. A typed-nil StackTracer (see isNilValue) is treated the same as no match at all, matching outermostCoded/GetErrorCode's handling of the same footgun - without this guard, a *BaseError-derived nil pointer assigned to a plain error variable would panic here instead of degrading gracefully.
func HTTPStatusCode ¶
HTTPStatusCode maps error codes to HTTP status codes: the RegisterStatusCode registry first, then the built-in mapping. This is the mapping the package-level writers use; a Renderer consults its own StatusCodes instead of the registry (see RendererConfig).
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. A typed-nil err (see isNilValue) is also a no-op rather than a panic, matching outermostCoded's treatment of the same case.
func RecoveryMiddleware ¶
Middleware for error recovery and logging, using the package-level configuration (global status registry, challenge default, and recovery header policy, read at response time). A Renderer's Middleware method provides the same behavior under that renderer's own immutable configuration and logger.
Example ¶
RecoveryMiddleware turns a handler panic into a proper 500 JSON response (when nothing was committed yet) and one structured log record; a nil logger disables logging without changing the response.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
handler := svcerr.RecoveryMiddleware(nil)(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
panic("something broke")
}))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/leagues", nil))
fmt.Println(rec.Code)
fmt.Println(rec.Body.String())
}
Output: 500 {"error":{"code":"INTERNAL_ERROR","message":"An internal error occurred. Please contact support if the problem persists."}}
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. code must be non-empty: New/Wrap already normalize an empty code to ErrCodeInternal, so a registration entry keyed on "" could never be reached by this package's own errors and would only ever be reachable by a caller-supplied Coder that skips New/Wrap's normalization, silently shadowing ErrCodeInternal's own mapping instead of a real code's.
Example ¶
An application-specific code outside the built-in set registers its HTTP status once, at startup. Custom codes aren't in the built-in safe-message categories, so pair them with SetPublicMessage.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
const errCodeOutOfStock svcerr.ErrorCode = "OUT_OF_STOCK"
if err := svcerr.RegisterStatusCode(errCodeOutOfStock, http.StatusConflict); err != nil {
panic(err) // a bad registration should fail loudly at startup
}
err := svcerr.New(errCodeOutOfStock, "sku WIDGET-42 depleted, restock ETA unknown")
err.SetPublicMessage("This item is out of stock.")
rec := httptest.NewRecorder()
status := svcerr.WriteJSON(rec, err)
fmt.Println(status)
fmt.Println(rec.Body.String())
}
Output: 409 {"error":{"code":"OUT_OF_STOCK","message":"This item is out of stock."}}
func SetDefaultAuthenticateChallenge ¶ added in v0.7.0
func SetDefaultAuthenticateChallenge(challenge string)
SetDefaultAuthenticateChallenge sets an application-wide WWW-Authenticate challenge (e.g. `Bearer realm="api"`) that every 401 response from this package's writers carries when the error itself doesn't provide one via SetAuthenticateChallenge - an error-specific challenge always wins over this default. RFC 9110 §11.6.1 requires at least one WWW-Authenticate challenge on every server-generated 401 response; this package can't invent an application's authentication scheme or realm on its own, so without this call (or per-error challenges) a bare 401 remains possible. Set it once at startup, like RegisterStatusCode; the empty string clears it. Safe for concurrent use.
Example ¶
An application-wide default WWW-Authenticate challenge, configured once at startup, covers every 401 whose error doesn't carry its own via SetAuthenticateChallenge - RFC 9110 §11.6.1 requires a challenge on every server-generated 401, and this saves each authentication-error construction site from having to remember that. (The deferred clear here is only to isolate this example from the rest of the test binary; real applications set it once and leave it.)
package main
import (
"fmt"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
svcerr.SetDefaultAuthenticateChallenge(`Bearer realm="api"`)
defer svcerr.SetDefaultAuthenticateChallenge("")
rec := httptest.NewRecorder()
status := svcerr.WriteJSON(rec, svcerr.NewAuthenticationError("token_expired", "session expired"))
fmt.Println(status)
fmt.Println(rec.Header().Get("WWW-Authenticate"))
}
Output: 401 Bearer realm="api"
func SetHeaderPolicy ¶ added in v0.8.0
func SetHeaderPolicy(p HeaderPolicy)
SetHeaderPolicy sets the HeaderPolicy for the normal error path - WriteHTTPError/WriteHTTPErrorHTML/WriteHTTPProblem and the WriteJSON/ WriteHTML/WriteProblem variants. It does not affect RecoveryMiddleware's panic replacement; see SetRecoveryHeaderPolicy. Set it once at startup, like RegisterStatusCode; the zero value restores the default behavior. Safe for concurrent use.
Example ¶
A deployment whose ResponseWriter is wrapped by a transparent compression middleware - one that sets Content-Encoding once and gzips everything written through it - declares that header live rather than stale, so error bodies written through the wrapper stay correctly labeled. RecoveryMiddleware's panic replacement has its own separate policy (SetRecoveryHeaderPolicy), since it writes underneath any such middleware. (The deferred reset only isolates this example; real applications set the policy once at startup.)
package main
import (
"fmt"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
svcerr.SetHeaderPolicy(svcerr.HeaderPolicy{KeepContentEncoding: true})
defer svcerr.SetHeaderPolicy(svcerr.HeaderPolicy{})
rec := httptest.NewRecorder()
rec.Header().Set("Content-Encoding", "gzip") // set by the compression wrapper
svcerr.WriteJSON(rec, svcerr.NewNotFoundError("league", "12345"))
fmt.Println(rec.Header().Get("Content-Encoding"))
}
Output: gzip
func SetRecoveryHeaderPolicy ¶ added in v0.8.0
func SetRecoveryHeaderPolicy(p HeaderPolicy)
SetRecoveryHeaderPolicy sets the HeaderPolicy for RecoveryMiddleware's panic-replacement response. Separate from SetHeaderPolicy because the two paths write to different points in the middleware stack: a normal error body goes through whatever wraps the handler's ResponseWriter (e.g. a compression middleware, whose Content-Encoding is then live), while a panic replacement is written to the writer recovery wraps directly, underneath any such middleware (the same header is then stale). Set it once at startup; the zero value restores the default behavior. Safe for concurrent use.
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.
Example ¶
Operational categories never leak their own text to clients: UserMessage returns the sanitized message the response writers send, while Error() keeps the full detail for logs.
package main
import (
"errors"
"fmt"
"github.com/n-ae/svcerr"
)
func main() {
cause := errors.New("dial tcp 10.0.0.5:5432: connection refused")
err := svcerr.WrapDatabaseError(cause, "query", "SELECT id FROM leagues")
fmt.Println(svcerr.UserMessage(err))
fmt.Println(err.Error())
}
Output: Database error occurred. Please try again. database query failed: dial tcp 10.0.0.5:5432: connection refused
func WriteHTML ¶ added in v0.4.0
func WriteHTML(w http.ResponseWriter, err error) int
WriteHTML mirrors WriteJSON for the HTML rendering WriteHTTPErrorHTML writes. Use WriteHTMLResult instead to also see a write failure this discards.
Example ¶
The HTML fragment rendering for HTMX-style endpoints. Rate-limit errors carry Retry-After on every rendering, HTML included.
package main
import (
"fmt"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
err := svcerr.NewRateLimitError("api", 100, 30)
rec := httptest.NewRecorder()
status := svcerr.WriteHTML(rec, err)
fmt.Println(status, rec.Header().Get("Retry-After"))
fmt.Println(rec.Body.String())
}
Output: 429 30 <div class="error-message" role="alert"><h3>Error</h3><p>rate limit exceeded for api: 100 requests</p></div>
func WriteHTTPError ¶
func WriteHTTPError(w http.ResponseWriter, err error, logger Logger)
WriteHTTPError writes a standardized error response to the HTTP response writer, logging through logger.
Prefer a Renderer, whose JSON method both logs (via the config Logger) and returns the WriteResult in one call - this function remains fully supported, but new code shouldn't need the split logging/result variants it exists for.
func WriteHTTPErrorHTML ¶
func WriteHTTPErrorHTML(w http.ResponseWriter, err error, logger Logger)
WriteHTTPErrorHTML writes an HTML error response (for non-API endpoints), logging through logger.
Prefer a Renderer, whose HTML method both logs (via the config Logger) and returns the WriteResult in one call - this function remains fully supported, but new code shouldn't need the split logging/result variants it exists for.
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.
Prefer a Renderer, whose Problem method both logs (via the config Logger) and returns the WriteResult in one call - this function remains fully supported, but new code shouldn't need the split logging/result variants it exists for.
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. Use WriteJSONResult instead to also see a render/write failure this discards.
Example ¶
The README's primary flow: a service returns a semantic error, the handler renders it. WriteJSON is the logger-free variant of WriteHTTPError with the identical body.
package main
import (
"fmt"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
err := svcerr.NewNotFoundError("league", "12345")
rec := httptest.NewRecorder()
status := svcerr.WriteJSON(rec, err)
fmt.Println(status)
fmt.Println(rec.Body.String())
}
Output: 404 {"error":{"code":"NOT_FOUND","message":"league not found: 12345","details":{"resource_id":"12345","resource_type":"league"}}}
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. Use WriteProblemResult instead to also see a render/write failure this discards.
Example ¶
The RFC 9457 problem-details rendering of the same error: registered members plus this package's extension members, flattened to the top level.
package main
import (
"fmt"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
err := svcerr.NewNotFoundError("league", "12345")
rec := httptest.NewRecorder()
svcerr.WriteProblem(rec, err)
fmt.Println(rec.Header().Get("Content-Type"))
fmt.Println(rec.Body.String())
}
Output: application/problem+json {"code":"NOT_FOUND","detail":"league not found: 12345","resource_id":"12345","resource_type":"league","status":404,"title":"Not Found","type":"about:blank"}
Types ¶
type AuthenticationError ¶
type AuthenticationError struct {
BaseError
// contains filtered or unexported fields
}
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.
func (*AuthenticationError) Context ¶ added in v1.0.0
func (e *AuthenticationError) Context() map[string]any
Context returns this error's identity as a fresh map - reason.
func (*AuthenticationError) Reason ¶
func (e *AuthenticationError) Reason() string
Reason returns the authentication failure reason the constructor derived this error's code from - "token_expired", "token_invalid", "permission_denied", or any other caller-chosen string (which maps to ErrCodeUnauthorized).
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 9110 §11.6.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 the response writers set one only when the error provides it - or, when it doesn't, from the application-wide default configured via SetDefaultAuthenticateChallenge (an error-specific challenge always wins over that default). BaseError implements it via SetAuthenticateChallenge/AuthenticateChallenge. As with ProblemTyper, return true only alongside a non-empty string: a true with "" ships an empty WWW-Authenticate header value instead of falling back to the configured default.
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. An empty code is normalized to ErrCodeInternal.
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. An empty code is normalized to ErrCodeInternal.
Example ¶
Joined errors classify by their first coded child (stdlib errors.As traversal order), so reversing errors.Join's arguments would flip the response between 404 and 500. Classify a mixed-severity aggregate explicitly with Wrap instead of relying on argument order; the joined children stay reachable for errors.Is/errors.As.
package main
import (
"errors"
"fmt"
"github.com/n-ae/svcerr"
)
func main() {
notFound := svcerr.NewNotFoundError("user", "123")
cleanupErr := errors.New("temp file cleanup failed")
joined := errors.Join(notFound, cleanupErr)
wrapped := svcerr.Wrap(joined, svcerr.ErrCodeInternal, "request processing failed")
fmt.Println(svcerr.GetErrorCode(joined))
fmt.Println(svcerr.GetErrorCode(wrapped))
fmt.Println(errors.Is(wrapped, notFound), errors.Is(wrapped, cleanupErr))
}
Output: NOT_FOUND INTERNAL_ERROR true true
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 the error's identity as a structured map. Since v1 it is derived on demand from the same canonical fields every other projection (details, headers, log fields) uses, rather than snapshotted at construction - each semantic type shadows this method with its own derivation (see e.g. NotFoundError.Context), so the returned map can never disagree with what the response writers report. BaseError itself carries no identity beyond code and message, so for the generic New/ Wrap errors this returns nil, as it always has. Every call builds a fresh map; mutating the result never reaches 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. Both returned maps are shallow copies, the same contract as Context(): adding or removing keys through them doesn't reach back into the error - use SetPublicDetail/RemovePublicDetail, which also keep the two maps' last-call-wins bookkeeping intact - but an addition value that's itself a map, slice, or pointer is shared, not copied.
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. Takes precedence over the application-wide SetDefaultAuthenticateChallenge value, which covers 401 errors that don't call this.
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.
The two response shapes place details differently, which matters for six reserved names: WriteHTTPError/WriteJSON nest details under error.details, where any key is fine, but WriteHTTPProblem/WriteProblem flatten them to the top level of the RFC 9457 object, where "type", "title", "status", "detail", "instance", and "code" are registered (or package-owned) members - a detail with one of those names is silently omitted from problem-details output rather than allowed to occupy the member's slot. To set the real members, use SetProblemType, SetProblemTitle, and SetProblemInstance; status, detail, and code always come from the error's own classification.
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).
Example ¶
SetPublicMessage opts a specific error instance into a caller-chosen client-facing message, overriding the per-code default - the logged Error() text is unaffected.
package main
import (
"errors"
"fmt"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
err := svcerr.WrapDatabaseError(errors.New("connection refused"), "query", "SELECT 1")
err.SetPublicMessage("We're having trouble reaching the database. Please try again shortly.")
rec := httptest.NewRecorder()
svcerr.WriteJSON(rec, err)
fmt.Println(rec.Body.String())
}
Output: {"error":{"code":"DB_QUERY","message":"We're having trouble reaching the database. Please try again shortly.","details":{"operation":"query"}}}
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. Code should return a non-empty value: GetErrorCode normalizes "" to ErrCodeInternal (the same normalization New and Wrap apply to their code argument), so an empty Code() loses whatever specific classification was intended rather than reaching the wire as-is.
type ConflictError ¶
type ConflictError struct {
BaseError
// contains filtered or unexported fields
}
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.
func (*ConflictError) ConflictKey ¶
func (e *ConflictError) ConflictKey() string
ConflictKey returns the conflicting key or constraint, e.g. "email".
func (*ConflictError) Context ¶ added in v1.0.0
func (e *ConflictError) Context() map[string]any
Context returns this error's identity as a fresh map - resource_type and conflict_key.
func (*ConflictError) ResourceType ¶
func (e *ConflictError) ResourceType() string
ResourceType returns the kind of resource in conflict, e.g. "user".
type DatabaseError ¶
type DatabaseError struct {
BaseError
// contains filtered or unexported fields
}
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
func (*DatabaseError) Context ¶ added in v1.0.0
func (e *DatabaseError) Context() map[string]any
Context returns this error's identity as a fresh map - operation, and query when one was recorded.
func (*DatabaseError) Operation ¶
func (e *DatabaseError) Operation() string
Operation returns the database operation this error describes - "query", "insert", "update", "delete", "transaction", "migration".
func (*DatabaseError) Query ¶
func (e *DatabaseError) Query() string
Query returns the SQL text WrapDatabaseError recorded, or "" - never rendered to clients (see extractErrorDetails and errorLogFields).
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]any `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
// contains filtered or unexported fields
}
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
func (*ExternalAPIError) Context ¶ added in v1.0.0
func (e *ExternalAPIError) Context() map[string]any
Context returns this error's identity as a fresh map - service, status_code, url, and retry_after when a hint was recorded.
func (*ExternalAPIError) RetryAfter ¶
func (e *ExternalAPIError) RetryAfter() (seconds int, ok bool)
RetryAfter returns the upstream retry hint SetRetryAfter recorded, in seconds, and whether one was recorded at all. The stored value is always a valid non-negative delay-seconds - SetRetryAfter clamps on the way in, and nothing can mutate it afterward.
func (*ExternalAPIError) Service ¶
func (e *ExternalAPIError) Service() string
Service returns the caller-defined upstream service name, e.g. "yahoo", "nba_stats".
func (*ExternalAPIError) SetRetryAfter ¶ added in v0.13.0
func (e *ExternalAPIError) SetRetryAfter(seconds int)
SetRetryAfter records an upstream retry hint of seconds (e.g. parsed from the upstream's own Retry-After), clamped to non-negative per RFC 9110 §10.2.3 - the only way to attach the hint, since no constructor takes it. The response writers then emit it as the Retry-After header and the retry_after details member; RetryAfter() reads it back.
func (*ExternalAPIError) StatusCode ¶
func (e *ExternalAPIError) StatusCode() int
StatusCode returns the upstream's HTTP status, or 0 when unknown.
func (*ExternalAPIError) URL ¶
func (e *ExternalAPIError) URL() string
URL returns the upstream URL the constructor recorded - never rendered to clients (see extractErrorDetails and errorLogFields).
type HTTPErrorResponse ¶
type HTTPErrorResponse struct {
Error ErrorDetail `json:"error"`
}
HTTPErrorResponse represents a standardized HTTP error response
type HeaderPolicy ¶ added in v0.8.0
type HeaderPolicy struct {
// KeepContentEncoding preserves a pre-existing Content-Encoding
// header instead of deleting it. By default it's deleted because the
// body these writers produce is always plain, uncompressed text - a
// stale "gzip" left by a handler that never got to write its
// compressed body would make clients gzip-decode plain JSON. Set this
// when the ResponseWriter these writers receive belongs to a
// transparent compression middleware that sets the header once and
// compresses everything written through it: there the header is live,
// not stale, and deleting it mislabels a genuinely compressed body as
// plain text (see the README's compression-ordering section).
KeepContentEncoding bool
// ClearValidators additionally deletes ETag, Last-Modified, and
// Accept-Ranges. By default they're kept: they describe the specific
// successful representation this error response isn't attempting to
// be, but - unlike a wrong Content-Length or Content-Encoding - a
// stale conditional-request header doesn't actively mislead a client
// about the body it's receiving, and a plain WriteJSON call may
// legitimately want handler-set headers preserved. Set this for
// deployments that would rather no abandoned-representation metadata
// ever ride along on an error response.
ClearValidators bool
}
HeaderPolicy configures how this package's response writers treat representation headers a handler (or a wrapping middleware) already set before the error body is written. The zero value is the long-standing default behavior: Content-Encoding is cleared, validators are kept. Field polarities follow from that - each false value must mean "what the package always did".
type InternalError ¶
type InternalError struct {
BaseError
// contains filtered or unexported fields
}
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
func (*InternalError) Component ¶
func (e *InternalError) Component() string
Component returns the component the failure is attributed to, e.g. "billing" - also carried into the structured log fields on 5xx responses.
func (*InternalError) Context ¶ added in v1.0.0
func (e *InternalError) Context() map[string]any
Context returns this error's identity as a fresh map - component.
type Logger ¶
type Logger interface {
// Log records msg at the given level. err may be nil (e.g. when a
// caller passes a nil error to WriteHTTPError - the record still
// carries the response fields). 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]any, 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 ¶
type NotFoundError struct {
BaseError
// contains filtered or unexported fields
}
NotFoundError represents resource not found errors
func NewNotFoundError ¶
func NewNotFoundError(resourceType, resourceID string) *NotFoundError
NewNotFoundError creates a new not found error
Example ¶
Check error types with stdlib errors.As - there's no per-type IsXError wrapper.
package main
import (
"errors"
"fmt"
"github.com/n-ae/svcerr"
)
func main() {
findLeague := func(id string) error {
return svcerr.NewNotFoundError("league", id)
}
err := findLeague("12345")
var nfErr *svcerr.NotFoundError
if errors.As(err, &nfErr) {
fmt.Println(nfErr.ResourceType(), nfErr.ResourceID())
}
fmt.Println(svcerr.GetErrorCode(err), svcerr.HTTPStatusCode(svcerr.GetErrorCode(err)))
}
Output: league 12345 NOT_FOUND 404
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.
func (*NotFoundError) Context ¶ added in v1.0.0
func (e *NotFoundError) Context() map[string]any
Context returns this error's identity as a fresh map - resource_type and resource_id.
func (*NotFoundError) ResourceID ¶
func (e *NotFoundError) ResourceID() string
ResourceID returns the identifier that wasn't found. It's included in the response details and the error message by default - see RemovePublicDetail and SetPublicMessage when the identifier itself is sensitive.
func (*NotFoundError) ResourceType ¶
func (e *NotFoundError) ResourceType() string
ResourceType returns the kind of resource that wasn't found, e.g. "league", "user".
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]any // 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. Extension entries named after a registered member (or "code") are dropped - see reservedProblemMembers.
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. As with ProblemTyper, return true only alongside a non-empty string.
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. As with ProblemTyper, return true only alongside a non-empty string: a true with "" ships a blank RFC 9457 title rather than falling back to the computed default.
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. The bool is the contract: return true only alongside a non-empty string, the same guarantee BaseError's own implementation makes. WriteHTTPProblem uses the string as-is whenever the bool is true, so a true with "" ships an empty RFC 9457 "type" rather than falling back to "about:blank".
type PublicDetailer ¶ added in v0.4.1
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 ¶
type RateLimitError struct {
BaseError
// contains filtered or unexported fields
}
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.
func (*RateLimitError) Context ¶ added in v1.0.0
func (e *RateLimitError) Context() map[string]any
Context returns this error's identity as a fresh map - service, limit, and retry_after.
func (*RateLimitError) Limit ¶
func (e *RateLimitError) Limit() int
Limit returns the request limit that was exceeded.
func (*RateLimitError) RetryAfter ¶
func (e *RateLimitError) RetryAfter() int
RetryAfter returns the retry delay in seconds. The stored value is always a valid non-negative delay-seconds (RFC 9110 §10.2.3) - the constructors clamp on the way in, and nothing can mutate it afterward.
func (*RateLimitError) Service ¶
func (e *RateLimitError) Service() string
Service returns the rate-limited service name.
type Renderer ¶ added in v0.12.0
type Renderer struct {
// contains filtered or unexported fields
}
Renderer renders error responses under a fixed, instance-scoped configuration - for tests, for processes hosting two differently configured services, or simply to avoid global state. Construct with NewRenderer; the configuration is copied and immutable from then on.
A Renderer is fully self-contained: the package-level configuration - RegisterStatusCode's registry, SetDefaultAuthenticateChallenge, SetHeaderPolicy, SetRecoveryHeaderPolicy - does not affect it, and its own configuration never leaks to the package-level writers. If the application (or a library it uses) registers codes globally, mirror the ones this Renderer should know into RendererConfig.StatusCodes. Safe for concurrent use.
func NewRenderer ¶ added in v0.12.0
func NewRenderer(cfg RendererConfig) (*Renderer, error)
NewRenderer builds a Renderer from cfg. The config is deep-copied (mutating cfg or its StatusCodes map afterward has no effect), and every StatusCodes entry is validated against the same 400-599 rule as RegisterStatusCode - an invalid entry is rejected here, at startup, rather than surfacing later as a WriteHeader panic inside an error handler.
Example ¶
A Renderer scopes the package's configuration to an instance - status codes, challenge default, header policies, and the logger in one place - instead of the package-level globals. It's fully self-contained: global configuration doesn't affect it, and two differently configured Renderers can serve in the same process.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"github.com/n-ae/svcerr"
)
func main() {
r, err := svcerr.NewRenderer(svcerr.RendererConfig{
StatusCodes: map[svcerr.ErrorCode]int{"OUT_OF_CREDITS": http.StatusPaymentRequired},
DefaultAuthenticateChallenge: `Bearer realm="api"`,
})
if err != nil {
panic(err) // invalid config should fail loudly at startup
}
rec := httptest.NewRecorder()
res := r.JSON(rec, svcerr.New("OUT_OF_CREDITS", "credits exhausted"))
fmt.Println(res.Status)
rec = httptest.NewRecorder()
r.JSON(rec, svcerr.NewAuthenticationError("token_expired", "session expired"))
fmt.Println(rec.Header().Get("WWW-Authenticate"))
}
Output: 402 Bearer realm="api"
func (*Renderer) HTML ¶ added in v0.12.0
func (r *Renderer) HTML(w http.ResponseWriter, err error) WriteResult
HTML mirrors JSON for the HTML fragment rendering WriteHTTPErrorHTML writes. RenderErr is always nil - see WriteResult.
func (*Renderer) JSON ¶ added in v0.12.0
func (r *Renderer) JSON(w http.ResponseWriter, err error) WriteResult
JSON renders err's standardized JSON error response under this Renderer's configuration - the same body WriteHTTPError writes. It both logs (when a Logger is configured) and returns the WriteResult, collapsing the package-level WriteHTTPError/WriteJSON/WriteJSONResult split into one method.
func (*Renderer) Middleware ¶ added in v0.12.0
Middleware returns RecoveryMiddleware's behavior under this Renderer's configuration and Logger: panic recovery with commit tracking, writing the replacement internal-error response with this Renderer's status mapping, challenge default, and RecoveryHeaderPolicy.
func (*Renderer) Problem ¶ added in v0.12.0
func (r *Renderer) Problem(w http.ResponseWriter, err error) WriteResult
Problem mirrors JSON for the RFC 9457 application/problem+json rendering WriteHTTPProblem writes.
type RendererConfig ¶ added in v0.12.0
type RendererConfig struct {
// StatusCodes maps application ErrorCodes to HTTP statuses, layered
// over the built-in mapping (a built-in code may be overridden; an
// absent code falls through to the built-ins). Each status must be
// 400-599, the same rule RegisterStatusCode enforces. Note that a
// Renderer does NOT consult the global RegisterStatusCode registry -
// see Renderer.
StatusCodes map[ErrorCode]int
// DefaultAuthenticateChallenge is the WWW-Authenticate challenge for
// 401 responses whose error doesn't provide its own via
// SetAuthenticateChallenge - the per-instance equivalent of
// SetDefaultAuthenticateChallenge. Empty means no default.
DefaultAuthenticateChallenge string
// HeaderPolicy configures representation-header handling for the
// JSON/HTML/Problem methods - the per-instance equivalent of
// SetHeaderPolicy.
HeaderPolicy HeaderPolicy
// RecoveryHeaderPolicy configures the same for Middleware's panic
// replacement - the per-instance equivalent of
// SetRecoveryHeaderPolicy. See SetRecoveryHeaderPolicy for why the
// two paths are configured separately.
RecoveryHeaderPolicy HeaderPolicy
// Logger, when non-nil, receives one structured record per rendered
// response from JSON/HTML/Problem (the same fields WriteHTTPError
// logs) and per recovered panic from Middleware. Nil disables
// logging; the methods still return their WriteResult either way, so
// a Renderer never forces a choice between logging and result
// reporting the way the package-level WriteHTTPError/WriteJSONResult
// split does.
Logger Logger
}
RendererConfig is the complete configuration for a Renderer - every knob the package-level globals expose, plus the logger, in one struct. The zero value reproduces the package's default behavior with no logging.
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 ¶
type ValidationError struct {
BaseError
// contains filtered or unexported fields
}
ValidationError represents input validation errors
func NewValidationError ¶
func NewValidationError(message string, field string, value any) *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
func (*ValidationError) Context ¶ added in v1.0.0
func (e *ValidationError) Context() map[string]any
Context returns this error's identity as a fresh map - field and value - derived on demand from the same canonical state every other projection uses.
func (*ValidationError) Field ¶
func (e *ValidationError) Field() string
Field returns the input field this validation error describes.
func (*ValidationError) Value ¶
func (e *ValidationError) Value() any
Value returns the offending input value, when the constructor was given one - never rendered to clients (see extractErrorDetails), but available to callers for their own handling. The reference is fixed at construction but not deep-copied: if the caller passed a mutable object and mutates it later, Value (and Context) observe the change.
type WriteResult ¶ added in v0.6.3
type WriteResult struct {
// Status is the HTTP status code svcerr selected and passed to
// w.WriteHeader - on a marshal failure, the active mapping's status
// for ErrCodeInternal (500 by default), not necessarily err's own
// classification. It's what svcerr chose to
// send, not a transport confirmation that the client received exactly
// that status: a custom or third-party ResponseWriter could ignore it,
// have already committed a different status earlier, transform it, or
// panic during WriteHeader (see trackingResponseWriter.WriteHeader).
Status int
// RenderErr is the marshal error when the real body couldn't be
// JSON-encoded and a generic fallback was substituted instead (nil
// otherwise) - including a caller-supplied json.Marshaler that
// panicked, which is recovered and reported here as an error rather
// than allowed to escape the response writer (see safeJSONMarshal).
// Always nil from WriteHTMLResult, whose body is plain string
// concatenation and can't fail to encode.
RenderErr error
// WriteErr is whatever the final w.Write returned (nil on a full
// write) - a client disconnect, an expired deadline, or any other
// transport failure during delivery. Unlike RenderErr, it doesn't
// imply a different body was sent, only that delivery of the
// intended one may have failed or been truncated.
WriteErr error
// BytesWritten is the number of body bytes the final w.Write reported
// written: the full body length when WriteErr is nil, less on a
// truncated delivery (WriteErr is then non-nil - the transport's own
// error, or io.ErrShortWrite for a non-conforming writer that
// under-reported with a nil error). Body accounting only; status line
// and header bytes are never counted. Like Status, it's what the
// ResponseWriter reported, not a transport confirmation the client
// received those bytes.
BytesWritten int
}
WriteResult reports what WriteJSONResult/WriteHTMLResult/ WriteProblemResult actually did, for a caller that wants to detect a serialization or delivery failure without participating in this package's Logger contract. WriteHTTPError/WriteHTTPErrorHTML/ WriteHTTPProblem carry the same information into their log fields (response_render_error, response_write_error, response_bytes_written, rendered_error_code) instead of returning it; the plain WriteJSON/ WriteHTML/WriteProblem functions discard it entirely, same as before this type existed.
func WriteHTMLResult ¶ added in v0.6.3
func WriteHTMLResult(w http.ResponseWriter, err error) WriteResult
WriteHTMLResult mirrors WriteJSONResult for the HTML rendering. RenderErr is always nil - see WriteResult.
Prefer a Renderer, whose HTML method both logs (via the config Logger) and returns the WriteResult in one call - this function remains fully supported, but new code shouldn't need the split logging/result variants it exists for.
func WriteJSONResult ¶ added in v0.6.3
func WriteJSONResult(w http.ResponseWriter, err error) WriteResult
WriteJSONResult mirrors WriteJSON, additionally reporting a render or write failure - e.g. so a caller can avoid claiming success to its own caller, or report the failure through its own error-tracking system instead of this package's Logger contract.
Prefer a Renderer, whose JSON method both logs (via the config Logger) and returns the WriteResult in one call - this function remains fully supported, but new code shouldn't need the split logging/result variants it exists for.
func WriteProblemResult ¶ added in v0.6.3
func WriteProblemResult(w http.ResponseWriter, err error) WriteResult
WriteProblemResult mirrors WriteJSONResult for the RFC 9457 rendering.
Prefer a Renderer, whose Problem method both logs (via the config Logger) and returns the WriteResult in one call - this function remains fully supported, but new code shouldn't need the split logging/result variants it exists for.