Quickback Docs
Quickback for Hono API

Errors

HTTP error responses and status codes returned by the generated API, organized by security layer.

The generated API returns structured error responses with consistent fields across all security layers.

Status Codes

CodeMeaningWhen
200OKSuccessful GET, PATCH, DELETE
201CreatedSuccessful POST
207Multi-StatusBatch operation with mixed results
400Bad RequestGuard violation, validation error, invalid input
401UnauthorizedMissing or expired authentication
403ForbiddenAccess denied (wrong role, condition failed) or firewall blocked it
404Not FoundRecord doesn't exist (or firewall blocked with errorMode: 'hide')
429Too Many RequestsRate limit exceeded

Error Response Format

All errors use a flat structure with contextual fields. Every structured error response also carries a request block with the HTTP method, URL, request ID, and a redacted+truncated copy of the request body — enough to triage or hand the response to an LLM agent without grepping logs:

{
  "error": "Insufficient permissions",
  "layer": "access",
  "code": "ACCESS_ROLE_REQUIRED",
  "details": {
    "required": ["hiring-manager"],
    "current": ["interviewer"]
  },
  "hint": "Contact an administrator to grant necessary permissions",
  "request": {
    "method": "POST",
    "url": "/api/v1/candidates/cnd_01H.../advance",
    "body": "{\"stage\":\"offer\"}",
    "requestId": "0c4f2c9e-..."
  }
}
FieldTypeDescription
errorstringHuman-readable error message
layerstringSecurity layer that rejected the request
codestringMachine-readable error code
detailsobjectLayer-specific context (optional)
hintstringActionable guidance for resolution (optional)
requestobjectMethod, URL, redacted body, and request ID. Auto-attached by the runtime so the error body is self-contained — bodies are capped at 4 KB and well-known sensitive keys (password, token, secret, apiKey, otp, code, authorization, …) are replaced with "[REDACTED]"

Errors by Security Layer

Authentication (401)

Missing or invalid authentication tokens.

{
  "error": "Authentication required",
  "layer": "authentication",
  "code": "AUTH_MISSING",
  "hint": "Include Authorization header with Bearer token"
}

Error codes:

CodeDescription
AUTH_MISSINGNo Authorization header provided
AUTH_INVALID_TOKENToken is malformed or invalid
AUTH_EXPIREDToken has expired
AUTH_RATE_LIMITEDToo many auth attempts

Firewall (403)

Records outside the user's firewall scope return 403 Forbidden by default with a structured error:

{
  "error": "Record not found or not accessible",
  "layer": "firewall",
  "code": "FIREWALL_NOT_FOUND",
  "hint": "Check the record ID and your organization membership"
}

Firewall filtering is transparent — the query is scoped by WHERE organizationId = ? so inaccessible records simply don't appear in results.

Error codes:

CodeDescription
FIREWALL_NOT_FOUNDRecord not found behind firewall (wrong org, soft-deleted, or doesn't exist)
FIREWALL_ORG_ISOLATIONRecord belongs to a different organization
FIREWALL_USER_ISOLATIONRecord belongs to another user
FIREWALL_SOFT_DELETEDRecord has been soft deleted

For security-hardened deployments, set errorMode: 'hide' in your firewall config to return opaque 404 Not Found responses instead. This prevents attackers from distinguishing between "record exists but you can't access it" and "record doesn't exist".

Access (403)

Access violations return 403 Forbidden when the user's role doesn't match the required roles for the operation.

{
  "error": "Insufficient permissions",
  "layer": "access",
  "code": "ACCESS_ROLE_REQUIRED",
  "details": {
    "required": ["hiring-manager"],
    "current": ["interviewer"]
  },
  "hint": "Contact an administrator to grant necessary permissions"
}

Error codes:

CodeDescription
ACCESS_ROLE_REQUIREDUser doesn't have the required role
ACCESS_CONDITION_FAILEDRecord-level access condition not met
ACCESS_OWNERSHIP_REQUIREDUser must own the record
ACCESS_NO_ORGNo active organization set

Guards (400)

Guard violations return 400 Bad Request when the request body contains fields that aren't allowed.

{
  "error": "Field cannot be set during creation",
  "layer": "guards",
  "code": "GUARD_FIELD_NOT_CREATEABLE",
  "details": {
    "fields": ["stage"]
  },
  "hint": "These fields are set automatically or must be omitted"
}

Error codes:

CodeDescription
GUARD_FIELD_NOT_CREATEABLEField not in createable list
GUARD_FIELD_NOT_UPDATABLEField not in updatable list
GUARD_FIELD_PROTECTEDField is action-only (protected)
GUARD_FIELD_IMMUTABLEField cannot be modified after creation
GUARD_SYSTEM_MANAGEDSystem field (createdAt, modifiedAt, etc.)

Masking

Masking doesn't produce errors — it silently transforms field values in the response.

Database

Surfaced when a SQL write fails post-validation — most often a NOT NULL or UNIQUE constraint hit at the storage layer (e.g. an FK target was deleted concurrently, or a unique column collided with another tenant's row). All four codes share the same layer: "database" so consumers can switch on the layer alone and treat the rest as one bucket.

{
  "error": "Duplicate value",
  "layer": "database",
  "code": "DB_UNIQUE_VIOLATION",
  "details": { "column": "email" },
  "hint": "A record with this \"email\" already exists"
}

Error codes:

CodeStatusDescription
DB_NOT_NULL_VIOLATION400Required column was null at write time
DB_UNIQUE_VIOLATION409Unique constraint hit
DB_INSERT_FAILED500INSERT failed for an unrecognized reason
DB_UPDATE_FAILED500UPDATE failed for an unrecognized reason

The details.column field is only present when the driver's error message identified the offending column (SQLite/D1 do; some PG variants don't).

Batch Errors

Batch operations can return:

  • 201 — All records succeeded
  • 207 — Partial success (some records failed)
  • 400 — Atomic mode and at least one record failed (all rolled back)

Partial Success (207)

{
  "success": [{ "id": "app_1", "candidateId": "cand_101", "stage": "applied" }],
  "errors": [
    {
      "index": 1,
      "record": { "candidateId": "cand_102", "jobId": "job_201", "stage": "interview" },
      "error": {
        "error": "Field cannot be set during creation",
        "layer": "guards",
        "code": "GUARD_FIELD_NOT_CREATEABLE",
        "details": { "fields": ["stage"] },
        "hint": "These fields are set automatically or must be omitted"
      }
    }
  ],
  "meta": { "total": 2, "succeeded": 1, "failed": 1, "atomic": false }
}

Atomic Failure (400)

{
  "error": "Batch operation failed in atomic mode",
  "layer": "validation",
  "code": "BATCH_ATOMIC_FAILED",
  "details": {
    "failedAt": 2,
    "reason": { "error": "Not found", "code": "NOT_FOUND" }
  },
  "hint": "Transaction rolled back. Fix the error and retry the entire batch."
}

Batch error codes:

CodeDescription
BATCH_SIZE_EXCEEDEDToo many records in a single request
BATCH_ATOMIC_FAILEDAtomic batch failed, all changes rolled back
BATCH_MISSING_IDSBatch update/delete missing required IDs

Action Failures (500)

When an action's execute() body throws something that isn't a known transition / state / FK / constraint failure (e.g. a TypeError, a fetch timeout, a third-party API error), the route returns ACTION_EXECUTION_FAILED. The body forwards as much of the underlying error as is safe to expose: error class name, message, cause-chain message, and the top three stack frames with absolute paths sanitized to basename + line number.

{
  "error": "Cannot read properties of undefined (reading 'getXmlFragment')",
  "layer": "action",
  "code": "ACTION_EXECUTION_FAILED",
  "details": {
    "name": "TypeError",
    "frames": [
      "at updateContent (notebooks.ts:142:31)",
      "at execute (actions.ts:88:7)"
    ]
  },
  "hint": "The action handler threw an unhandled error. Check server logs for the underlying cause.",
  "request": {
    "method": "POST",
    "url": "/api/v1/notebooks/nb_123/updateContent",
    "body": "{\"content\":\"...\"}",
    "requestId": "0c4f2c9e-..."
  }
}

Internal Errors (500)

Uncaught throws that escape every route handler land in the global onError and return INTERNAL_ERROR. The body carries the same underlying-error context as ACTION_EXECUTION_FAILED: name, cause, sanitized frames, and the request block.

{
  "error": "Connection terminated unexpectedly",
  "layer": "database",
  "code": "INTERNAL_ERROR",
  "details": {
    "name": "Error",
    "cause": "ECONNRESET",
    "frames": ["at query (db.ts:54:12)"]
  },
  "hint": "Uncaught error during request handling. Look up the requestId in server logs for the full stack trace, or set EXPOSE_ERROR_STACK=1 to include it inline.",
  "requestId": "0c4f2c9e-...",
  "request": {
    "method": "GET",
    "url": "/api/v1/jobs",
    "requestId": "0c4f2c9e-..."
  }
}

Exposing the full stack

By default the response body only carries the top three sanitized frames — full stack traces stay server-side so the worker bundle's on-disk layout doesn't leak to API clients. To include the full stack in the response (useful in dev / staging), set EXPOSE_ERROR_STACK=1 on the worker (wrangler.toml [vars], or wrangler secret put):

[vars]
EXPOSE_ERROR_STACK = "1"

When set, both the global 500 and ACTION_EXECUTION_FAILED bodies gain a details.stack field with the unsanitized stack trace.

On this page