svcerr

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 11 Imported by: 0

README

svcerr

CI

Typed application errors for Go services: error codes, HTTP status mapping, JSON/HTML response writers, panic-recovery middleware, and stack trace capture. The core package imports no logging library — see "Logging" below.

import "github.com/n-ae/svcerr"

Why

Handlers need to turn an error into the right HTTP status, a safe user-facing message, and a structured log line, without leaking internals (raw SQL, stack traces, third-party error text) into the response. svcerr does that mapping once, centrally, instead of every handler improvising it.

Usage

func (s *Service) GetLeague(id string) (*League, error) {
	var league League
	err := s.db.QueryRow(`SELECT * FROM leagues WHERE id = ?`, id).Scan(&league)
	if err == sql.ErrNoRows {
		return nil, svcerr.NewNotFoundError("league", id)
	}
	if err != nil {
		return nil, svcerr.WrapDatabaseError(err, "query", "SELECT * FROM leagues...")
	}
	return &league, nil
}

func (h *Handler) GetLeague(w http.ResponseWriter, r *http.Request) {
	league, err := h.service.GetLeague(r.URL.Query().Get("id"))
	if err != nil {
		// h.logger implements svcerr.Logger - see "Logging" below
		svcerr.WriteHTTPError(w, err, h.logger) // maps to the right status, logs, writes JSON
		return
	}
	json.NewEncoder(w).Encode(league)
}

WriteHTTPError writes a JSON body:

{
  "error": {
    "code": "NOT_FOUND",
    "message": "league not found: 12345",
    "details": { "resource_type": "league", "resource_id": "12345" }
  }
}

WriteHTTPErrorHTML writes an HTML fragment instead, for HTMX-style endpoints. WriteHTTPProblem writes an RFC 9457 application/problem+json body instead, for clients that expect the standard problem-details shape:

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "league not found: 12345",
  "code": "NOT_FOUND",
  "resource_type": "league",
  "resource_id": "12345"
}

title is the HTTP status's standard reason phrase, per RFC 9457 §4.2.1 (since type is "about:blank" here); detail is specific to this occurrence (and follows the same public/internal message rules as WriteHTTPError - see "Public vs. internal messages" below). Extension members (code, resource_type, resource_id, ...) sit at the top level, per RFC 9457, rather than nested under a sub-object; see "Public details" below for adding to or suppressing them.

SetProblemType/SetProblemInstance/SetProblemTitle override type/instance/title on a specific error instance, for an application with its own stable problem-type URIs instead of about:blank:

err := svcerr.NewNotFoundError("league", id)
err.SetProblemType("https://example.com/problems/resource-not-found")
err.SetProblemInstance(requestURL) // omitted entirely when unset
err.SetProblemTitle("League not found")

title stays the HTTP status's reason phrase absent SetProblemTitle - correct alongside the default about:blank type (per RFC 9457 §4.2.1), but RFC 9457 expects a custom type to define its own, occurrence-invariant title rather than reusing the HTTP status's in general.

UserMessage(err) returns just the sanitized message, for callers embedding it in a custom response.

Check error types with stdlib errors.As — there's no per-type IsXError wrapper. svcerr is a distinct package name (not errors), so both imports coexist without an alias:

import (
	"errors"

	"github.com/n-ae/svcerr"
)

var nfErr *svcerr.NotFoundError
if errors.As(err, &nfErr) {
	// ...
}

Recover panics in HTTP handlers and turn them into a proper error response:

router.Use(svcerr.RecoveryMiddleware(h.logger))

RecoveryMiddleware tracks whether the handler already wrote a response before panicking, and won't write an error body over one that's already committed - it logs, then re-panics with http.ErrAbortHandler, which closes the HTTP/1 connection (or resets the HTTP/2 stream) instead of letting net/http treat whatever partial bytes the handler wrote as a complete, successful response. An outer panic-recovery layer, if any, must not swallow http.ErrAbortHandler - RecoveryMiddleware should normally be the outermost one. It also passes through http.Flusher and http.Hijacker when (and only when) the underlying ResponseWriter supports them, so SSE handlers and WebSocket upgrades still work for handlers wrapped by the middleware, and a handler's own w.(http.Flusher)/w.(http.Hijacker) checks (or an http.ResponseController) get a truthful answer instead of the wrapper always claiming both regardless of what's underneath. A successful hijack is itself treated as committing the response.

Flushing, hijacking, and the error-reporting FlushError() error form (which http.ResponseController prefers) are the only optional interfaces the wrapper preserves - http.Pusher and io.ReaderFrom in particular are dropped, so HTTP/2 server push and sendfile-style copy optimizations aren't available to handlers behind the middleware. One deliberate asymmetry: an underlying writer implementing only FlushError() error (not plain Flush()) gains a Flush() method through the wrapper, since the flush capability genuinely exists and http.Flusher is how handlers conventionally probe for it.

Not compatible with transparent, eagerly-header-setting compression wrappers. Every writer in this package (WriteJSON, WriteHTML, WriteProblem, WriteHTTPError/WriteHTTPErrorHTML/WriteHTTPProblem, and RecoveryMiddleware) unconditionally deletes any pre-existing Content-Encoding on the ResponseWriter before writing its own always-plaintext body - this is what stops a panic replaced mid-gzip-response from leaving a stale Content-Encoding: gzip on a body that's actually plain JSON. The tradeoff: if the ResponseWriter you pass in belongs to a compression middleware that sets Content-Encoding once up front and then transparently gzips everything written through it (a common pattern, including outside RecoveryMiddleware - any plain WriteJSON call reaches the same code), that header gets deleted while the body is compressed anyway, and the client receives gzip bytes labeled as plain text. There's no way for this package to tell a stale header (left over from whatever the handler intended before erroring out) apart from a live one a wrapper is about to honor. If you use this kind of compression middleware, put it inside (called before) any of this package's writers, not outside/wrapping them.

ETag, Last-Modified, and Accept-Ranges are left alone, unlike Content-Length/Content-Encoding/Trailer/Retry-After/ WWW-Authenticate, which every writer clears. Those three describe a specific successful representation - if a handler set one in anticipation of the response it never got to send (an ETag for content that won't match the error body, say), it can still be present on the error response that replaces it. This is deliberate: 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 to preserve headers a handler set for reasons unrelated to the error. It's most likely to matter for RecoveryMiddleware specifically, where the response is replacing an abandoned success path rather than being the request's only response.

Error types

ValidationError, DatabaseError, ExternalAPIError, AuthenticationError, NotFoundError, ConflictError, RateLimitError, InternalError — each has a New*/Wrap* constructor, carries an ErrorCode, and supports stdlib errors.Is/errors.As/errors.Unwrap in the usual way. See the ErrorCode constants in errors.go for the full list of codes and HTTPStatusCode in http.go for their HTTP status mapping.

For a code with no dedicated constructor (e.g. ErrCodeDatabaseConnection, ErrCodeMissingRequired, ErrCodeResourceConflict, ErrCodeQuotaExceeded), use the generic New/Wrap:

err := svcerr.New(svcerr.ErrCodeDatabaseConnection, "could not reach the database")
err := svcerr.Wrap(dbErr, svcerr.ErrCodeDatabaseConnection, "could not reach the database")

For an application-specific code entirely outside this package's built-in set, register its HTTP status once, at startup:

const ErrCodeOutOfStock svcerr.ErrorCode = "OUT_OF_STOCK"

func init() {
	if err := svcerr.RegisterStatusCode(ErrCodeOutOfStock, http.StatusConflict); err != nil {
		panic(err) // a bad registration at startup should fail loudly, not later inside an error handler
	}
}

RegisterStatusCode can also override a built-in code's mapping, for a deployment that wants different semantics than the default. It rejects any status outside 400-599, since this package only ever maps error codes to error responses.

Note that a custom code registers a status, not a message policy: it's not in the built-in safe-category list (see "Public vs. internal messages" below), so its response message falls back to the generic "An unexpected error occurred." rather than the message passed to New/Wrap. That's deliberate - this package can't know whether an unfamiliar code's messages are client-safe - so pair custom codes with SetPublicMessage:

err := svcerr.New(ErrCodeOutOfStock, "sku WIDGET-42 depleted, restock ETA unknown")
err.SetPublicMessage("This item is out of stock.")
Joined errors

Classification follows stdlib errors.As traversal order - pre-order, depth-first - so for a joined error (errors.Join, or any tree whose Unwrap returns []error) the first coded error wins:

svcerr.GetErrorCode(errors.Join(notFoundErr, internalErr)) // NOT_FOUND  -> 404
svcerr.GetErrorCode(errors.Join(internalErr, notFoundErr)) // INTERNAL_ERROR -> 500

Merely reversing the arguments changes the client's response. When aggregating errors of different severities - a client-facing error joined with an operational cleanup failure, say - don't rely on argument order; classify the aggregate explicitly:

return svcerr.Wrap(errors.Join(notFoundErr, cleanupErr),
	svcerr.ErrCodeInternal, "request processing failed")
Custom error types

You don't have to embed BaseError to plug into this package - implement just the capability you need:

type Coder interface {
	Code() ErrorCode // GetErrorCode, HTTPStatusCode
}

type StackTracer interface {
	StackTrace() []string // GetStackTrace
}

type PublicMessager interface {
	PublicMessage() (string, bool) // getUserFriendlyMessage / UserMessage overrides
}

type PublicDetailer interface {
	PublicDetails() (add map[string]interface{}, remove map[string]struct{}) // extractErrorDetails overrides
}

type ProblemTyper interface {
	ProblemType() (string, bool) // WriteHTTPProblem's "type", instead of "about:blank"
}

type ProblemInstancer interface {
	ProblemInstance() (string, bool) // WriteHTTPProblem's "instance"
}

type ProblemTitler interface {
	ProblemTitle() (string, bool) // WriteHTTPProblem's "title", instead of http.StatusText(status)
}

type Authenticator interface {
	AuthenticateChallenge() (string, bool) // WWW-Authenticate on a 401 response
}

A minimal type implementing Coder (and error) is enough to get correct status-code mapping from WriteHTTPError/WriteHTTPProblem; add Unwrap() error if you also want the "don't leak a wrapped cause" safety property. ErrorWithCode is the full combination BaseError-derived types implement (plus StackTracer), kept as a single name for convenience - it's not a requirement for participating in the package's functions.

Public vs. internal messages

By default, whether the client-facing message is the error's own message depends on its category, not on whether it wraps a cause:

  • Validation, not-found, conflict, auth, and rate-limit codes are client-input-shaped - the message you pass to NewValidationError, WrapValidationError, NewNotFoundError, etc. is always an explicit argument you chose, never text derived from a wrapped cause, so it's shown by default even when the error wraps one.
  • Database, external-API, and internal codes fall back to a generic per-code message instead, whether or not they wrap a cause - Error() text in these categories often carries operational detail (a raw query, an internal hostname, third-party response text, a secret) that must never reach a client without an explicit opt-in.

SetPublicMessage overrides the client-facing text explicitly, for either category:

err := svcerr.WrapDatabaseError(dbErr, "query", "SELECT * FROM leagues...")
err.SetPublicMessage("We're having trouble reaching the database. Please try again shortly.")
return err // WriteHTTPError/UserMessage now send the override; logs still get err.Error()
Public details

Built-in types automatically contribute a few structured fields to the response's details map - NotFoundError's resource_type/resource_id, ValidationError's field, RateLimitError's limit/retry_after, and so on (never anything caller-supplied and unbounded, like ValidationError.Value - see the source for the exact list per type). SetPublicDetail/RemovePublicDetail let you add to or suppress that on a specific error instance, and are the only way to get structured details at all for a code built with the generic New/Wrap (which have no built-in type to extract from). Calling either again for the same key later - even after the other one - changes your mind: whichever call was most recent wins.

err := svcerr.New(svcerr.ErrCodeConstraintViolation, "out of stock")
err.SetPublicDetail("sku", sku)
err.SetPublicDetail("available", 0)

RemovePublicDetail only touches the details map - not the message. NewNotFoundError(resourceType, resourceID)'s own message embeds resourceID directly ("user not found: secret@example.com"), and NOT_FOUND is a category mayExposeOwnMessage shows by default (see "Public vs. internal messages" above) - so RemovePublicDetail alone still leaves the identifier in message. If the identifier itself is sensitive, pair it with SetPublicMessage:

err := svcerr.NewNotFoundError("user", customerEmail)
err.RemovePublicDetail("resource_id")     // removes it from details...
err.SetPublicMessage("user was not found") // ...and this removes it from message
Authentication challenges

RFC 9110 §11.6.1 requires a server generating a 401 response 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 it never invents one - configure an application-wide default once, at startup (like RegisterStatusCode):

func init() {
	svcerr.SetDefaultAuthenticateChallenge(`Bearer realm="api"`)
}

Every 401 from WriteHTTPError/WriteHTTPErrorHTML/WriteHTTPProblem (and the WriteJSON/WriteHTML/WriteProblem variants) now carries that challenge, without each authentication-error construction site having to remember an HTTP protocol rule. A specific error instance can still override it via SetAuthenticateChallenge, which always wins over the default:

err := svcerr.NewAuthenticationError("token_expired", "session expired")
err.SetAuthenticateChallenge(`Bearer realm="api", error="invalid_token", error_description="the access token expired"`)
svcerr.WriteHTTPError(w, err, logger) // sends the error-specific challenge

Both are only applied when the response status is 401 - set on an error whose code maps elsewhere and they're silently unused. Without either, a bare 401 (no WWW-Authenticate) is still possible; the package won't guess a scheme for you. SetDefaultAuthenticateChallenge("") clears the default.

Wrapping constructors in your own helper

Every New*/Wrap* constructor assumes it's called directly from the site its stack trace should point at. If you wrap one in your own helper (e.g. a project-wide validation function), the trace ends up pointing at the helper instead of its caller. Fix that with RecaptureStackTrace, called from inside the helper right after construction:

func validateTeamID(id string) error {
	if id == "" {
		err := svcerr.NewValidationError("team_id is required", "team_id", nil)
		svcerr.RecaptureStackTrace(err, 1) // point past this helper
		return err
	}
	return nil
}
Concurrency

Errors aren't safe for concurrent mutation. SetPublicMessage, SetPublicDetail, RemovePublicDetail, SetProblemType, SetProblemInstance, SetProblemTitle, SetAuthenticateChallenge, and RecaptureStackTrace all mutate the receiver in place with no locking. That's fine for the normal pattern - construct an error, configure it, return it - but don't call these once an error might be read or mutated from another goroutine.

Logging

WriteHTTPError, WriteHTTPErrorHTML, WriteHTTPProblem, and RecoveryMiddleware log through a minimal Logger interface, not a specific logging library:

type Logger interface {
	Log(level Level, err error, fields map[string]interface{}, msg string)
}

Using zerolog? Wrap it with the zerologadapter subpackage instead of writing your own adapter:

import "github.com/n-ae/svcerr/zerologadapter"

svcerr.WriteHTTPError(w, err, zerologadapter.New(logger))

zerologadapter is a separate Go module (its own go.mod, nested in this repo at zerologadapter/) - it's the one depending on zerolog, not the core svcerr module. go get github.com/n-ae/svcerr never pulls in zerolog; only go get github.com/n-ae/svcerr/zerologadapter does, and only for callers who use it.

Adapter versioning: the adapter is tagged independently (zerologadapter/vX.Y.Z) and only when the adapter itself changes - not on every root release, so its latest tag lagging the root's is expected, not an oversight. Compatibility is carried by the Logger interface, not by version lockstep: the adapter's go.mod requires whatever root version was current at its last change, and Go's module resolution picks the higher of that and your own requirement, so an older adapter tag works fine with newer root releases. The adapter's minimum Go version is also dictated by zerolog's dependency chain rather than by this repo, so it can sit higher than the root module's own floor.

For any other logger, implement the one-method Logger interface directly. A nil Logger is also fine - WriteHTTPError/WriteHTTPErrorHTML/ WriteHTTPProblem/RecoveryMiddleware simply skip logging rather than panicking, so you don't have to plumb through a no-op implementation just to render a response without logging it.

If you want response rendering with no Logger involvement at all - e.g. you're reporting errors through something other than this package's Logger contract - use WriteJSON, WriteHTML, or WriteProblem directly. Each writes the same body as its WriteHTTP* counterpart and returns the status code used, without touching a logger:

statusCode := svcerr.WriteJSON(w, err) // no logging, no Logger argument
myReporter.Report(r.Context(), err, statusCode)

That discards whether the body actually rendered and reached the client. Use WriteJSONResult (or WriteHTMLResult/WriteProblemResult) instead to see that too, so your reporter doesn't claim success when the client never got a valid response:

result := svcerr.WriteJSONResult(w, err)
myReporter.Report(r.Context(), err, result.Status, result.RenderErr, result.WriteErr)

Origin

Extracted from an application's internal/errors package once it had no callers left depending on app-specific behavior — see the release notes for what changed along the way. Note: zerologadapter/v0.4.0 shipped with a go.mod that made it unfetchable outside this repo; use zerologadapter/v0.4.1 or later instead (the root svcerr module's own v0.4.0 tag is unaffected).

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")

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

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetStackTrace

func GetStackTrace(err error) []string

GetStackTrace extracts the stack trace from an error. It only requires StackTracer, not the full ErrorWithCode.

func HTTPStatusCode

func HTTPStatusCode(code ErrorCode) int

HTTPStatusCode maps error codes to HTTP status codes

func RecaptureStackTrace added in v0.2.1

func RecaptureStackTrace(err error, extraSkip int)

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

func RecoveryMiddleware(logger Logger) func(http.Handler) http.Handler

Middleware for error recovery and logging

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

func RegisterStatusCode(code ErrorCode, status int) error

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.

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 UserMessage added in v0.1.1

func UserMessage(err error) string

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

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. 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
	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

type Authenticator interface {
	AuthenticateChallenge() (string, bool)
}

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.

type BaseError

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

BaseError provides common error functionality

func New added in v0.3.0

func New(code ErrorCode, message string) *BaseError

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

func Wrap(err error, code ErrorCode, message string) *BaseError

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.

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

func (e *BaseError) AuthenticateChallenge() (string, bool)

AuthenticateChallenge returns the challenge set by SetAuthenticateChallenge, and whether one was set at all.

func (*BaseError) Code

func (e *BaseError) Code() ErrorCode

Code returns the error code

func (*BaseError) Context

func (e *BaseError) Context() map[string]interface{}

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) Error

func (e *BaseError) Error() string

Error implements the error interface

func (*BaseError) ProblemInstance added in v0.4.1

func (e *BaseError) ProblemInstance() (string, bool)

ProblemInstance returns the URI set by SetProblemInstance, and whether one was set at all.

func (*BaseError) ProblemTitle added in v0.5.0

func (e *BaseError) ProblemTitle() (string, bool)

ProblemTitle returns the title set by SetProblemTitle, and whether one was set at all.

func (*BaseError) ProblemType added in v0.4.1

func (e *BaseError) ProblemType() (string, bool)

ProblemType returns the URI set by SetProblemType, and whether one was set at all.

func (*BaseError) PublicDetails added in v0.4.1

func (e *BaseError) PublicDetails() (add map[string]interface{}, remove map[string]struct{})

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

func (e *BaseError) PublicMessage() (string, bool)

PublicMessage returns the message set by SetPublicMessage, and whether one was set at all.

func (*BaseError) RemovePublicDetail added in v0.4.1

func (e *BaseError) RemovePublicDetail(key string)

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

func (e *BaseError) SetAuthenticateChallenge(challenge string)

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

func (e *BaseError) SetProblemInstance(uri string)

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

func (e *BaseError) SetProblemTitle(title string)

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

func (e *BaseError) SetProblemType(uri string)

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

func (e *BaseError) SetPublicDetail(key string, value interface{})

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

func (e *BaseError) SetPublicMessage(msg string)

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

func (e *BaseError) StackTrace() []string

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.

func (*BaseError) Unwrap

func (e *BaseError) Unwrap() error

Unwrap implements error unwrapping

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

type ConflictError struct {
	BaseError
	ResourceType string
	ConflictKey  string
}

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)
	ErrCodeUnauthorized     ErrorCode = "UNAUTHORIZED"
	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

func GetErrorCode(err error) ErrorCode

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

type InternalError struct {
	BaseError
	Component string
}

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 Level

type Level int

Level is a log severity, independent of any specific logging library.

const (
	LevelInfo Level = iota
	LevelWarn
	LevelError
)

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

type NotFoundError struct {
	BaseError
	ResourceType string
	ResourceID   string
}

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.

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. Extension entries named after a registered member (or "code") are dropped - see reservedProblemMembers.

type ProblemInstancer added in v0.4.1

type ProblemInstancer interface {
	ProblemInstance() (string, bool)
}

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

type ProblemTitler interface {
	ProblemTitle() (string, bool)
}

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

type ProblemTyper interface {
	ProblemType() (string, bool)
}

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

type PublicMessager interface {
	PublicMessage() (string, bool)
}

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
	Service    string
	Limit      int
	RetryAfter int // seconds
}

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

type ValidationError struct {
	BaseError
	Field string
	Value interface{}
}

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

type WriteResult added in v0.6.3

type WriteResult struct {
	// Status is the HTTP status code svcerr selected and passed to
	// w.WriteHeader - the fallback 500 on a marshal failure, 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). 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
}

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, 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.

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.

func WriteProblemResult added in v0.6.3

func WriteProblemResult(w http.ResponseWriter, err error) WriteResult

WriteProblemResult mirrors WriteJSONResult for the RFC 9457 rendering.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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