Skip to content

Error responses (problem+json)

The management API (/api/v1/…, /api/admin/…) reports every error as an RFC 7807 problem-detail document — Content-Type: application/problem+json, one JSON object per error. There is one shared writer (api/internal/problem/problem.go) behind every handler in internal/handlers, so the shape below is uniform across the whole management surface.

Per-format wire protocols (/maven/…, /npm/…, /debian/…, the other adapters under internal/formats/, and Docker/OCI's /v2/…) are explicitly out of scope — they keep the error body their own protocol spec requires (e.g. Docker's {"errors":[{"code",...}]}), because the package manager on the other end parses that shape, not this one.

Shape

json
{
  "type": "about:blank",
  "title": "Conflict",
  "status": 409,
  "detail": "A retention policy with this name already exists in the repository.",
  "instance": "/api/admin/retention/policies",
  "request_id": "0e2d1a3c-...-...",
  "error": "A retention policy with this name already exists in the repository."
}
FieldRFC 7807?Meaning
typeA URI identifying the problem type. Always about:blank today — no handler defines a more specific type yet, so treat status + error as the machine-readable pair instead.
titleThe standard HTTP status phrase (http.StatusText(status)), not customized per error.
statusHTTP status code, duplicated from the response line for JSON-only consumers.
detailHuman-readable message. Safe to render directly in UI.
instanceThe request path that produced the error.
request_idExtensionCorrelates the response with server-side logs/traces. Include it when filing a bug.
errorExtensionMirrors detail for the pre-RFC-7807 {"error": "..."} shape older call sites relied on. A small number of handlers (e.g. slug validation, CVE-policy promotion blocks — see below) instead set error to a stable machine-readable code so a CLI or the Terraform provider can switch on it without parsing English.

Any additional top-level members beyond this list are handler-specific extensions, flattened onto the document by the same writer. Two examples already in the codebase:

  • Slug validation (internal/handlers/slug.go) — 400s carry error: "invalid_slug" plus field, message, examples, and an optional suggestion. See Naming conventions for the grammar these errors are validating against.
  • CVE-policy promotion block (internal/handlers/promotions.go) — 409s carry error: "cve_policy_block" plus reason and hits (the blocking finding rows).

Don't rely on the presence of any extension member beyond request_id unless you already know the specific endpoint sets it — most handlers call the plain Write(w, status, detail) helper and emit only the fields in the table above.

Client-side parsing

The frontend's shared parser (frontend/src/api/problem.ts, parseErrorResponse) reads a failed fetch response and returns an ApiError whose .message is detail (falling back to title, then the raw body, then the status line) — so existing String(error) / error.message call sites read as a sentence instead of a bare status code. It's wired into frontend/src/api/client.ts's central fetcher and every direct fetch() call site under frontend/src/api/ and the admin form-submit pages, so any SWR hook's error already carries this shape.

ts
import { parseErrorResponse } from './problem';

const res = await fetch('/api/v1/repos', { method: 'POST', body });
if (!res.ok) {
  throw await parseErrorResponse(res);
}

What's still the raw wire-protocol shape

AdminApiReference.tsx (the in-app API playground) intentionally shows the raw response body regardless of shape, and Login.tsx's 401/429 paths keep their own hardcoded copy — neither goes through parseErrorResponse. Outside internal/handlers, the per-format adapters and Docker/OCI push/pull paths also don't use this envelope; see their own protocol's spec for the error body they return.

Released under the Apache-2.0 License.