Fehler behandeln und Anfragen wiederholen
Entscheiden Sie, welche Antworten Sie wiederholen und welche Sie beheben, warten Sie mit Backoff und Jitter, beachten Sie Retry-After und protokollieren Sie die request_id.
Jeder Fehler der API ist ein Problem nach RFC 9457 (application/problem+json) mit einem stabilen code und einer
request_id. Manche Fehler verschwinden, wenn Sie warten und es erneut versuchen; die meisten verlangen eine
Änderung auf Ihrer Seite. Diese Anleitung ordnet sie und zeigt eine kleine Hilfsfunktion, die die richtigen
wiederholt. Die Codes stehen unter Fehler.
{
"type": "about:blank",
"title": "Too Many Requests",
"status": 429,
"code": "rate_limited",
"detail": "Too many attempts. Try again later.",
"request_id": "8b1e6f03c2d94a7e5f0a1b2c"
}Die request_id steht auch im Header X-Request-Id jeder Antwort, auch der erfolgreichen.
Wiederholen oder beheben?
| Antwort | Wiederholen? | Was zu tun ist |
|---|---|---|
429 rate_limited | Ja, immer | So viele Sekunden warten, wie Retry-After angibt. Die Anfrage wurde abgelehnt, bevor sie etwas bewirkt hat. |
500 internal_error, 502, 503, 504 | GET und updateCondition: ja, mit Backoff. createLifecycleEvent: nicht automatisch | Warten und erneut versuchen; hält der Fehler an, den Support mit der request_id kontaktieren. |
| Netzwerkfehler oder Timeout | Wie bei 5xx | Sie wissen nicht, ob die Anfrage angekommen ist. |
400 validation_error | Nein | Anfrage korrigieren; errors[] nennt jedes Feld (field, code, message). |
401 unauthenticated | Nein | Der Schlüssel fehlt, ist unbekannt oder widerrufen. Einen Menschen alarmieren, statt in einer Schleife zu wiederholen. |
403 forbidden | Nein | Dem Schlüssel fehlt der Scope der Operation (read, condition oder import). |
404 not_found | Nein | Falsche ID, oder die Ressource gehört zu einer anderen Organisation. |
409 conflict | Nein | Die Ressource ist in einem Zustand, der das nicht erlaubt, z. B. ein QR-Code, bevor die Kennungen gesperrt sind, oder Zustandsdaten für eine Einheit eines archivierten Modells. |
Warum die Methoden sich unterscheiden: GET ändert nichts, und updateCondition überspringt eine Messung, die
schon gespeichert ist; denselben Body erneut zu senden schadet also nicht. createLifecycleEvent erfasst bei
jedem erfolgreichen Aufruf ein neues Ereignis; prüfen Sie nach einem Timeout
listLifecycleEvents, bevor Sie erneut senden.
Einzelheiten unter Wiederholungen und Idempotenz.
Wiederholen Sie 401 nicht in einer Schleife. Unbekannte und widerrufene Schlüssel sind auf 30 Versuche pro
15 Minuten und Client-Adresse begrenzt; ein falsch konfigurierter Job bekommt also bald 429 statt 401, und die
eigentliche Ursache gerät aus dem Blick.
Backoff mit Jitter
Wenn sich eine Wiederholung lohnt:
- Ist
Retry-Aftergesetzt, warten Sie so viele Sekunden. - Andernfalls warten Sie eine zufällige Zeit zwischen 0 und
min(30 s, 0.5 s × 2^attempt)(„Full Jitter“), damit viele Clients nicht im Gleichtakt wiederholen. - Geben Sie nach wenigen Versuchen auf (fünf reichen) und melden Sie den Fehler mit seiner
request_id. - Setzen Sie ein Client-Timeout (zum Beispiel 30 Sekunden), damit eine hängende Verbindung als Netzwerkfehler zählt.
In Batch-Jobs bedeutet ein 429, dass die ganze Organisation über ihrem Limit von 600 Anfragen pro Minute liegt
(alle Schlüssel zusammen). Bremsen Sie den Job, statt nur die einzelne Anfrage zu wiederholen; siehe
Ratenlimits.
Eine Hilfsfunktion für jeden Aufruf
Die Hilfsfunktion sendet eine Anfrage, gibt das geparste JSON zurück, wiederholt, was sich zu wiederholen lohnt, und wirft einen Fehler, der das Problem enthält. Jede Fassung läuft für sich; die letzten Zeilen zeigen die Verwendung.
// batterypass.mts: npx tsx batterypass.mts
const BASE = `${process.env.BATTERYPASS_URL}/api/v1`;
const KEY = process.env.BATTERYPASS_API_KEY;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export class ApiError extends Error {
status: number;
problem: { code?: string; detail?: string; request_id?: string; errors?: unknown[] };
constructor(status: number, problem: ApiError['problem']) {
super(`${status} ${problem.code}: ${problem.detail} (request_id ${problem.request_id})`);
this.status = status;
this.problem = problem;
}
}
/** Sends a request; retries 429 always, and 5xx or network errors unless the method is POST. */
export async function call<T>(method: string, path: string, body?: unknown, maxAttempts = 5): Promise<T> {
for (let attempt = 1; ; attempt++) {
let res: Response | undefined;
try {
res = await fetch(BASE + path, {
method,
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
} catch (err) {
if (method === 'POST' || attempt === maxAttempts) throw err; // network error or timeout
}
if (res?.ok) return (await res.json()) as T;
const retryable = !res || res.status === 429 || (res.status >= 500 && method !== 'POST');
if (res && (!retryable || attempt === maxAttempts)) {
const requestId = res.headers.get('X-Request-Id') ?? undefined;
const problem = await res.json().catch(() => ({ request_id: requestId }));
throw new ApiError(res.status, problem);
}
// Retry-After (seconds) wins; otherwise exponential backoff with full jitter, at most 30 s.
const retryAfter = Number(res?.headers.get('Retry-After'));
await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.random() * Math.min(30_000, 500 * 2 ** attempt));
}
}
// Usage
try {
const page = await call<{ items: { serial_number: string }[] }>('GET', '/batteries?limit=10');
console.log(page.items.map((u) => u.serial_number));
} catch (err) {
if (err instanceof ApiError) console.error(err.message); // log the request_id, never the key
else throw err;
}Was ins Log gehört
| Protokollieren | Nie protokollieren |
|---|---|
| Operation, Methode und Pfad | Den Header Authorization oder den Schlüssel |
HTTP-Status und code | Den Inhalt von JSON- oder PDF-Exporten (sie enthalten eingeschränkte Daten) |
request_id (oder X-Request-Id) | |
errors[] eines validation_error |
Wenn Sie den Support kontaktieren, nennen Sie die request_id: Damit findet sich die
Anfrage in den Serverprotokollen, ohne dass jemand Ihren Schlüssel braucht.
Einheiten abgleichen
Übernehmen Sie die Einheiten Ihrer Organisation mit Cursor-Paginierung in ERP, MES oder Datenbank, setzen Sie nach einer Unterbrechung fort und erkennen Sie Änderungen.
QR-Code auflösen
Vom gescannten GS1 Digital Link zum öffentlichen Pass-JSON, über den Resolver oder über GTIN und Seriennummer.