Aller au contenu

Erreurs, nouvelles tentatives et limites de débit

Hiérarchie des exceptions

Tout ce que le SDK lève dérive de Pennylane\Sdk\Exception\PennylaneException :

PennylaneException
├── ConfigurationException      le SDK n'est pas utilisable tel que configuré (jeton, baseUrl, client HTTP)
├── InvalidRequestBodyException un corps de requête ou une valeur de filtre ne peut pas être envoyé sans risque (ex. un flottant)
├── InvalidResponseException    un statut de succès mais un corps qui n'est pas du JSON exploitable
├── ApiConnectionException      la requête n'a jamais reçu de réponse HTTP
│   └── ApiTimeoutException
└── ApiStatusException          l'API a répondu 4xx/5xx
    ├── BadRequestException       400  JSON malformé, mauvais types
    ├── AuthenticationException   401  jeton manquant/invalide/expiré
    ├── PermissionDeniedException 403  scope manquant
    ├── NotFoundException         404
    ├── ConflictException         409  ex. document en doublon
    ├── ValidationException       422  échec de validation métier
    ├── RateLimitException        429  ajoute ->retryAfter
    └── ServerException           5xx

Chaque ApiStatusException porte :

  • statusCode : le statut HTTP ;
  • errorCode : le code exploitable par un programme, quand l'API en envoie un ;
  • getMessage() : le meilleur message lisible disponible ;
  • details : des détails structurés lorsqu'ils existent (ex. quels totaux ne s'équilibrent pas) ;
  • rawBody : le corps de réponse brut, tronqué à 2000 caractères par sécurité ;
  • body : le corps d'erreur JSON décodé sous forme de tableau, ou null quand le corps n'est pas un objet JSON.
use Pennylane\Sdk\Exception\ValidationException;

try {
    $client->ledgerEntries->create(/* ... */);
} catch (ValidationException $e) {
    echo $e->getMessage();  // "Entry lines are not balanced"
    print_r($e->details);   // ['debit_total' => '100.00', 'credit_total' => '80.00']
}

ApiStatusException ne conserve volontairement aucune référence à la requête ou à la réponse PSR-7, afin de ne jamais risquer de journaliser l'en-tête Authorization par accident. Journalisez plutôt statusCode, errorCode et getMessage().

Pourquoi chaque champ est optionnel

Le format du corps d'erreur de l'API varie selon l'endpoint (parfois {code, message, details}, parfois {error, status}, parfois {message, field}). Chaque propriété de ApiStatusException est donc optionnelle et analysée de façon défensive, getMessage() retombant du message du corps à son code d'erreur puis au texte brut de la réponse, afin de toujours vous fournir le maximum d'informations disponibles.

Nouvelles tentatives automatiques

Le SDK relance les requêtes de façon transparente, avec un backoff exponentiel et du jitter, jusqu'à maxRetries fois (3 par défaut, en partant d'une demi-seconde et plafonné à 8 secondes entre les tentatives). Cette politique est volontairement prudente car l'API Pennylane n'a pas d'idempotence côté serveur : renvoyer une requête de création que le serveur a peut-être déjà traitée pourrait produire une facture ou une écriture comptable en double.

Situation GET / PUT / DELETE POST
429 limite de débit atteinte nouvelle tentative (respecte retry-after) nouvelle tentative (la requête a été rejetée sans être traitée, sans risque)
500 / 502 / 503 / 504 nouvelle tentative pas de nouvelle tentative (risque de doublon)
Échec de connexion avant envoi nouvelle tentative nouvelle tentative
Timeout / connexion perdue après envoi nouvelle tentative pas de nouvelle tentative

La ligne "échec de connexion avant envoi" ne s'applique que lorsque l'échec de transport est prouvé sans risque (résolution DNS, connexion refusée, échec de la négociation TLS et similaires) ; tout autre échec de transport est considéré comme potentiellement envoyé et n'est retenté que pour les méthodes idempotentes.

Si vous avez besoin d'une sémantique "au moins une fois" sur les créations, dédupliquez de votre côté (par exemple avec des champs external_reference), comme Pennylane le recommande officiellement.

Limites de débit

API Limite
Company API v2 25 requêtes par 5 secondes, par jeton
Firm API v1 5 requêtes par seconde

Deux niveaux vous protègent :

  1. Régulation de débit côté client (autoThrottle: true par défaut) : les requêtes sont cadencées pour que les pics, la pagination automatique et les imports massifs restent sous la limite. Désactivez avec new Pennylane(autoThrottle: false) si vous gérez vous-même l'orchestration des limites.
  2. Gestion des 429 : si un 429 survient malgré tout (par exemple plusieurs processus partageant un même jeton), le SDK attend le délai indiqué par retry-after (plafonné à 60 secondes) puis relance la requête, quelle que soit la méthode HTTP.

L'API indique le budget restant à chaque réponse ; le SDK expose les dernières valeurs :

$client->me->retrieve();
$info = $client->lastRateLimit();
echo $info->limit . ' ' . $info->remaining . ' ' . $info->reset;  // 25 24 1751980800

lastRateLimit() renvoie null tant que le client n'a fait aucune requête, ou si la réponse de l'API ne portait aucun en-tête ratelimit-*.

Un jeton par intégration

Les limites s'appliquent par jeton. Attribuez à chaque intégration son propre jeton afin qu'elles ne se privent pas mutuellement de quota.