Error-Envelope

v2-Error-Envelope (Standard seit quickfix-B) - vollständiges Schema, Fehlercodes, Request-IDs und v1-Legacy-Migration.

7 Min. Lesezeit
errorserror-envelopeerror-codes

Jede Antwort der CodeCourier-API, die nicht 2xx ist, nutzt einen strukturierten JSON-Envelope. Clients können auf error.code branchen, um zuverlässig programmatisch zu reagieren, und error.message an Menschen ausgeben.

v2 ist der Standard seit quickfix-B. Aufrufer erhalten den strukturierten Envelope automatisch - ein Header-Opt-in ist nicht erforderlich. Das alte v1-Schema mit reinem String bleibt während des Deprecation-Fensters über den Request-Header X-API-Version: 1 verfügbar und wird am 2026-10-24 entfernt.

v2-Envelope (Standard)

Jede Fehlerantwort trägt das folgende Schema. Quelle der Wahrheit: convex/lib/apiResponse.ts (respondError).

{
  "error": {
    "code": "VALIDATION",
    "message": "Invalid request arguments",
    "field": "workflowId",
    "requestId": "5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2",
    "retryable": false
  }
}
  • code - stabiler, maschinenlesbarer Bezeichner aus der ErrorCode-Union. Niemals übersetzt, niemals umbenannt.
  • message - menschenlesbare Beschreibung auf Englisch. Sicher zur Anzeige; auf dem Client mit code als i18n-Schlüssel lokalisierbar.
  • field - optional. Bei VALIDATION-Fehlern vorhanden, um das fehlerhafte Request-Feld zu kennzeichnen.
  • requestId - serverseitig generierte UUID. Im Response-Header X-Request-ID bei jeder Antwort (Erfolg und Fehler) gespiegelt. Immer loggen; in Support-Tickets stets angeben.
  • retryable - optional. Serverseitiger Hinweis, dass der Aufrufer mit Backoff erneut versuchen sollte. Standardmäßig true bei 5xx, sonst nicht gesetzt.

Taxonomie der Fehlercodes

Die ErrorCode-Union wird aus convex/lib/apiResponse.ts exportiert. Codes sind in SCREAMING_SNAKE_CASE und werden über classifyError() auf HTTP-Statuscodes abgebildet.

400 - Client-Validierung

  • VALIDATION - Request-Body / Query / Pfad hat die Schema-Validierung nicht bestanden. field kennzeichnet den fehlerhaften Schlüssel.

401 - Authentifizierung

  • AUTH_MISSING - kein Authorization-Header übergeben.
  • AUTH_INVALID - Bearer-Token ist fehlerhaft, unbekannt oder widerrufen.

403 - Autorisierung

  • FORBIDDEN - Aufrufer ist authentifiziert, hat aber keinen Zugriff auf die Ressource (Projekt passt nicht, Mitgliedschaft fehlt usw.).
  • SCOPE_DENIED - dem API-Key fehlt der erforderliche Scope. Scope im Dashboard hinzufügen und erneut versuchen.

404 - Nicht gefunden

  • NOT_FOUND - Ressource existiert nicht oder wurde gelöscht.

409 - Konflikt

  • CONFLICT - generischer optimistischer Concurrency-/Zustandskonflikt. Neu laden und erneut versuchen.
  • IDEMPOTENCY_MISMATCH - derselbe Idempotency-Key mit anderem Body wiederverwendet. Siehe Idempotenz.
  • IDEMPOTENCY_IN_PROGRESS - gleicher Key, ursprüngliche Anfrage läuft noch. Nach kurzer Verzögerung erneut versuchen.

500 / 501 - Server

  • INTERNAL - unerwarteter Fehler. requestId im Support-Ticket angeben. Mit retryable: true markiert.
  • NOT_IMPLEMENTED - Endpoint ist reserviert, aber noch nicht implementiert.

v1-Legacy-Envelope (Opt-out)

Clients auf dem Deprecation-Pfad können das alte Schema durch Senden des Headers X-API-Version: 1 anfordern. Der Body ist ein einfacher String:

// Request:
//   GET /api/v1/workflows/get?id=missing
//   Authorization: Bearer cc_live_...
//   X-API-Version: 1
//
// Response:
//   HTTP/2 404
//   X-Request-ID: 5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2
{
  "error": "Workflow 'missing' does not exist."
}

v1-Antworten enthalten weiterhin den Header X-Request-ID (eingeführt in quickfix-A), damit Betreiber Fehler auch dann korrelieren können, wenn Clients noch nicht migriert sind.

Sunset: 2026-10-24. Nach diesem Datum hat der Header X-API-Version: 1 keine Wirkung mehr, und alle Aufrufer erhalten den v2-Envelope.

Migration

Wenn Ihr Client v1 erwartet, senden Sie den Request-Header X-API-Version: 1, bis Sie auf v2 umstellen können. Zur Migration:

  1. Entfernen Sie den Header X-API-Version, damit der Server v2 standardmäßig liefert.
  2. Ersetzen Sie String-Parsing durch strukturelles Parsing auf error.code.
  3. Geben Sie error.requestId (oder den Response-Header X-Request-ID) in Ihren Logs und in der Fehleranzeige aus.
  4. Berücksichtigen Sie error.retryable in Retry-Policies. Werten Sie Abwesenheit für 4xx als false und für 5xx als true.

Fehler behandeln

curl (v2-Standard)

curl -i https://<your-deployment>.convex.site/api/v1/runs/start \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"workflowId": "missing"}'
# HTTP/2 404
# X-Request-ID: 5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2
# {
#   "error": {
#     "code": "NOT_FOUND",
#     "message": "Workflow 'missing' does not exist.",
#     "requestId": "5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2"
#   }
# }

TypeScript

type ErrorCode =
  | "AUTH_INVALID"
  | "AUTH_MISSING"
  | "FORBIDDEN"
  | "SCOPE_DENIED"
  | "NOT_FOUND"
  | "VALIDATION"
  | "CONFLICT"
  | "INTERNAL"
  | "NOT_IMPLEMENTED"
  | "IDEMPOTENCY_MISMATCH"
  | "IDEMPOTENCY_IN_PROGRESS";

type ApiError = {
  error: {
    code: ErrorCode;
    message: string;
    field?: string;
    requestId: string;
    retryable?: boolean;
  };
};

async function call<T>(url: string, init: RequestInit): Promise<T> {
  const res = await fetch(url, init);
  if (res.ok) return (await res.json()) as T;

  const body = (await res.json()) as ApiError;
  switch (body.error.code) {
    case "SCOPE_DENIED":
      throw new Error(`Missing scope. requestId=${body.error.requestId}`);
    case "VALIDATION":
      throw new Error(`Bad field: ${body.error.field ?? "<unknown>"}`);
    case "IDEMPOTENCY_MISMATCH":
      throw new Error("Use a fresh Idempotency-Key.");
    case "INTERNAL":
      // body.error.retryable === true - retry with backoff.
      throw new Error(`Server error. requestId=${body.error.requestId}`);
    default:
      throw new Error(`${body.error.code}: ${body.error.message}`);
  }
}

Python

import requests

class ApiError(Exception):
    def __init__(self, code, message, request_id, field=None, retryable=None):
        super().__init__(f"{code}: {message} [requestId={request_id}]")
        self.code = code
        self.request_id = request_id
        self.field = field
        self.retryable = retryable

def call(method, url, **kwargs):
    r = requests.request(method, url, timeout=30, **kwargs)
    if r.ok:
        return r.json()
    body = r.json().get("error", {})
    raise ApiError(
        code=body.get("code", "UNKNOWN"),
        message=body.get("message", r.text),
        request_id=body.get("requestId") or r.headers.get("X-Request-ID", "<none>"),
        field=body.get("field"),
        retryable=body.get("retryable"),
    )

Lokalisierung

Da error.code stabil ist, können Client-Apps übersetzte Nachrichten nach Code nachschlagen:

const messages = {
  de: { SCOPE_DENIED: "Dem API-Schlüssel fehlt ein erforderlicher Scope." },
  en: { SCOPE_DENIED: "API key is missing a required scope." },
};
const display = messages[userLocale][err.code] ?? err.message;

Request-IDs

Der Header X-Request-ID wird bei jeder Antwort ausgegeben (Erfolg und Fehler, v1 und v2). Der v2-Body stellt denselben Wert zusätzlich als error.requestId (bzw. data.requestId bei erfolgreichen Antworten) bereit, damit Client-SDKs korrelieren können, ohne Header zu parsen. Loggen Sie die ID immer; geben Sie sie stets in Support-Tickets an.

Verwandt