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, ounullquand 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 :
- Régulation de débit côté client (
autoThrottle: truepar 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 avecnew Pennylane(autoThrottle: false)si vous gérez vous-même l'orchestration des limites. - 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.