Envelope di errore
Envelope di errore v2 (DEFAULT dal quickfix-B) - schema completo, codici di errore, request ID e migrazione dalla v1 legacy.
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’unionErrorCode. Mai tradotto, mai rinominato.message- descrizione leggibile dall’uomo, in inglese. Sicuro da mostrare; sicuro da localizzare lato client usandocodecome chiave i18n.field- opzionale. Presente sugli erroriVALIDATIONper individuare il campo di richiesta problematico.requestId- UUID generato dal server. Restituito nell’header di rispostaX-Request-IDsu ogni risposta (successo ed errore). Registralo sempre; citalo nei ticket di supporto.retryable- opzionale. Suggerimento lato server che il chiamante dovrebbe riprovare con backoff. Valetrueper 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.fieldindividua la chiave problematica.
401 - Autenticazione
AUTH_MISSING- non è stato fornito alcun headerAuthorization.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- stessaIdempotency-Keyriutilizzata 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. CitarequestIdnel tuo ticket di supporto. Marcatoretryable: 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:
- Rimuovi l’header
X-API-Versioncosì il server emette la v2 per default. - Sostituisci il parsing di stringhe con il parsing strutturale su
error.code. - Mostra
error.requestId(o l’header di rispostaX-Request-ID) nei tuoi log e nella tua UI di errore. - Rispetta
error.retryablenelle tue politiche di retry. Tratta l’assenza comefalseper i 4xx etrueper 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.