Envelope di errore

Envelope di errore v2 (DEFAULT dal quickfix-B) - schema completo, codici di errore, request ID e migrazione dalla v1 legacy.

7 min letto
errorserror-envelopeerror-codes

Ogni risposta non-2xx dell’API CodeCourier usa un envelope JSON strutturato. I client possono fare switch su error.code per una gestione programmatica affidabile e mostrare error.message agli umani.

La v2 è il default a partire da quickfix-B. I chiamanti ricevono automaticamente l’envelope strutturato - non è richiesto alcun opt-in tramite header. La forma legacy v1 solo-stringa rimane disponibile durante la finestra di deprecazione tramite l’header di richiesta X-API-Version: 1 ed è pianificata per la rimozione il 2026-10-24.

Envelope v2 (default)

Ogni risposta di errore porta la forma seguente. Fonte di verità: convex/lib/apiResponse.ts (respondError).

{
  "error": {
    "code": "VALIDATION",
    "message": "Invalid request arguments",
    "field": "workflowId",
    "requestId": "5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2",
    "retryable": false
  }
}
  • code - identificatore stabile e leggibile dalla macchina proveniente dall’union ErrorCode. Mai tradotto, mai rinominato.
  • message - descrizione leggibile dall’uomo, in inglese. Sicuro da mostrare; sicuro da localizzare lato client usando code come chiave i18n.
  • field - opzionale. Presente sugli errori VALIDATION per individuare il campo di richiesta problematico.
  • requestId - UUID generato dal server. Restituito nell’header di risposta X-Request-ID su ogni risposta (successo ed errore). Registralo sempre; citalo nei ticket di supporto.
  • retryable - opzionale. Suggerimento lato server che il chiamante dovrebbe riprovare con backoff. Vale true per default per i 5xx e assente altrimenti.

Tassonomia dei codici di errore

L’union ErrorCode è esportata da convex/lib/apiResponse.ts. I codici sono in SCREAMING_SNAKE_CASE e mappati ai codici di stato HTTP da classifyError().

400 - Validazione client

  • VALIDATION - il body / query / path della richiesta ha fallito la validazione dello schema. field individua la chiave problematica.

401 - Autenticazione

  • AUTH_MISSING - non è stato fornito alcun header Authorization.
  • AUTH_INVALID - il bearer token è malformato, sconosciuto o revocato.

403 - Autorizzazione

  • FORBIDDEN - il chiamante è autenticato ma non può accedere alla risorsa (mismatch di progetto, membership mancante, ecc.).
  • SCOPE_DENIED - la chiave API non ha lo scope richiesto. Aggiungi lo scope nel dashboard, poi riprova.

404 - Non trovato

  • NOT_FOUND - la risorsa non esiste o è stata eliminata.

409 - Conflitto

  • CONFLICT - conflitto generico di concorrenza ottimistica / di stato. Ri-fetcha e riprova.
  • IDEMPOTENCY_MISMATCH - stessa Idempotency-Key riutilizzata con un body diverso. Vedi Idempotenza.
  • IDEMPOTENCY_IN_PROGRESS - stessa chiave, la richiesta originale è ancora in volo. Riprova dopo un breve ritardo.

500 / 501 - Server

  • INTERNAL - fallimento inatteso. Cita requestId nel tuo ticket di supporto. Marcato retryable: true.
  • NOT_IMPLEMENTED - l’endpoint è riservato ma non ancora implementato.

Envelope v1 legacy (opt-out)

I client sul binario di deprecazione possono richiedere la vecchia forma inviando l’header X-API-Version: 1. Il body è una semplice stringa:

// 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."
}

Le risposte v1 portano ancora l’header X-Request-ID (aggiunto in quickfix-A), così gli operatori possono correlare i fallimenti anche quando i client non hanno migrato.

Sunset: 2026-10-24. Dopo quella data l’header X-API-Version: 1 diventa un no-op e tutti i chiamanti ricevono l’envelope v2.

Migrazione

Se il tuo client si aspetta la v1, invia l’header X-API-Version: 1 finché non puoi passare alla v2. Per migrare:

  1. Rimuovi l’header X-API-Version così il server emette la v2 per default.
  2. Sostituisci il parsing di stringhe con il parsing strutturale su error.code.
  3. Mostra error.requestId (o l’header di risposta X-Request-ID) nei tuoi log e nella tua UI di errore.
  4. Rispetta error.retryable nelle tue politiche di retry. Tratta l’assenza come false per i 4xx e true per i 5xx.

Gestire gli errori

curl (v2 default)

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"),
    )

Localizzazione

Poiché error.code è stabile, le app client possono cercare messaggi tradotti indicizzati per codice:

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 ID

L’header X-Request-ID viene emesso su ogni risposta (successo ed errore, v1 e v2). Il body v2 mostra inoltre lo stesso valore come error.requestId (o data.requestId sulle risposte riuscite), così gli SDK client possono correlare senza fare il parsing degli header. Registra sempre l’ID; includilo sempre nei ticket di supporto.

Correlati