Skip to content

Latest commit

 

History

History
545 lines (427 loc) · 21 KB

File metadata and controls

545 lines (427 loc) · 21 KB
title Error Code Catalog
description Complete reference for all ObjectStack error codes with causes, fixes, and retry strategies

Error Code Catalog

ObjectStack uses a structured error system with 9 error categories and 53 standardized error codes. Every error includes a machine-readable code, HTTP status mapping, and retry guidance.

**Source:** `packages/spec/src/api/errors.zod.ts` **Import:** `import { StandardErrorCode, ErrorCategory, ErrorResponseSchema } from '@objectstack/spec/api'` **Spec vs. wire format:** The codes below are `StandardErrorCode` — the standardized, lowercase snake_case contract that plugin/hook authors should throw with (see [Server-Side Error Handling](/docs/api/error-handling-server)). The kernel REST server (`@objectstack/rest`) that serves `/api/v1/data/*` today emits a flatter envelope with SCREAMING_SNAKE_CASE codes instead — e.g. `VALIDATION_FAILED`, `PERMISSION_DENIED`, `RECORD_NOT_FOUND`, `CONCURRENT_UPDATE`, `ERR_DATASOURCE_UNAVAILABLE` — not the codes in this catalog. See the [API Overview](/docs/api#error-handling) for both wire formats in use and [Wire Format](/docs/api/wire-format#7-error-response-format) for JSON examples.

Reconciling the two vocabularies is #3841, still open. In the meantime the runtime dispatcher emits whichever code its producer already used (PERMISSION_DENIED, VALIDATION_FAILED, unauthenticated, …) and falls back to a StandardErrorCode from this catalog when a branch has no code of its own — HttpStatusErrorCodeMap in @objectstack/spec/api is the single place that mapping is spelled.


Error Categories

Category HTTP Status Description
validation 400 Request data failed validation
authentication 401 Identity not verified
authorization 403 Insufficient permissions
not_found 404 Resource does not exist
conflict 409 State conflict or duplicate
rate_limit 429 Too many requests
server 500 Internal server error
external 502 External service failure
maintenance 503 System under maintenance

Retry Strategies

Strategy Description When to Use
no_retry Do not retry the request Validation errors, permission denied
retry_immediate Retry immediately Transient network errors
retry_backoff Retry with exponential backoff Rate limits, server errors
retry_after Wait the specified retryAfter seconds Rate limit with explicit cooldown

Validation Errors (400)

validation_error

Cause: Generic validation failure — request body does not match expected schema.
Fix: Check the fieldErrors array for specific field-level issues.
Retry: no_retry

{
  "code": "validation_error",
  "message": "Request validation failed",
  "category": "validation",
  "httpStatus": 400,
  "retryable": false,
  "fieldErrors": [
    { "field": "email", "message": "Invalid email format", "code": "invalid_format" }
  ]
}

invalid_field

Cause: A field name in the request does not exist on the target object.
Fix: Check the object schema for valid field names. Use os meta get object <name> to inspect the object's fields.
Retry: no_retry

missing_required_field

Cause: A required field was not provided in the request body.
Fix: Include the missing field. Check fieldErrors for the field name.
Retry: no_retry

invalid_format

Cause: Field value does not match the expected format (e.g., invalid email, wrong date format).
Fix: Ensure the value matches the field's format constraint or built-in type validation.
Retry: no_retry

value_too_long

Cause: String value exceeds the field's maxLength constraint.
Fix: Truncate the value to the maximum allowed length.
Retry: no_retry

value_too_short

Cause: String value is below the field's minLength constraint.
Fix: Provide a longer value that meets the minimum length requirement.
Retry: no_retry

value_out_of_range

Cause: Numeric value is outside the field's min/max range.
Fix: Ensure the value is within the allowed range.
Retry: no_retry

invalid_reference

Cause: A lookup or master_detail field references a record that does not exist.
Fix: Verify the referenced record ID exists in the target object.
Retry: no_retry

duplicate_value

Cause: A field with unique: true already has a record with the same value.
Fix: Use a different value or update the existing record.
Retry: no_retry

invalid_query

Cause: The query structure is malformed.
Fix: Check the query syntax against the Query Cheat Sheet.
Retry: no_retry

invalid_filter

Cause: A filter operator or field in the where clause is invalid.
Fix: Use valid filter operators ($eq, $ne, $gt, $lt, $in, $contains, etc.).
Retry: no_retry

invalid_sort

Cause: The orderBy clause references a non-existent or non-sortable field.
Fix: Ensure the field exists and has sortable: true.
Retry: no_retry

max_records_exceeded

Cause: The request attempts to create or return more records than the system limit.
Fix: Use pagination (limit/offset or cursor-based) for large datasets. Batch operations for bulk creates.
Retry: no_retry


Authentication Errors (401)

unauthenticated

Cause: No authentication credentials were provided.
Fix: Include a valid Authorization header (Bearer token, API key, or session cookie).
Retry: no_retry

invalid_credentials

Cause: Username/password combination is incorrect.
Fix: Verify credentials and try again. After multiple failures, account may be locked.
Retry: no_retry

expired_token

Cause: The authentication token (JWT, session token) has expired.
Fix: Obtain a new token using the refresh token endpoint or re-authenticate.
Retry: retry_immediate (after refreshing token)

invalid_token

Cause: The authentication token is malformed or has been tampered with.
Fix: Obtain a fresh token from the authentication endpoint.
Retry: no_retry

session_expired

Cause: The user session has timed out due to inactivity.
Fix: Re-authenticate to start a new session.
Retry: no_retry

mfa_required

Cause: Multi-factor authentication is required but the MFA challenge has not been completed.
Fix: Complete the MFA verification step before proceeding.
Retry: no_retry

email_not_verified

Cause: The user's email address has not been verified.
Fix: Complete email verification through the link sent to the user's email.
Retry: no_retry


Authorization Errors (403)

permission_denied

Cause: The authenticated user does not have permission for this operation.
Fix: Request the necessary permissions from an administrator, or use an account with the required role.
Retry: no_retry

insufficient_privileges

Cause: The user has some access but lacks the specific privilege required for this action.
Fix: Contact an administrator to grant the necessary privilege.
Retry: no_retry

field_not_accessible

Cause: The user does not have access to a specific field on the object.
Fix: Remove the inaccessible field from the request, or request field-level access.
Retry: no_retry

record_not_accessible

Cause: The user cannot access this specific record due to record-level sharing rules.
Fix: Request access to the record from its owner or an administrator.
Retry: no_retry

license_required

Cause: The feature requires a specific license that is not active.
Fix: Upgrade the license or contact your administrator.
Retry: no_retry

ip_restricted

Cause: The request originates from an IP address that is not in the allowlist.
Fix: Ensure the request comes from an approved IP range, or update the IP restriction settings.
Retry: no_retry

time_restricted

Cause: Access is limited to specific time windows and the current time is outside the allowed range.
Fix: Retry during the allowed time window.
Retry: retry_after


Not Found Errors (404)

resource_not_found

Cause: The requested resource does not exist (generic).
Fix: Verify the resource identifier and endpoint path.
Retry: no_retry

object_not_found

Cause: The specified object (table/entity) does not exist in the schema.
Fix: Check the object name. Use os meta list object to list available objects.
Retry: no_retry

record_not_found

Cause: No record exists with the given ID in the specified object.
Fix: Verify the record ID. The record may have been deleted.
Retry: no_retry

field_not_found

Cause: The specified field does not exist on the object.
Fix: Check the field name against the object schema. Field names are snake_case.
Retry: no_retry

endpoint_not_found

Cause: The API endpoint path does not match any registered route.
Fix: Verify the URL path and HTTP method. Check the API discovery endpoint for available routes.
Retry: no_retry


Conflict Errors (409)

resource_conflict

Cause: The operation conflicts with the current state of the resource.
Fix: Fetch the latest resource state and retry the operation.
Retry: retry_immediate

concurrent_modification

Cause: The record was modified by another user/process since you last read it (optimistic locking failure).
Fix: Re-fetch the record, merge changes, and retry.
Retry: retry_immediate

delete_restricted

Cause: The record cannot be deleted because other records depend on it (referential integrity).
Fix: Delete or reassign dependent records first, then retry the delete.
Retry: no_retry

duplicate_record

Cause: A record with the same unique key already exists.
Fix: Update the existing record instead, or use a different unique key value.
Retry: no_retry

lock_conflict

Cause: The record is locked by another process or user.
Fix: Wait for the lock to be released, or contact the lock holder.
Retry: retry_backoff


Request Errors (405/428)

Added in #3842 so standardErrorCodeForHttpStatus can name every status the runtime actually returns. Without them a 405 would fall into the generic 4xx bucket and be reported as a validation failure.

method_not_allowed

Cause: The route exists but does not serve this HTTP method.
Fix: Use the method named in the response's Allow header.
Retry: no_retry

precondition_required

Cause: The request is missing a precondition the route requires — most often an environment scope (no X-Environment-Id header and no hostname mapping).
Fix: Send the missing header, or address the environment-scoped URL form.
Retry: no_retry


Rate Limit Errors (429)

rate_limit_exceeded

Cause: Too many requests in the current time window.
Fix: Reduce request frequency. Check the retryAfter field for the wait time.
Retry: retry_after

quota_exceeded

Cause: The API usage quota for the current period has been exhausted.
Fix: Wait for the quota to reset (check retryAfter), or upgrade the plan.
Retry: retry_after

concurrent_limit_exceeded

Cause: Too many concurrent requests from the same client.
Fix: Reduce parallel request count. Implement request queuing.
Retry: retry_backoff


Server Errors (500)

internal_error

Cause: An unexpected server-side error occurred.
Fix: Report the issue with the requestId and traceId. Retry may succeed.
Retry: retry_backoff

database_error

Cause: A database operation failed (connection, query, constraint).
Fix: Report the issue. May be transient — retry with backoff.
Retry: retry_backoff

timeout

Cause: The operation exceeded the maximum allowed time.
Fix: Simplify the query/operation. For large datasets, use pagination or async processing.
Retry: retry_backoff

service_unavailable

Cause: A required internal service is temporarily unavailable.
Fix: Retry after a brief delay. Check system status.
Retry: retry_backoff

not_implemented

Cause: The requested feature or endpoint is not yet implemented.
Fix: Check the documentation for alternative approaches, or wait for the feature release.
Retry: no_retry


External Service Errors (502)

external_service_error

Cause: An external API or service returned an error.
Fix: Check the external service status. The details field may contain the upstream error.
Retry: retry_backoff

integration_error

Cause: An integration connector encountered an error.
Fix: Verify the integration configuration and credentials.
Retry: retry_backoff

webhook_delivery_failed

Cause: A webhook delivery attempt failed (target unreachable or returned error).
Fix: Verify the webhook URL is accessible. The system will auto-retry per delivery policy.
Retry: retry_backoff


Batch Operation Errors

batch_partial_failure

Cause: Some operations in a batch request succeeded while others failed.
Fix: Check the details for individual operation results. Retry only the failed operations.
Retry: retry_immediate (failed operations only)

batch_complete_failure

Cause: All operations in the batch request failed.
Fix: Check the details for root cause. Fix and retry the entire batch.
Retry: retry_backoff

transaction_failed

Cause: A database transaction failed and was rolled back.
Fix: Check the details for the specific failure. Retry the entire transaction.
Retry: retry_backoff


Action Errors (/api/v1/actions)

**`/actions` is the one route where a failure is not always a non-2xx.** An action that runs and *rejects* is a business outcome, so it is reported inside the payload at HTTP 200. Only failures where no handler produced that outcome carry a status. Branch on both.
What happened HTTP Body
Action ran, returned 200 { success: true, data: { success: true, data } }
Action ran, rejected (business rule, validation) 200 { success: true, data: { success: false, error, code?, fields? } }
No such action registered 404 { success: false, error: { message, code } }
Caller lacks requiredPermissions 403 { success: false, error: { message, code } }
Type has no server dispatch (url/modal/form/api), or a param-contract violation 400 { success: false, error: { message, code } }
Data engine or automation service unavailable 503 { success: false, error: { message, code } }
Handler crashed (TypeError, driver error, sandbox timeout) 500 { success: false, error: { message, code } }

A rejection and a crash are told apart by the thrown error: a plain throw new Error(msg), a sandboxed body's deliberate throw, or a ValidationError is a rejection; a TypeError / ReferenceError / a driver's own error class is a crash. An error carrying its own status is served with it.

const res = await fetch(`/api/v1/actions/${object}/${action}`, { /* … */ });
const json = await res.json();

// BOTH checks are required — neither one alone is sufficient.
if (!res.ok) throw new Error(json.error?.message);             // never dispatched, or crashed
if (json.data?.success === false) showToast(json.data.error);  // ran and rejected

Using @objectstack/client removes the footgun: client.actions.invoke() folds both shapes into one result and never throws.

const res = await client.actions.invoke(object, action, { recordId, params });
if (!res.success) showToast(res.error);

Error Response Structure

Every error response follows the EnhancedApiError schema:

interface EnhancedApiError {
  code: StandardErrorCode;       // Machine-readable error code
  message: string;               // Human-readable description
  category?: ErrorCategory;      // Error category
  httpStatus?: number;           // HTTP status code
  retryable: boolean;            // Whether retry may succeed
  retryStrategy?: RetryStrategy; // Recommended retry approach
  retryAfter?: number;           // Seconds to wait (for rate limits)
  details?: unknown;             // Additional error context
  fieldErrors?: FieldError[];    // Field-specific validation errors
  timestamp?: string;            // ISO 8601 timestamp
  requestId?: string;            // Request tracking ID
  traceId?: string;              // Distributed trace ID
  documentation?: string;        // URL to error documentation
  helpText?: string;             // Suggested resolution actions
}

Field Error Structure

interface FieldError {
  field: string;            // Field path (supports dot notation)
  code: StandardErrorCode;  // Field-level error code
  message: string;          // Human-readable error for this field
  value?: unknown;          // The invalid value (if safe to include)
  constraint?: unknown;     // The constraint that was violated (e.g., max length)
}

Client-Side Error Handling

The `@objectstack/client` SDK's built-in fetch error handling attaches `code`, `category`, `httpStatus`, `retryable`, `details`, and — for validation failures — `fields`. It does **not** attach `retryAfter` or `requestId` directly; if your server populates those on the response body, read them from `apiError.details` until the client surfaces them at the top level.

Two naming details matter here:

  • The per-field list is apiError.fields, matching the wire (fields[]), not the spec contract's fieldErrors. Each entry is { field, code, message }. It is left unset when the server reported no per-field detail, so if (apiError.fields) tests "this failure is field-anchored".
  • apiError.code is always the semantic code as a string. The numeric HTTP status is on apiError.httpStatus and nowhere else. This holds for both wire formats: the flat REST envelope carries the code at the top level and the runtime dispatcher's wrapped envelope carries it in error.code, with the status alongside it in error.httpStatus.

TypeScript Example

import type { EnhancedApiError, FieldError } from '@objectstack/spec/api';

// The spec type names the per-field list `fieldErrors`; what the client
// actually attaches at runtime is `fields` (the wire name). Widen the cast
// until the two names converge.
type ThrownApiError = EnhancedApiError & { fields?: FieldError[] };

async function handleApiCall() {
  try {
    const result = await client.data.create('task', { title: 'New Task' });
    return result;
  } catch (error) {
    const apiError = error as ThrownApiError;

    switch (apiError.category) {
      case 'validation':
        // Show field-level errors to the user. `fields` is what the client
        // attaches (the wire name); the spec type calls the same idea
        // `fieldErrors`.
        apiError.fields?.forEach(fe => {
          showFieldError(fe.field, fe.message);
        });
        break;

      case 'authentication':
        // Redirect to login
        if (apiError.code === 'expired_token') {
          await refreshToken();
          return handleApiCall(); // Retry
        }
        redirectToLogin();
        break;

      case 'rate_limit':
        // Wait and retry
        const waitTime = apiError.retryAfter ?? 60;
        await sleep(waitTime * 1000);
        return handleApiCall();

      case 'server':
      case 'external':
        // Log and retry with backoff
        console.error(`Server error [${apiError.requestId}]:`, apiError.message);
        break;

      default:
        showGenericError(apiError.message);
    }
  }
}

HTTP Status Quick Reference

Status Category Common Codes
400 validation validation_error, invalid_field, missing_required_field, invalid_query
401 authentication unauthenticated, expired_token, invalid_credentials
403 authorization permission_denied, field_not_accessible, license_required
404 not_found record_not_found, object_not_found, endpoint_not_found
409 conflict concurrent_modification, duplicate_record, delete_restricted
429 rate_limit rate_limit_exceeded, quota_exceeded
500 server internal_error, database_error, timeout
502 external external_service_error, integration_error
503 maintenance service_unavailable