Workers P2: Zeitgrenze je Abruf und Sperre für frische JWTs nach einer 401-Folge

Endprüfung P2. Ein Abruf auf toter Verbindung (Rechner im Ruhezustand, Netzwechsel) blieb offen, und die
Feed-Schleifen laufen mit einem Abruf je Schleife: die Seite stand still und zeigte veraltete Daten als aktuell.
createWorkersApi bricht jetzt jeden Versuch (JWT holen, senden, Antwort lesen) nach 20 s mit "Der Hustler-Dienst ist
gerade nicht erreichbar." (status 0) ab, auch wenn fetch oder createJWT das Signal nicht beachten.

Außerdem erzwang jede 401-Antwort ein frisches createJWT, bei dauerhaftem 401 etwa 16 bis 20 Mal je Minute und Tab.
Appwrite begrenzt createJWT je Nutzer; das Limit hätte auch Serverpuls, Finanzen und Vorschauen gesperrt. Nach einer
401-Folge (frisches JWT wird ebenfalls abgelehnt oder lässt sich nicht erzeugen) gibt es 2 Minuten lang kein frisches
JWT mehr, die Meldung "Anmeldung abgelaufen" bleibt; Erfolg hebt die Sperre auf.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 05:34:51 +00:00
1 parent 820ccd510d
commit 9ebc562c83
3 files changed
+234 -11

No files matched your search

+132 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// API-Client der Hustler-Seite: URLs, Methoden, Header, deutsche Fehlertexte, JWT-Wiederholung, Pult-Prüfungen
const { createWorkersApi } = await loadModule('src/lib/workersApi.js')
const { createWorkersApi, REQUEST_TIMEOUT_MS, AUTH_COOLDOWN_MS } = await loadModule('src/lib/workersApi.js')
const jwts = []
const getJwt = async (o) => {
@@ -144,3 +144,134 @@ test('limit wird geklemmt (Ereignisse 1..200, Spur 1..50)', async () => {
'/api/workers/events?limit=150',
])
})
// ---- Endprüfung P2: Zeitgrenze je Abruf und Sperre für frische JWTs nach einer 401-Folge ----
const DOWN = 'Der Hustler-Dienst ist gerade nicht erreichbar.'
const SESSION = 'Anmeldung abgelaufen. Bitte neu anmelden.'
test('Zeitgrenze: ein hängender fetch endet mit der Dienst-Meldung (status 0), das Signal wird abgebrochen', async () => {
const signals = []
const hang = (url, init) => {
signals.push(init.signal)
return new Promise(() => {}) // beachtet das Signal nicht einmal: die Zeitgrenze muss trotzdem greifen
}
const api = createWorkersApi({ getJwt, fetchImpl: hang, timeoutMs: 30 })
const t0 = Date.now()
await assert.rejects(api.state(), { message: DOWN, status: 0 })
assert.ok(Date.now() - t0 < 2000)
assert.equal(signals.length, 1)
assert.equal(signals[0].aborted, true)
// jeder Aufruf hat seine eigene Zeitgrenze (hier: Schreibaufruf und Spur)
await assert.rejects(api.pause(true), { message: DOWN, status: 0 })
await assert.rejects(api.trace('kim'), { message: DOWN, status: 0 })
assert.equal(signals.length, 3)
})
test('Zeitgrenze: auch eine Antwort, deren Rumpf nie ankommt, und ein hängendes createJWT enden mit der Dienst-Meldung', async () => {
const stalledBody = async () => ({ ok: true, status: 200, json: () => new Promise(() => {}) })
await assert.rejects(createWorkersApi({ getJwt, fetchImpl: stalledBody, timeoutMs: 30 }).state(), { message: DOWN, status: 0 })
const hungJwt = () => new Promise(() => {})
const f = fakeFetch(okReplies(1))
await assert.rejects(createWorkersApi({ getJwt: hungJwt, fetchImpl: f, timeoutMs: 30 }).state(), { message: DOWN, status: 0 })
assert.equal(f.calls.length, 0) // ohne JWT ging nichts hinaus
})
test('Zeitgrenze: rechtzeitige Antworten bleiben unberührt, ein späterer Abruf klappt wieder (Schleife wird frei)', async () => {
let n = 0
const flaky = async () => {
n += 1
if (n === 1) return new Promise(() => {})
return { ok: true, status: 200, json: async () => ({ n }) }
}
const api = createWorkersApi({ getJwt, fetchImpl: flaky, timeoutMs: 40 })
await assert.rejects(api.state(), { message: DOWN, status: 0 })
assert.deepEqual(await api.state(), { n: 2 })
const quick = createWorkersApi({ getJwt, fetchImpl: fakeFetch(okReplies(1)) }) // Standard-Zeitgrenze 20 s: wird nach der Antwort aufgeräumt
assert.deepEqual(await quick.state(), {})
})
test('Zeitgrenze: Standardwerte 20 s und 2 min', () => {
assert.equal(REQUEST_TIMEOUT_MS, 20000)
assert.equal(AUTH_COOLDOWN_MS, 120000)
})
test('401-Folge: danach kein frisches JWT mehr, bis die Sperre abgelaufen ist (höchstens ein createJWT je Sperre)', async () => {
jwts.length = 0
let t = 1_000_000
const calls = []
const always401 = async (url, init) => {
calls.push(init.headers['X-Appwrite-JWT'])
return { ok: false, status: 401, json: async () => ({}) }
}
const api = createWorkersApi({ getJwt, fetchImpl: always401, now: () => t, authCooldownMs: 60000 })
await assert.rejects(api.state(), { message: SESSION, status: 401 })
assert.deepEqual(jwts, [false, true])
assert.deepEqual(calls, ['jwt-alt', 'jwt-neu']) // Versuch und Wiederholung
// innerhalb der Sperre: je Abruf genau eine Anfrage mit dem gespeicherten JWT, nie ein frisches, die Meldung bleibt
for (let i = 0; i < 20; i += 1) {
t += 2900 // 20 Schleifendurchläufe innerhalb der Sperre von 60 s (Zustand, Ereignisse und Spur zusammen)
await assert.rejects(api.events(null), { message: SESSION, status: 401 })
}
assert.equal(jwts.filter((fresh) => fresh).length, 1)
assert.equal(calls.length, 2 + 20)
// nach Ablauf der Sperre wird wieder einmal erneuert, danach beginnt die nächste Sperre
t += 2100
await assert.rejects(api.state(), { message: SESSION, status: 401 })
assert.equal(jwts.filter((fresh) => fresh).length, 2)
t += 5000
await assert.rejects(api.trace('kim'), { message: SESSION, status: 401 })
assert.equal(jwts.filter((fresh) => fresh).length, 2)
})
test('401-Folge: Erfolg hebt die Sperre auf, ein einzelnes 401 danach wird wieder mit frischem JWT wiederholt', async () => {
jwts.length = 0
let t = 0
const replies = [
{ status: 401, body: {} }, { status: 401, body: {} }, // Folge ⇒ Sperre
{ status: 200, body: { ok: 1 } }, // Dienst nimmt das gespeicherte JWT wieder an ⇒ Sperre weg
{ status: 401, body: {} }, { status: 200, body: { ok: 2 } }, // einzelnes 401 ⇒ Wiederholung mit frischem JWT hilft
]
const api = createWorkersApi({ getJwt, fetchImpl: fakeFetch(replies), now: () => t })
await assert.rejects(api.state(), { status: 401 })
t += 5000
assert.deepEqual(await api.state(), { ok: 1 })
assert.deepEqual(await api.state(), { ok: 2 })
assert.deepEqual(jwts, [false, true, false, false, true])
})
test('401-Folge: scheitert schon die Erzeugung des frischen JWT, beginnt dieselbe Sperre', async () => {
let t = 0
const mints = []
const brokenFresh = async (o) => {
mints.push(o?.fresh === true)
if (o?.fresh) throw new Error('429 Too Many Requests')
return 'jwt-alt'
}
const api = createWorkersApi({ getJwt: brokenFresh, fetchImpl: fakeFetch(Array.from({ length: 6 }, () => ({ status: 401, body: {} }))), now: () => t, authCooldownMs: 60000 })
await assert.rejects(api.state(), { message: SESSION, status: 401 })
t += 10000
await assert.rejects(api.state(), { message: SESSION, status: 401 })
t += 10000
await assert.rejects(api.state(), { message: SESSION, status: 401 })
assert.deepEqual(mints, [false, true, false, false]) // ein einziger Versuch, ein frisches JWT zu erzeugen
t += 60000
await assert.rejects(api.state(), { message: SESSION, status: 401 })
assert.deepEqual(mints, [false, true, false, false, false, true])
})
test('401-Folge: andere Fehler (403, 500, 502) heben die Sperre auf, weil der Dienst das JWT angenommen hat', async () => {
jwts.length = 0
let t = 0
const api = createWorkersApi({
getJwt,
fetchImpl: fakeFetch([{ status: 401, body: {} }, { status: 401, body: {} }, { status: 403, body: {} }, { status: 401, body: {} }, { status: 200, body: {} }]),
now: () => t,
})
await assert.rejects(api.state(), { status: 401 })
await assert.rejects(api.state(), { status: 403 })
assert.deepEqual(await api.state(), {}) // 401, Wiederholung frisch ⇒ 200
assert.deepEqual(jwts, [false, true, false, false, true])
})