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.
This commit is contained in:
2026-08-01 13:45:03 +02:00
parent 060f6a8fe6
commit 3f2758610c
+50 -12
View File
@@ -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 12x 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}`);