Enveloppe d'erreur

Enveloppe d'erreur v2 (PAR DÉFAUT depuis quickfix-B) - schéma complet, codes d'erreur, request IDs et migration depuis la v1 legacy.

7 min lire
errorserror-envelopeerror-codes

Chaque réponse non-2xx de l’API CodeCourier utilise une enveloppe JSON structurée. Les clients peuvent aiguiller sur error.code pour une gestion programmatique fiable et faire remonter error.message aux humains.

La v2 est le défaut depuis quickfix-B. Les appelants reçoivent automatiquement l’enveloppe structurée - aucun opt-in par header n’est requis. La forme legacy v1 en chaîne uniquement reste disponible pendant la fenêtre de dépréciation via le header de requête X-API-Version: 1 et son retrait est prévu pour le 2026-10-24.

Enveloppe v2 (défaut)

Chaque réponse d’erreur porte la forme suivante. Source de vérité : convex/lib/apiResponse.ts (respondError).

{
  "error": {
    "code": "VALIDATION",
    "message": "Invalid request arguments",
    "field": "workflowId",
    "requestId": "5b2c1f0a-8e7d-4a4f-bb6d-f0a3c8a1e7e2",
    "retryable": false
  }
}
  • code - identifiant stable et lisible par machine issu de l’union ErrorCode. Jamais traduit, jamais renommé.
  • message - description lisible par un humain, en anglais. Sûr à afficher ; sûr à localiser côté client en utilisant code comme clé i18n.
  • field - optionnel. Présent sur les erreurs VALIDATION pour pointer le champ de requête fautif.
  • requestId - UUID généré par le serveur. Renvoyé dans le header de réponse X-Request-ID sur chaque réponse (succès et erreur). Loggez-le toujours ; citez-le dans les tickets de support.
  • retryable - optionnel. Indice côté serveur que l’appelant devrait réessayer avec backoff. Vaut true par défaut pour les 5xx et est absent sinon.

Taxonomie des codes d’erreur

L’union ErrorCode est exportée depuis convex/lib/apiResponse.ts. Les codes sont en SCREAMING_SNAKE_CASE et mappés aux codes de statut HTTP par classifyError().

400 - Validation client

  • VALIDATION - le body / query / path de requête a échoué à la validation de schéma. field pointe la clé fautive.

401 - Authentification

  • AUTH_MISSING - aucun header Authorization n’a été fourni.
  • AUTH_INVALID - le bearer token est malformé, inconnu ou révoqué.

403 - Autorisation

  • FORBIDDEN - l’appelant est authentifié mais ne peut pas accéder à la ressource (mismatch de projet, appartenance manquante, etc.).
  • SCOPE_DENIED - la clé API n’a pas le scope requis. Ajoutez le scope dans le dashboard, puis réessayez.

404 - Non trouvé

  • NOT_FOUND - la ressource n’existe pas ou a été supprimée.

409 - Conflit

  • CONFLICT - conflit générique de concurrence optimiste / d’état. Re-fetchez et réessayez.
  • IDEMPOTENCY_MISMATCH - même Idempotency-Key réutilisée avec un body différent. Voir Idempotence.
  • IDEMPOTENCY_IN_PROGRESS - même clé, la requête originale est encore en vol. Réessayez après un court délai.

500 / 501 - Serveur

  • INTERNAL - échec inattendu. Citez requestId dans votre ticket de support. Marqué retryable: true.
  • NOT_IMPLEMENTED - l’endpoint est réservé mais pas encore implémenté.

Enveloppe v1 legacy (opt-out)

Les clients sur le rail de dépréciation peuvent demander l’ancienne forme en envoyant le header X-API-Version: 1. Le body est une simple chaîne :

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

Les réponses v1 portent toujours le header X-Request-ID (ajouté dans quickfix-A), de sorte que les opérateurs peuvent corréler les échecs même lorsque les clients n’ont pas migré.

Sunset : 2026-10-24. Après cette date, le header X-API-Version: 1 devient un no-op et tous les appelants reçoivent l’enveloppe v2.

Migration

Si votre client attend la v1, envoyez le header X-API-Version: 1 jusqu’à ce que vous puissiez passer à la v2. Pour migrer :

  1. Retirez le header X-API-Version pour que le serveur émette la v2 par défaut.
  2. Remplacez le parsing de chaîne par un parsing structurel sur error.code.
  3. Faites remonter error.requestId (ou le header de réponse X-Request-ID) dans vos logs et votre UI d’erreur.
  4. Honorez error.retryable dans vos politiques de retry. Traitez l’absence comme false pour les 4xx et true pour les 5xx.

Gérer les erreurs

curl (v2 par défaut)

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

Localisation

Parce que error.code est stable, les applications clientes peuvent rechercher des messages traduits indexés par code :

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

Le header X-Request-ID est émis sur chaque réponse (succès et erreur, v1 et v2). Le body v2 fait en outre remonter la même valeur en tant que error.requestId (ou data.requestId sur les réponses réussies), de sorte que les SDK clients peuvent corréler sans parser les headers. Loggez toujours l’ID ; incluez-le toujours dans les tickets de support.

Liens connexes