# Erreurs

> Le format des erreurs, quand réessayer, et chaque code que l'API peut renvoyer.

Une erreur a toujours la même forme, quel que soit l'appel :

```json
{
  "error": {
    "type": "invalid_request",
    "code": "parameter_invalid",
    "message": "Paramètre invalide : limit.",
    "param": "limit",
    "doc_url": "https://docs.rustine.io/api/errors#parameter_invalid"
  }
}
```

- `type` : la catégorie, pour savoir qui doit agir (toi, ton offre, ou Rustine).
- `code` : le cas précis. C'est lui qu'il faut tester dans ton code.
- `message` : une phrase pour un humain, dans la langue du propriétaire de la clé. Journalise-la, ne la teste pas : sa formulation peut changer.
- `param` : avec `invalid_request`, le paramètre en cause.
- `doc_url` : la ligne de cette page qui explique le code.
- `upgrade_url` : avec `plan_limit`, où changer d'offre ou acheter un pack.

Chaque réponse porte aussi un en-tête `Request-Id` : donne-le au support si une erreur revient.

## Quand réessayer

- `429` : oui, après le nombre de secondes de l'en-tête `Retry-After`.
- `500`, `502` et `503` : oui, un peu plus tard. Pour une écriture, renvoie la même [`Idempotency-Key`](https://docs.rustine.io/idempotency.md) : l'action ne sera pas faite deux fois.
- `409 idempotency_in_progress` : oui, dans un instant.
- Les autres `4xx` : non. La même requête recevra la même réponse ; corrige-la d'abord, ou change d'offre pour un `402`.

## Tous les codes

| error.code | HTTP | error.type | Ce que ça veut dire |
| --- | --- | --- | --- |
| `body_invalid` | 400 | `invalid_request` | Le corps de la requête n'est pas du JSON valable. Vérifie l'en-tête `Content-Type: application/json`. |
| `cursor_invalid` | 400 | `invalid_request` | `starting_after` n'est l'identifiant d'aucun élément de cette liste. |
| `idempotency_key_invalid` | 400 | `invalid_request` | L'en-tête `Idempotency-Key` doit faire de 1 à 255 caractères imprimables. |
| `parameter_invalid` | 400 | `invalid_request` | Un paramètre manque ou n'est pas valable ; `param` le nomme. |
| `reason_required` | 400 | `invalid_request` | Marquer un faux positif ou un risque accepté demande une raison, de 10 à 500 caractères. |
| `target_kind_invalid` | 400 | `invalid_request` | Le type de scan ne va pas avec la cible : `repository` pour un dépôt GitHub, `quick` ou `audit` pour un site. |
| `url_invalid` | 400 | `invalid_request` | L'adresse n'est pas l'URL valable d'un site public. |
| `url_private` | 400 | `invalid_request` | Rustine ne scanne que des sites publics, jamais une adresse locale ou privée. |
| `authentication_required` | 401 | `authentication` | Aucune clé dans la requête. Passe-la dans l'en-tête `Authorization: Bearer rst_…`, jamais dans l'adresse. |
| `invalid_api_key` | 401 | `authentication` | La clé ou le jeton OAuth est faux, révoqué ou expiré. Vérifie qu'il est copié en entier, ou crée une nouvelle clé. |
| `address_limit_reached` | 402 | `plan_limit` | Le projet suit déjà autant d'adresses que ton offre le permet. |
| `api_plan_required` | 402 | `plan_limit` | L'espace est sur l'offre Gratuite. L'API vient avec Pro, ou à l'essai 14 jours sur Solo. |
| `api_trial_ended` | 402 | `plan_limit` | L'essai de 14 jours de Solo est terminé. Passe à Pro : tes clés remarchent aussitôt. |
| `audit_not_in_plan` | 402 | `plan_limit` | L'offre Gratuite ne fait que des scans rapides ; l'audit complet vient avec Solo et Pro. |
| `audit_quota_exceeded` | 402 | `plan_limit` | Les audits complets de la période sont épuisés, et il ne reste aucun crédit de pack. |
| `domain_taken` | 402 | `plan_limit` | Sur l'offre Gratuite, un domaine déjà suivi par un autre compte gratuit ne peut être ni ajouté ni scanné. |
| `fix_quota_exceeded` | 402 | `plan_limit` | Les correctifs de la période sont épuisés, et il ne reste aucun crédit de pack. Un correctif déjà débloqué reste lisible. |
| `plan_limit` | 402 | `plan_limit` | Ton offre ne comprend pas cette action. `upgrade_url` mène à l'offre qui la permet. |
| `project_limit_reached` | 402 | `plan_limit` | Tous les projets de ton offre sont pris. Ajoute un projet en plus ou change d'offre. |
| `project_paused` | 402 | `plan_limit` | Le projet est en pause : ton offre couvre moins de projets que ton espace n'en a. Ses problèmes restent lisibles, mais rien de nouveau ne tourne. |
| `repositories_not_in_plan` | 402 | `plan_limit` | L'analyse des dépôts GitHub vient avec Solo et Pro. |
| `scan_quota_exceeded` | 402 | `plan_limit` | Les scans rapides de la période sont épuisés, et il ne reste aucun crédit de pack. |
| `scope_missing` | 403 | `authentication` | Le jeton OAuth n'a pas la permission que demande cette route (indiquée sur sa page). Reconnecte l'agent pour l'accorder. Une clé d'API a toutes les permissions. |
| `fix_locked` | 404 | `not_found` | Le correctif de ce problème n'est pas encore débloqué : `POST /issues/{issue_id}/fix` le débloque. |
| `resource_not_found` | 404 | `not_found` | Rien avec cet identifiant dans l'espace de la clé. Un objet d'un autre espace répond pareil : son existence n'est jamais révélée. |
| `route_not_found` | 404 | `not_found` | Cette route n'existe pas. Vérifie la méthode et le chemin dans la référence. |
| `conflict` | 409 | `conflict` | La requête ne va pas avec l'état actuel de l'objet. Relis-le, puis réessaie. |
| `github_unavailable` | 409 | `conflict` | L'analyse des dépôts GitHub n'est pas disponible pour le moment. |
| `idempotency_conflict` | 409 | `conflict` | Cette `Idempotency-Key` a déjà servi pour une autre requête (méthode, chemin ou corps). Une nouvelle action prend une nouvelle clé. |
| `idempotency_in_progress` | 409 | `conflict` | Une requête avec cette `Idempotency-Key` est encore en cours. Réessaie dans un instant. |
| `issue_already_fixed` | 409 | `conflict` | Le problème est corrigé : il n'y a rien à marquer. |
| `proof_required` | 409 | `conflict` | Le site doit d'abord être prouvé comme le tien (audit complet, correctif, marquage). Prouve-le dans l'app : DNS, fichier ou balise meta. |
| `site_excluded` | 409 | `conflict` | Le propriétaire de ce site a demandé à ne pas être scanné. Rustine ne le scannera pas. |
| `target_exists` | 409 | `conflict` | Le projet suit déjà cette adresse. |
| `rate_limited` | 429 | `rate_limited` | Trop de requêtes pour cette clé (120 par minute). Attends `Retry-After` secondes. |
| `recheck_limit_reached` | 429 | `rate_limited` | Trop de revérifications sur ce projet aujourd'hui (60 par jour). Réessaie demain. |
| `repository_analyses_limit_reached` | 429 | `rate_limited` | L'espace a lancé beaucoup d'analyses de dépôt ces dernières 24 heures. Réessaie plus tard. |
| `internal_error` | 500 | `api_error` | Une erreur de notre côté, signalée automatiquement. Réessaie plus tard ; donne le `Request-Id` au support si elle revient. |
| `recheck_failed` | 502 | `api_error` | La revérification n'a pas pu aller au bout (site injoignable ?). Vérifie que le site répond, puis réessaie. |
| `platform_busy` | 503 | `rate_limited` | Rustine ne lance pas de nouveau scan pendant quelques minutes. Réessaie plus tard ; rien n'a été décompté. |
| `service_unavailable` | 503 | `api_error` | Rustine est indisponible un instant. Réessaie avec la même `Idempotency-Key`. |

## Les types

Teste `type` pour décider quoi faire, `code` pour savoir exactement ce qui s'est passé.

| error.type | Ce que ça veut dire |
| --- | --- |
| `invalid_request` | La requête elle-même est à corriger. |
| `authentication` | La clé ou le jeton ne passe pas, ou n'a pas la permission. |
| `plan_limit` | L'offre ou le quota ne le permet pas ; `upgrade_url` dit quoi faire. |
| `not_found` | L'objet ou la route n'existe pas pour cette clé. |
| `rate_limited` | Trop de requêtes, ou un plafond atteint : attends un peu. |
| `conflict` | L'action ne va pas avec l'état actuel (site non prouvé, clé déjà servie…). |
| `api_error` | Un souci côté Rustine : réessaie. |

## Exemples

### Node

```js
const res = await fetch(url, { headers })
if (!res.ok) {
  const { error } = await res.json()
  if (error.type === "plan_limit") {
    console.log(`Offre ou quota : ${error.upgrade_url}`)
  } else if (res.status === 429 || res.status >= 500) {
    const wait = Number(res.headers.get("Retry-After") ?? 30)
    await new Promise((r) => setTimeout(r, wait * 1000))
    // ...puis réessaie, avec la même Idempotency-Key
  } else {
    throw new Error(`${error.code}: ${error.message}`)
  }
}
```

### Réponse : 402 Payment Required

```json
{
  "error": {
    "type": "plan_limit",
    "code": "audit_quota_exceeded",
    "message": "Les audits complets de la période sont épuisés.",
    "doc_url": "https://docs.rustine.io/api/errors#audit_quota_exceeded",
    "upgrade_url": "https://app.rustine.io/settings?section=offer"
  }
}
```
