Zum Inhalt springen

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.

429
{
  "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?

AntwortWiederholen?Was zu tun ist
429 rate_limitedJa, immerSo viele Sekunden warten, wie Retry-After angibt. Die Anfrage wurde abgelehnt, bevor sie etwas bewirkt hat.
500 internal_error, 502, 503, 504GET und updateCondition: ja, mit Backoff. createLifecycleEvent: nicht automatischWarten und erneut versuchen; hält der Fehler an, den Support mit der request_id kontaktieren.
Netzwerkfehler oder TimeoutWie bei 5xxSie wissen nicht, ob die Anfrage angekommen ist.
400 validation_errorNeinAnfrage korrigieren; errors[] nennt jedes Feld (field, code, message).
401 unauthenticatedNeinDer Schlüssel fehlt, ist unbekannt oder widerrufen. Einen Menschen alarmieren, statt in einer Schleife zu wiederholen.
403 forbiddenNeinDem Schlüssel fehlt der Scope der Operation (read, condition oder import).
404 not_foundNeinFalsche ID, oder die Ressource gehört zu einer anderen Organisation.
409 conflictNeinDie 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:

  1. Ist Retry-After gesetzt, warten Sie so viele Sekunden.
  2. 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.
  3. Geben Sie nach wenigen Versuchen auf (fünf reichen) und melden Sie den Fehler mit seiner request_id.
  4. 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

ProtokollierenNie protokollieren
Operation, Methode und PfadDen Header Authorization oder den Schlüssel
HTTP-Status und codeDen 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.

Auf dieser Seite