GunSpec
Praxis

Fehler bewusst behandeln

Manche Fehler lohnen einen zweiten Versuch, andere können nur wieder scheitern. Sie zu unterscheiden trennt eine langsame Minute von einem verbrannten Kontingent.

Verzweigen Sie über error.reason, nie über die Meldung: Gründe sind stabil und werden nie umbenannt, Meldungen sind für Menschen geschrieben. Die Fehlerseite listet jeden davon.

GrundStatusWie lange warten
RATE_LIMITED429Retry-After nennt 60 s. Richten Sie sich nach dem Header, nicht nach der Zahl.
PAGINATION_BURST429Exponentielles Backoff mit Jitter.
DAILY_CAP_EXCEEDED429Bis zum Reset: Mitternacht UTC bei der Tagesgrenze, der 1. beim Monatskontingent. Retry-After nennt die verbleibenden Sekunden und error.resetsAt den Zeitpunkt. Warten Sie nur, wenn er Sekunden entfernt ist, bei einem Aufruf kurz vor einem Reset. Sonst anhalten, die Arbeit für danach einplanen und nicht in einer Schleife wiederholen.
MONTHLY_CAP_EXCEEDED429Bis zum Reset: Mitternacht UTC bei der Tagesgrenze, der 1. beim Monatskontingent. Retry-After nennt die verbleibenden Sekunden und error.resetsAt den Zeitpunkt. Warten Sie nur, wenn er Sekunden entfernt ist, bei einem Aufruf kurz vor einem Reset. Sonst anhalten, die Arbeit für danach einplanen und nicht in einer Schleife wiederholen.
DEPENDENCY_UNAVAILABLE503Exponentielles Backoff mit Jitter.
MAINTENANCE503Exponentielles Backoff mit Jitter.
INTERNAL_ERROR500Exponentielles Backoff mit Jitter.

Das sind Antworten, keine Störungen. Sie zu wiederholen kostet Anfragen für denselben Satz, und in einer Schleife erreicht ein Schlüssel so bis mittags sein Tageslimit.

GrundStatusWas tatsächlich hilft
KEY_INVALID401Check the key for a typo, or create a new one in your account.
KEY_DISABLED401Re-enable it in your account, or use another key.
KEY_EXPIRED401Create a new key in your account.
PLAN_REQUIRED403Upgrade the plan. A new key on the same plan will get the same answer.
ACCOUNT_SUSPENDED403Contact support. Rotating the key will not help.
PAGINATION_DEPTH_EXCEEDED403Narrow the list with filters, or upgrade for deeper paging.
INVALID_PARAMETER400Correct the named fields and send the request again.
RESOURCE_NOT_FOUND404Check the id. The list and search endpoints return valid ones.

401 betrifft die Zugangsdaten, 403 das Erlaubte. Ein neuer Schlüssel behebt das Erste und ändert am Zweiten nichts. Authentifizierung zieht die Linie vollständig.

Drei Regeln: nur wiederholbare Gründe, das serverseitige Retry-After wo gesendet, und Jitter, damit eine Flotte, die gemeinsam gescheitert ist, nicht gemeinsam wiederholt. Ein verbrauchtes Tages- oder Monatskontingent ist die Ausnahme: warten Sie nur, wenn sein Reset Sekunden entfernt ist, sonst halten Sie an.

call.ts
javascript
// Retry the failures that can succeed later, and only those.const RETRYABLE = new Set(['RATE_LIMITED', 'PAGINATION_BURST', 'DAILY_CAP_EXCEEDED', 'MONTHLY_CAP_EXCEEDED', 'DEPENDENCY_UNAVAILABLE', 'MAINTENANCE', 'INTERNAL_ERROR']) // A spent daily or monthly allowance resets at midnight UTC or on the 1st.// Wait for it only when it is seconds away, as the SDKs do (30 seconds by// default). Never sleep through a day or a month.const ALLOWANCES = new Set(['DAILY_CAP_EXCEEDED', 'MONTHLY_CAP_EXCEEDED'])const MAX_WAIT_SECONDS = 30 async function call(request, attempt = 0) {  const res = await fetch(request)  if (res.ok) return res   const body = await res.json().catch(() => null)  const reason = body?.error?.reason   // A reason the API says is permanent. Sending it again is a  // request spent to receive the same sentence.  if (!RETRYABLE.has(reason) || attempt >= 4) throw new GunSpecError(res.status, body)   // Honour the server's own number when it sends one: it knows when  // the window resets and a guess does not.  const retryAfter = Number(res.headers.get('Retry-After')) || 0   // The allowance is spent and the reset is far off. Nothing is served  // before it, so sleeping here is a stuck request, not a retry. Stop, and  // run the work again after the instant the API names.  if (ALLOWANCES.has(reason) && retryAfter > MAX_WAIT_SECONDS) {    throw new LimitReached(reason, body.error.resetsAt)  }   const backoff = retryAfter * 1000 || 2 ** attempt * 500   // Jitter, or every client that failed together retries together.  await sleep(backoff + Math.random() * 250)  return call(request, attempt + 1)}

Die offiziellen SDKs tun das bereits: maxRetries steht auf 2, Retry-After wird bis maxRetryAfterMs beachtet (eine Ablehnung, deren Reset weiter entfernt ist, wird sofort als Fehler ausgelöst und nie ausgesessen), und jeder Fehler trägt reason und requestId. Stellen Sie es über retry am Client ein, statt es zu umwickeln. Siehe die SDK-Referenz.

Eine Seite, die ohne unsere Daten rendert, ist besser als eine, die gar nicht rendert. Entscheiden Sie das einmal an der Grenze, nicht in jeder Komponente.

firearm-for-page.ts
javascript
// What the reader sees while this is going wrong.try {  return await gunspec.firearms.get(id)} catch (err) {  // A copy from ten minutes ago is a better page than an error, and  // the catalog changes on the order of days.  const stale = await cache.get(id)  if (stale) return { ...stale, stale: true }   // Nothing cached. Say what failed, quote the request id, and let  // the page render the rest of itself.  logger.warn({ requestId: err.requestId, reason: err.reason }, 'gunspec unavailable')  return null}
  • Liefern Sie eine veraltete Kopie, bevor Sie einen Fehler liefern. Der Katalog bewegt sich in Tagen, also sind zehn Minuten alte Daten keine Lüge.
  • Lassen Sie einen Abschnitt scheitern, nicht die Seite: nicht erreichbare Händler dürfen die Datentabelle nicht mitnehmen.
  • Setzen Sie jedem Aufruf ein Timeout. Eine Anfrage, die nie zurückkehrt, ist schlimmer als eine, die scheitert, weil danach nichts mehr entscheiden kann.
  • Wiederholen Sie einen Schreibvorgang nie nur wegen einer langsamen Antwort, solange Sie keine Idempotenz haben. Ein Duplikat ist ein echter Schaden, eine Verzögerung nicht.

Jede Antwort trägt X-Request-Id, und jeder SDK-Fehler trägt sie als requestId. Protokollieren Sie sie. Damit finden wir genau diese Anfrage; ohne sie ist eine Meldung die Beschreibung einer Seite. Support und Feedback ist der Weg dorthin.