From 3f2758610cf8c217f4c51447b22203410594c682 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sat, 1 Aug 2026 13:45:03 +0200 Subject: [PATCH] Ticketanizer-Versand: automatische Wiederholung bei Rate-Limit (429) Fremde Bewerber koennen theoretisch kurz hintereinander absenden (z.B. wenn Dogi live "bewerbt euch jetzt" sagt) und dabei dasselbe Rate-Limit treffen, das beim Testen aufgefallen ist. Jetzt wird ein 429 automatisch bis zu 2x mit kurzer Pause wiederholt (Retry-After-Header wird beachtet, sonst 2s/5s Standard-Wartezeit), bevor endgueltig aufgegeben wird - laeuft komplett im Hintergrund ueber ctx.waitUntil, blockiert nie die Antwort an den Bewerber. Die Bewerbung selbst landet ohnehin immer sicher in der eigenen Datenbank, unabhaengig vom Ticketanizer-Ergebnis. Getestet mit 3 gleichzeitigen Bewerbungen - alle liefen sauber durch. --- cloudflare-worker/src/lib/ticketanizer.js | 62 ++++++++++++++++++----- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/cloudflare-worker/src/lib/ticketanizer.js b/cloudflare-worker/src/lib/ticketanizer.js index bc2fe54..2bace73 100644 --- a/cloudflare-worker/src/lib/ticketanizer.js +++ b/cloudflare-worker/src/lib/ticketanizer.js @@ -26,6 +26,14 @@ Rein informativ, kein Ausfallpunkt: Fehlt der API-Key, oder schlägt der Versand fehl (Ticketanizer down, falscher Key, ...), wird die Bewerbung trotzdem ganz normal in der eigenen Datenbank gespeichert. + + Automatische Wiederholung bei Rate-Limit (HTTP 429): Wenn mehrere + Menschen kurz hintereinander ein Formular abschicken, kann Ticketanizer + kurzzeitig 429 antworten (selbst erlebt beim Testen). In dem Fall wird + der Versand automatisch bis zu 2x mit kurzer Pause wiederholt (siehe + ticketAnfrageSenden), bevor endgültig aufgegeben wird — läuft komplett + im Hintergrund über ctx.waitUntil, blockiert also nie die Antwort an + die Bewerberin/den Bewerber. ===================================================================== */ const TICKETANIZER_BASIS_URL = "https://api.ticketanizer.com"; @@ -67,6 +75,47 @@ function ersteNachrichtAufbauen(type, data) { return text.length > 3500 ? text.slice(0, 3500) + "…" : text; } +const warten = (ms) => new Promise((fertig) => setTimeout(fertig, ms)); + +// Wenn viele Menschen kurz hintereinander ein Formular abschicken (z.B. weil +// Dogi gerade live "bewerbt euch jetzt!" sagt), kann Ticketanizer mit +// HTTP 429 ("zu viele Anfragen") antworten. Die Bewerbung selbst ist davon +// nie betroffen (steht schon sicher in unserer DB) — hier wird nur der +// Ticket-Versand selbst automatisch 1–2x mit kurzer Pause wiederholt, +// bevor endgültig aufgegeben wird. Nutzt Retry-After, falls Ticketanizer +// das mitschickt, sonst eine feste, ansteigende Wartezeit. +const MAX_VERSUCHE = 3; +const STANDARD_WARTEZEITEN_MS = [2000, 5000]; + +async function ticketAnfrageSenden(apiKey, panelId, visitorName, firstMessage) { + for (let versuch = 1; versuch <= MAX_VERSUCHE; versuch++) { + const antwort = await fetch(`${TICKETANIZER_BASIS_URL}/v1/tickets`, { + method: "POST", + headers: { + "X-API-Key": apiKey, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + panel_id: panelId, + visitor_name: visitorName, + first_message: firstMessage, + }), + }); + + if (antwort.status !== 429 || versuch === MAX_VERSUCHE) { + return antwort; + } + + const retryAfterHeader = Number(antwort.headers.get("Retry-After")); + const wartezeit = Number.isFinite(retryAfterHeader) && retryAfterHeader > 0 + ? retryAfterHeader * 1000 + : STANDARD_WARTEZEITEN_MS[versuch - 1] || 5000; + + console.log(`[ticketanizer] Rate-Limit (Versuch ${versuch}/${MAX_VERSUCHE}) — warte ${wartezeit}ms und versuche es erneut.`); + await warten(wartezeit); + } +} + export async function bewerbungAlsTicketAnlegen(env, type, data) { const apiKey = env.TICKETANIZER_API_KEY; const panelId = PANEL_IDS[type]; @@ -83,18 +132,7 @@ export async function bewerbungAlsTicketAnlegen(env, type, data) { const firstMessage = ersteNachrichtAufbauen(type, data); try { - const antwort = await fetch(`${TICKETANIZER_BASIS_URL}/v1/tickets`, { - method: "POST", - headers: { - "X-API-Key": apiKey, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - panel_id: panelId, - visitor_name: visitorName, - first_message: firstMessage, - }), - }); + const antwort = await ticketAnfrageSenden(apiKey, panelId, visitorName, firstMessage); const antwortText = await antwort.text().catch(() => ""); if (!antwort.ok) { console.error(`[ticketanizer] Fehler beim Ticket-Anlegen (${type}, panel ${panelId}): HTTP ${antwort.status} — ${antwortText}`);