Error Code Reference¶
SynthOrg's REST API emits RFC 9457 Problem Details responses on every error path. Every response carries three machine-readable fields that clients can discriminate on:
type: a stable URI describing the error categoryerror_code: an integer in one of the ranges belowerror_category: the category name matching the URI slug
Clients should dispatch on error_code (most specific) and fall back to error_category for generic handling. Messages and titles are human-readable and may change without notice; the code is the contract.
Category URIs¶
| Category | type URI |
Code range |
|---|---|---|
| Authentication / authorization | https://synthorg.io/docs/errors#auth |
1000-1999 |
| Request validation | https://synthorg.io/docs/errors#validation |
2000-2999 |
| Resource not found | https://synthorg.io/docs/errors#not_found |
3000-3999 |
| Conflict / duplicate | https://synthorg.io/docs/errors#conflict |
4000-4999 |
| Rate limit / concurrency | https://synthorg.io/docs/errors#rate_limit |
5000-5999 |
| Budget exhausted | https://synthorg.io/docs/errors#budget_exhausted |
6000-6999 |
| Provider / integration failure | https://synthorg.io/docs/errors#provider_error |
7000-7999 |
| Internal / service unavailable | https://synthorg.io/docs/errors#internal |
8000-8999 |
Authentication (1xxx)¶
| Code | Name | When |
|---|---|---|
| 1000 | UNAUTHORIZED |
Missing or invalid session |
| 1001 | FORBIDDEN |
Authenticated but not permitted |
| 1002 | SESSION_REVOKED |
Session revoked by operator or user |
| 1003 | ACCOUNT_LOCKED |
Too many failed login attempts |
| 1004 | CSRF_REJECTED |
CSRF double-submit failed |
| 1005 | REFRESH_TOKEN_INVALID |
Refresh rotation mismatch or expired |
| 1006 | SESSION_LIMIT_EXCEEDED |
Per-user session cap reached |
| 1007 | TOOL_PERMISSION_DENIED |
Agent not permitted to invoke the tool |
| 1008 | SESSION_NO_TOKEN |
No session cookie or bearer token on the request |
| 1009 | SESSION_EXPIRED |
Session cookie / JWT decoded but past expiry |
Validation (2xxx)¶
| Code | Name | When |
|---|---|---|
| 2000 | VALIDATION_ERROR |
Generic validation failure |
| 2001 | REQUEST_VALIDATION_ERROR |
Litestar-parsed body/params rejected |
| 2002 | ARTIFACT_TOO_LARGE |
Upload exceeds artifact.max_bytes |
| 2003 | TOOL_PARAMETER_ERROR |
Tool parameters failed schema validation |
| 2004 | PROVIDER_TIER_COVERAGE_INSUFFICIENT |
Setup wizard cannot apply a template because no configured provider exposes any models |
| 2005 | IMMUTABLE_FIELD_MISMATCH |
A restore/rollback would change an immutable field (e.g. agent id/name/department) |
| 2006 | CHECKPOINT_ROLLBACK_UNAVAILABLE |
Fine-tune checkpoint rollback target is missing or unusable |
| 2007 | CHECKPOINT_ROLLBACK_CORRUPT |
Fine-tune checkpoint rollback backup data is corrupt |
Not Found (3xxx)¶
The NotFound hierarchy is rooted at NotFoundError. Each resource has a dedicated subclass that pins the right ErrorCode as a ClassVar (for example TaskNotFoundError in synthorg.engine.errors, ConnectionNotFoundError in synthorg.integrations.errors, and ResourceNotFoundError in synthorg.core.domain_errors as the 3000 fallback). Controllers raise the subclass directly, or via require_resource_or_404(..., error_class=...), so the wire error_code is fixed by the class rather than a runtime argument.
| Code | Name | Resource |
|---|---|---|
| 3000 | RESOURCE_NOT_FOUND |
Fallback: the resource type isn't in the table below |
| 3001 | RECORD_NOT_FOUND |
Generic DB row not found |
| 3002 | ROUTE_NOT_FOUND |
HTTP path had no handler |
| 3003 | PROJECT_NOT_FOUND |
Project |
| 3004 | TASK_NOT_FOUND |
Task |
| 3005 | SUBWORKFLOW_NOT_FOUND |
Sub-workflow definition |
| 3006 | WORKFLOW_EXECUTION_NOT_FOUND |
Workflow execution record |
| 3007 | CHANNEL_NOT_FOUND |
Communication channel |
| 3008 | TOOL_NOT_FOUND |
Registered tool |
| 3009 | ONTOLOGY_NOT_FOUND |
Ontology entry |
| 3010 | CONNECTION_NOT_FOUND |
Integration connection |
| 3011 | MODEL_NOT_FOUND |
Provider model |
| 3012 | ESCALATION_NOT_FOUND |
Escalation queue entry |
| 3013 | WORKFLOW_DEFINITION_NOT_FOUND |
Workflow definition record |
| 3014 | AB_TEST_NOT_FOUND |
A/B test record for a proposal |
| 3015 | BACKUP_NOT_FOUND |
Backup archive |
| 3016 | MEMORY_ENTRY_NOT_FOUND |
Agent memory entry |
| 3017 | CONVERSATION_NOT_FOUND |
Conversation record |
All share the same type URI; the numeric code is the discriminator.
Conflict (4xxx)¶
| Code | Name | When |
|---|---|---|
| 4000 | RESOURCE_CONFLICT |
Generic 409 (resource state mismatch) |
| 4001 | DUPLICATE_RECORD |
Unique-constraint violation |
| 4002 | VERSION_CONFLICT |
Optimistic-concurrency (ETag) mismatch |
| 4003 | TASK_VERSION_CONFLICT |
Same, scoped to a task update |
| 4004 | ONTOLOGY_DUPLICATE |
Duplicate ontology entity or alias |
| 4005 | CHANNEL_ALREADY_EXISTS |
Channel name already taken |
| 4006 | ESCALATION_ALREADY_DECIDED |
Late decision on a closed escalation |
| 4007 | MIXED_CURRENCY_AGGREGATION |
Cross-currency aggregation attempted |
| 4008 | WORKFLOW_EXECUTION_ALREADY_TERMINAL |
Cancel hit an execution already in a terminal status (no retry will succeed) |
| 4009 | BACKUP_IN_PROGRESS |
A backup/restore operation is already running |
| 4010 | CHECKPOINT_OPERATION_CONFLICT |
Checkpoint deploy/delete rejected (e.g. active checkpoint) |
| 4011 | FINE_TUNE_RUN_ACTIVE |
A fine-tune run is already active (start/resume blocked) |
| 4012 | TRAINING_PLAN_NOT_MODIFIABLE |
Training plan cannot be modified after execution or failure |
| 4013 | BACKUP_UNRESTARTABLE |
Backup service stopped in an unrestartable state |
| 4014 | AGENT_RUNTIME_NOT_CONFIGURED |
No LLM provider configured; agent runtime cannot execute |
| 4015 | CONVERSATION_CLOSED |
Conversation is closed; no further messages or actions accepted |
| 4016 | PROJECT_WORKSPACE_NOT_PROVISIONED |
Project workspace required but never provisioned by the git backend |
| 4017 | LIVING_DOC_VERSION_CONFLICT |
Living-doc write lost an optimistic-concurrency race |
| 4018 | ENVIRONMENT_BACKEND_UNAVAILABLE |
Declaration needs a sandbox backend that is not active (e.g. devcontainer on the subprocess backend) |
Rate Limit (5xxx)¶
| Code | Name | When |
|---|---|---|
| 5000 | RATE_LIMITED |
Global per-user / per-IP throttle tripped |
| 5001 | PER_OPERATION_RATE_LIMITED |
Specific operation's (max_requests, window) budget exhausted |
| 5002 | CONCURRENCY_LIMIT_EXCEEDED |
Too many in-flight requests for the op |
Budget Exhausted (6xxx)¶
| Code | Name | When |
|---|---|---|
| 6000 | BUDGET_EXHAUSTED |
Company-level budget hard stop |
| 6001 | DAILY_LIMIT_EXCEEDED |
Prorated daily cap tripped |
| 6002 | RISK_BUDGET_EXHAUSTED |
Per-risk-tier budget exceeded |
| 6003 | PROJECT_BUDGET_EXHAUSTED |
Project-scoped budget hard stop |
| 6004 | QUOTA_EXHAUSTED |
Metered feature quota reached |
Provider / Integration (7xxx)¶
| Code | Name | When |
|---|---|---|
| 7000 | PROVIDER_ERROR |
Generic upstream failure |
| 7001 | PROVIDER_TIMEOUT |
Upstream timed out |
| 7002 | PROVIDER_CONNECTION |
Network-level failure |
| 7003 | PROVIDER_INTERNAL |
Provider returned 5xx |
| 7004 | PROVIDER_AUTHENTICATION_FAILED |
Invalid credentials |
| 7005 | PROVIDER_INVALID_REQUEST |
Provider rejected the request |
| 7006 | PROVIDER_CONTENT_FILTERED |
Provider filtered the content |
| 7007 | INTEGRATION_ERROR |
Non-LLM integration failure |
| 7008 | OAUTH_ERROR |
OAuth exchange failed |
| 7009 | WEBHOOK_ERROR |
Webhook receive/replay failure |
| 7010 | CONVERSATIONAL_PROPOSE_RESPONSE_INVALID |
Chief-of-Staff proposer returned an invalid response |
Internal (8xxx)¶
| Code | Name | When |
|---|---|---|
| 8000 | INTERNAL_ERROR |
Unspecified server error |
| 8001 | SERVICE_UNAVAILABLE |
Dependent service not wired yet |
| 8002 | PERSISTENCE_ERROR |
DB-level failure |
| 8003 | ENGINE_ERROR |
Engine-layer failure |
| 8004 | ONTOLOGY_ERROR |
Ontology subsystem failure |
| 8005 | COMMUNICATION_ERROR |
Meeting/message bus failure |
| 8006 | TOOL_ERROR |
Generic tool failure |
| 8007 | ARTIFACT_STORAGE_FULL |
Artifact store at capacity |
| 8008 | TOOL_EXECUTION_ERROR |
Tool runtime failure (subclass of TOOL_ERROR) |
| 8009 | FEATURE_NOT_IMPLEMENTED |
Active backend or deployment fundamentally does not implement the requested operation (501) |
| 8010 | ARTIFACT_NO_STORAGE_BACKEND |
Artifact service was constructed without a storage backend; controller-helper misconfiguration |
| 8011 | AGENT_IDENTITY_ROLLBACK_FAILED |
Unexpected server failure during agent-identity rollback |
| 8012 | BACKUP_RESTORE_FAILED |
Restore operation failed (non-recoverable backend error) |
| 8013 | BACKUP_MANIFEST_ERROR |
Backup manifest could not be parsed or validated |
| 8014 | SETTINGS_ENCRYPTION_ERROR |
Internal error processing a sensitive (encrypted) setting |
| 8015 | SINK_CONFIG_VALIDATION_ERROR |
Internal error validating an observability sink configuration |
| 8016 | WORKER_DEAD_LETTER_ERROR |
Worker dead-letter handling failed |
| 8017 | LIVING_DOC_INDEX_ERROR |
Living-doc RAG index operation failed |
| 8018 | LIVING_DOC_COMMIT_ERROR |
Living-doc commit to the workspace failed |
| 8019 | KNOWLEDGE_INGEST_ERROR |
Knowledge-source ingestion failed |
| 8020 | KNOWLEDGE_RETRIEVAL_ERROR |
Knowledge retrieval failed |
| 8021 | KNOWLEDGE_DEPENDENCY_ERROR |
Knowledge subsystem dependency unavailable |
| 8022 | KNOWLEDGE_SOURCE_UNAVAILABLE |
Requested knowledge source is unavailable |
| 8023 | ENVIRONMENT_ERROR |
Generic reproducible-environment failure |
| 8024 | ENVIRONMENT_PROVISION_FAILED |
Environment setup/provisioning failed |
| 8025 | ENVIRONMENT_DOCKER_BUILD_FAILED |
Devcontainer image build failed |
Content negotiation¶
Clients that set Accept: application/problem+json receive a bare RFC 9457 body. Clients that accept application/json receive an ApiResponse envelope with error_detail carrying the same fields. See the API reference for per-route examples.
HTTP exception handler registration (MANDATORY)¶
Litestar resolves exception handlers by walking the raised exception's
MRO; the first matching type in EXCEPTION_HANDLERS
(src/synthorg/api/exception_handlers.py) wins. Domain error families
register a single base-class handler (e.g. BackupError,
PersistenceError, OntologyError) so every subtype maps to a
structured response without falling through to the catch-all
Exception: handle_unexpected (which would surface as a generic 500
without a domain-specific error_code).
Controllers MUST NOT catch a domain error and build their own
Response(...) envelope. Raise the typed domain error (declaring
status_code / error_code / error_category ClassVars on the
class) and let handle_domain_error produce the RFC 9457 envelope.
Enforced at pre-push by
scripts/check_no_controller_response_for_domain_errors.py.
Per-line opt-out:
# lint-allow: controller-domain-response -- <reason>.
Each handler:
- Calls
_log_error(request, exc, status=...)for structured logging (WARNING for 4xx, ERROR + traceback for 5xx). - Returns
_build_response(...)so the response carries the full RFC 9457 envelope (or bareapplication/problem+jsonbody when the client asks for it). - Scrubs the upstream message on 5xx. 4xx behaviour varies: domain
handlers like
handle_backup_errorandhandle_domain_errorpass a user-safe exception message through, while several Litestar-side handlers intentionally return fixed public messages (handle_record_not_found->"Resource not found",handle_not_authorized->"Authentication required",handle_permission_denied->"Forbidden",handle_not_found->"Not found"). When in doubt, mirror the nearest existing handler inexception_handlers.py.
When introducing a new domain error family:
- Add the base class to
EXCEPTION_HANDLERSmapped to a dedicatedhandle_<domain>_errorfunction. - Use
isinstancedispatch inside the handler to map subtypes to specific HTTP status codes (404,409, etc.) before falling through to the structured500branch. - Register the entry above the catch-all
Exception: handle_unexpectedline in the dict; MRO ordering does not depend on dict insertion order, but readability does. - Add tests in
tests/unit/api/test_exception_handlers.pycovering each branch and a regression test for the catch-all.
Domain-error-hierarchy gate¶
scripts/check_domain_error_hierarchy.py enforces the rule at pre-push
and in CI: every class definition under src/synthorg/ whose direct
base is one of Exception / RuntimeError / LookupError /
PermissionError / ValueError / TypeError / KeyError /
IndexError / AttributeError / OSError / IOError is a violation
unless the class itself reaches DomainError via another base.
Only the root of a stdlib-rooted chain is flagged; migrating the root
to DomainError automatically corrects every descendant.
Per-line opt-out:
class TsaError(Exception): # lint-allow: domain-error-hierarchy -- RFC 3161 internals; observability stays stdlib-rooted
...
The justification after -- is mandatory and must be non-empty. The
gate also accepts a frozen baseline file
(scripts/domain_error_hierarchy_baseline.txt) listing pre-existing
violations a rollout has not yet reached. The baseline shrinks
monotonically: any entry that no longer maps to a real violation is
reported as drift, so the file cannot harbour stale rows.
Error-code constants - frontend integration (MANDATORY)¶
src/synthorg/core/error_taxonomy.py is the single source of truth
for ErrorCode and ErrorCategory. The dashboard imports the same
constants from web/src/api/types/error-codes.gen.ts, which is
generated from the Python enums by
scripts/generate_error_codes_ts.py.
When adding or renaming a code:
- Edit
src/synthorg/core/error_taxonomy.py. - Run
uv run python scripts/generate_error_codes_ts.pyand commitweb/src/api/types/error-codes.gen.tsalongside the Python change. - Update any frontend call sites that newly want to discriminate on
the code (import
ErrorCodefrom@/api/types/errorsand use the named member, never the raw integer).
The pre-push gate scripts/check_error_codes_ts_in_sync.py re-runs
the generator and fails if the committed error-codes.gen.ts differs
byte-for-byte from the freshly rendered output, so a backend code
addition that forgets the regeneration step cannot land.
Frontend code MUST import error codes through @/api/types/errors
(re-exported from the generated module). Inlining numeric error codes
(e.g. error_code: 3000) is forbidden -- name-based discrimination
keeps the web/ and src/synthorg/ sides in lockstep when codes are
renumbered.
Interpreter-critical exception propagation¶
MemoryError and RecursionError are subclasses of Exception, so a
broad except Exception: block silently swallows them unless the
handler propagates them explicitly. Call
synthorg.core.critical_errors.reraise_critical(exc) as the first
statement of the broad handler:
from synthorg.core.critical_errors import reraise_critical
try:
...
except Exception as exc:
reraise_critical(exc)
logger.warning(EVENT, error_type=type(exc).__name__, ...)
raise QueryError(msg) from exc
asyncio.CancelledError is not routed through this helper because
it is a BaseException, not an Exception; a broad except Exception:
block never catches it. See
Async concurrency conventions
for the surrounding pattern.
Further reading¶
- Design: security: the SEC-1 rules behind the categories
- REST API reference: per-route examples plus a worked content-negotiation walkthrough
src/synthorg/core/error_taxonomy.py: the authoritativeErrorCategory/ErrorCodeenums + RFC 9457 helperssrc/synthorg/core/domain_errors.py: theDomainErrorbase + concrete subclasses (NotFoundError,ConflictError,ValidationError, ...)src/synthorg/core/persistence_errors.py: thePersistenceErrorhierarchysrc/synthorg/core/critical_errors.py: thereraise_criticalhelper for the broad-except idiom