Skip to content

Errors

Every error Kavo raises is an RFC 9457 problem-details document (ADR-0009), Content-Type: application/problem+json:

json
{
  "type": "https://kavo.dev/errors/kavo-not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Book with id 999 was not found.",
  "instance": "urn:kavo:request:a1b2c3d4",
  "code": "KAVO_NOT_FOUND"
}

code is stable API surface. Renaming one is a breaking change. type is https://kavo.dev/errors/<kebab-code>. instance is urn:kavo:request:<correlationId>, so you can correlate a single request's errors in your logs.

This shape is the same across REST, GraphQL, and MCP, because every binding runs the same engine and the same error handler. A NotFoundException looks the same no matter which protocol raised it. Over MCP it arrives as an isError: true tool result carrying ${code}: ${detail} as text, rather than an HTTP response. See MCP.

A query-validation failure additionally carries an errors[] array, so a client can fix every problem with a request in one round trip:

json
{
  "code": "KAVO_QUERY_INVALID",
  "status": 400,
  "errors": [{ "code": "KAVO_QUERY_INVALID_FIELD", "detail": "'nickname' is not a filterable field." }]
}

Full code catalog

CodeHTTPFires when
KAVO_QUERY_INVALID400Any query grammar/allowlist/limit violation (aggregate; carries errors[])
KAVO_QUERY_INVALID_FIELD400Field not on the filter/sort/select allowlist
KAVO_QUERY_INVALID_OPERATOR400Unknown or misspelled wire operator
KAVO_QUERY_INVALID_VALUE400Coercion failure, malformed bounds, bad pagination value
KAVO_QUERY_LIMIT_EXCEEDED400filter.limits.maxDepth / filter.limits.maxInValues exceeded
KAVO_QUERY_UNSUPPORTED_PARAM400withDeleted/onlyDeleted on a hard-delete entity; include with no include resolver wired
KAVO_QUERY_CONFLICTING_PARAMS400withDeleted=true and onlyDeleted=true set together
KAVO_ARRAY_MUTATION_INVALID_SHAPE400A relation-replace body isn't an array of ids/{id} refs, or null
KAVO_JSON_PATCH_INVALID_DOCUMENT400patchOne array body isn't a well-formed RFC 6902 document within Kavo's subset
KAVO_FORBIDDEN403A resolved policy (operation, entity, or global scope) returned false for the request; also raisable by a custom handler
KAVO_NOT_FOUND404Target row missing on findOne/updateOne/patchOne/deleteOne
KAVO_JSON_PATCH_TARGET_NOT_FOUND404A jsonPatch remove op names a relation member id that isn't currently associated
KAVO_CONFLICT409Unique violation, or an FK violation blocking a delete (row still referenced by children)
KAVO_ALREADY_DELETED409Soft-deleting an already-deleted row
KAVO_NOT_DELETED409Restoring or purging a row that isn't deleted
KAVO_PRECONDITION_FAILED412If-Match names no tag matching the target's current ETag
KAVO_PRECONDITION_UNSUPPORTED412If-Match on a request the engine can't evaluate it for: untargeted operation, cache.etag off, findOne disabled
KAVO_OPERATION_DISABLED405Programmatic call to a disabled registry entry (no route exists over HTTP)
KAVO_OPERATION_NOT_REGISTERED405Programmatic call naming an operation id the registry has no entry for
KAVO_UNRESOLVED_RELATION422A create/update whose payload references a related row that doesn't exist (dangling foreign key)
KAVO_BULK_FAILED422Reserved, bulk operations aren't implemented
KAVO_PERSISTENCE_FAILED500Unrecognized adapter/driver error
KAVO_TRANSACTION_FAILED500Deadlock/serialization failure (carries a retryable flag)
KAVO_CONFIG_INVALID500A bootstrap config error, or a request-time refusal when a handler returns a shape the response can't project
KAVO_PAGINATION_NOT_ADVANCING500A cursor page produced the token it was given, so following meta.nextCursor would loop forever (data- or adapter-dependent)
KAVO_HTTP_ERROR*A framework-level HttpException reaching the filter without going through Kavo's engine at all
KAVO_UNEXPECTED_ERROR500Any other error reaching the filter without going through Kavo's engine

*: KAVO_HTTP_ERROR carries whatever status the underlying framework exception already had. It's the one code whose status legitimately varies.

Exposing internal detail

Driver-level detail, like raw SQL error text and stack info, never leaks into detail unless errors.exposeInternals is turned on. Keep it off in production. See Reference/Config keys §errors.

Where these come from

Every code above maps to exactly one exception class in one hierarchy: KavoException and its leaves (NotFoundException, ConflictException, ConfigurationException, and so on). Application code raising its own errors from a custom operation handler throws these same classes. Anything else that escapes to the boundary is wrapped as KAVO_HTTP_ERROR or KAVO_UNEXPECTED_ERROR, so a framework-shaped error body never leaks out.

See Error handling for the full hierarchy and the adapter-level mapping tables from raw driver errors to catalog codes. See Guides/Error handling for handling these as a caller.