apierror

package
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Sep 27, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package apierror defines a structured error type and wire format used by cloop's HTTP servers (pkg/ui and pkg/apiserver).

The package exposes:

  • APIError: a typed error with a stable string Code, a human-readable Message, an HTTPStatus, and an optional Details map.
  • A registry of well-known error codes (INVALID_INPUT, NOT_FOUND, RATE_LIMITED, INTERNAL, …) that callers should reuse instead of inventing new strings. Codes are part of the API contract; renaming one is a breaking change.
  • WriteError, a helper that renders any error as the canonical JSON response body, mapping plain errors and pkg/statedb sentinels to a sensible default code/status.

Wire format:

{
  "error": {
    "code":    "NOT_FOUND",
    "message": "task 42 does not exist",
    "details": { "task_id": 42 }   // optional, omitted if empty
  }
}

The top-level "error" envelope keeps the body distinguishable from a successful payload at a glance and leaves room for additional metadata (request id, etc.) without breaking existing clients.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsCode

func IsCode(err error, code Code) bool

IsCode reports whether err (or any error wrapped by err) carries an APIError whose Code matches the given code.

func Write

func Write(w http.ResponseWriter, code Code, message string)

Write is a convenience wrapper that builds an APIError from a code and message and writes it. Equivalent to WriteError(w, New(code, message)) but reads more naturally at the call site:

apierror.Write(w, apierror.CodeInvalidInput, "title is required")

func WriteError

func WriteError(w http.ResponseWriter, e *APIError)

WriteError serialises e as the canonical JSON error body and writes the associated HTTP status. nil e is treated as a generic internal error so handlers can pass through a fresh helper return without nil-checking.

The Content-Type is set to application/json. Header writes happen before the body, so callers must not write to w themselves before invoking this helper. Errors writing the body are intentionally swallowed — the status has already been sent and there is nothing meaningful to do.

func WriteFromError

func WriteFromError(w http.ResponseWriter, err error)

WriteFromError accepts any error, runs it through FromError, and writes the result. nil err is a no-op so callers can use it in idiomatic

if err != nil { apierror.WriteFromError(w, err); return }

blocks without an extra layer of helpers.

func WriteStatus

func WriteStatus(w http.ResponseWriter, message string, status int)

WriteStatus is a convenience wrapper for the legacy `jsonErr(w, message, status)` call shape. It maps status to a Code via FromHTTPStatus, then writes the structured response. Existing handlers can therefore migrate in two phases: first switch to WriteStatus to land the wire-format change project-wide, then iteratively replace the call with an explicit Code to make the intent visible in the source.

Types

type APIError

type APIError struct {
	// Code is a stable machine-readable identifier. Required.
	Code Code `json:"code"`

	// Message is a human-readable explanation. Should be specific enough
	// for an operator reading logs but must not leak secrets, internal
	// paths, or stack traces.
	Message string `json:"message"`

	// HTTPStatus is the HTTP status code to send. Zero means "use the
	// default for Code". Setting a non-zero value is useful for codes
	// like CodeInternal whose default may be too coarse for a specific
	// failure mode.
	HTTPStatus int `json:"-"`

	// Details carries optional structured context (e.g. validation
	// failures, the offending field, the task id involved). Nil/empty
	// maps are omitted from the wire response.
	Details map[string]any `json:"details,omitempty"`
	// contains filtered or unexported fields
}

APIError is the canonical error type returned by cloop HTTP handlers.

APIError implements the error interface so it composes naturally with errors.Is / errors.As. Two APIErrors with the same Code are reported as equal by Is — handlers can therefore test for `apierror.IsCode(err, apierror.CodeNotFound)` without caring about the exact message.

func FromError

func FromError(err error) *APIError

FromError returns an *APIError that best represents err for an HTTP response. The mapping rules, in order of precedence:

  1. nil → nil.
  2. err already is an *APIError → returned unchanged.
  3. errors.Is matches a pkg/statedb sentinel → mapped to the corresponding code (NOT_FOUND, CONFLICT, UNAVAILABLE, INTERNAL).
  4. http.MaxBytesError → CodePayloadTooLarge.
  5. Anything else → CodeInternal wrapping err as the cause.

Use this from a handler when you have an arbitrary error and want to hand it straight to WriteError without writing a switch yourself.

func New

func New(code Code, message string) *APIError

New constructs an APIError with the given code and message. HTTPStatus is left zero so it picks up the code's default; callers needing a specific status should set it on the returned value.

func Newf

func Newf(code Code, format string, args ...any) *APIError

Newf is New with fmt.Sprintf-style formatting. Avoids importing fmt at every call site for the common "build a one-line message" pattern.

func (*APIError) Error

func (e *APIError) Error() string

Error implements the error interface. Format: "CODE: message".

func (*APIError) Is

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

Is matches another error against this APIError. Two APIErrors are equal when their Codes match. This lets callers write `errors.Is(err, apierror.New(apierror.CodeNotFound, ""))` without caring about message contents.

func (*APIError) Unwrap

func (e *APIError) Unwrap() error

Unwrap returns the underlying error, if any. Allows errors.Is / errors.As to traverse APIError → cause chains created by WrapError.

func (*APIError) WithCause

func (e *APIError) WithCause(cause error) *APIError

WithCause returns a copy of e wrapping cause. The cause becomes the errors.Unwrap target so errors.Is / errors.As can traverse to it.

func (*APIError) WithDetails

func (e *APIError) WithDetails(kv map[string]any) *APIError

WithDetails returns a copy of e with the provided detail fields merged in. Existing keys are overwritten. Nil-safe.

func (*APIError) WithStatus

func (e *APIError) WithStatus(status int) *APIError

WithStatus returns a copy of e with HTTPStatus overridden.

type Code

type Code string

Code is a stable, machine-readable identifier for a class of API errors. Codes are part of the public API contract — never rename one; add a new constant if you need finer granularity. The wire form is the bare string value (e.g. "INVALID_INPUT").

const (
	// CodeInvalidInput indicates the client sent a malformed request: bad
	// JSON, missing required fields, out-of-range numeric values, an
	// unparseable path parameter, etc. Maps to HTTP 400.
	CodeInvalidInput Code = "INVALID_INPUT"

	// CodeUnauthorized indicates the request is missing or carries invalid
	// credentials (bearer token). Maps to HTTP 401.
	CodeUnauthorized Code = "UNAUTHORIZED"

	// CodeForbidden indicates the caller is authenticated but not allowed
	// to perform the requested action. Maps to HTTP 403.
	CodeForbidden Code = "FORBIDDEN"

	// CodeNotFound indicates the requested resource (project, task,
	// artifact, …) does not exist. Maps to HTTP 404.
	CodeNotFound Code = "NOT_FOUND"

	// CodeMethodNotAllowed indicates the resource exists but the HTTP
	// method is not supported on it. Maps to HTTP 405.
	CodeMethodNotAllowed Code = "METHOD_NOT_ALLOWED"

	// CodeConflict indicates a state-machine or optimistic-concurrency
	// conflict: the resource is in a state incompatible with the request,
	// or the caller's plan version is stale. Maps to HTTP 409.
	CodeConflict Code = "CONFLICT"

	// CodePayloadTooLarge indicates the request body exceeded the
	// configured cap (MaxBytesReader). Maps to HTTP 413.
	CodePayloadTooLarge Code = "PAYLOAD_TOO_LARGE"

	// CodeRateLimited indicates the per-IP rate limit was exceeded. Maps
	// to HTTP 429. Servers should set Retry-After alongside this error.
	CodeRateLimited Code = "RATE_LIMITED"

	// CodeQuotaExceeded indicates the caller's identity is at one of its
	// configured ceilings: too many projects, concurrent tasks, executors
	// or sessions, or its daily token/cost budget is spent (pkg/quota).
	//
	// Deliberately distinct from CodeRateLimited, which is about request
	// frequency from an address and clears in milliseconds. A quota denial
	// is about an identity's standing allocation, and the two need opposite
	// client behaviour: retry the rate limit, do not retry a quota that
	// needs an admin to raise.
	//
	// The default status is 429 because the common cases — a concurrency
	// slot, a daily budget — do clear on their own, and those carry a
	// Retry-After. The cases that do not clear by waiting (max_projects,
	// max_executors, max_sessions) are written with WithStatus(403) and no
	// Retry-After, so the code stays stable while the status tells a client
	// whether waiting is worth anything.
	CodeQuotaExceeded Code = "QUOTA_EXCEEDED"

	// CodeInternal indicates an unexpected server-side failure. Maps to
	// HTTP 500. Use sparingly; prefer a specific code when one fits.
	CodeInternal Code = "INTERNAL"

	// CodeUnavailable indicates a transient backend failure: SQLite
	// reported "database is locked" beyond the busy_timeout, an upstream
	// dependency is down, etc. Maps to HTTP 503. Callers may retry.
	CodeUnavailable Code = "UNAVAILABLE"
)

func FromHTTPStatus

func FromHTTPStatus(status int) Code

FromHTTPStatus returns the Code that pairs with an HTTP status. Used by the legacy `jsonErr(w, message, status)` helpers in pkg/ui and pkg/apiserver as they migrate to structured codes — handlers that already know the precise status (e.g. from statedb.HTTPStatus) can forward through this helper without losing semantic information.

Statuses outside the small set we use intentionally fall through to CodeInternal: an unknown status almost certainly indicates a server-side failure, and labelling it as such is safer than silently inventing a new code.

Jump to

Keyboard shortcuts

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