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:
1 parent
820ccd510d
commit
9ebc562c83
3 files changed
+234
-11
No files matched your search
+65
-10
@@ -7,6 +7,14 @@ export const BAD_REPLY_TEXT = 'Der Hustler-Dienst antwortet nicht richtig.'
|
||||
export const SESSION_TEXT = 'Anmeldung abgelaufen. Bitte neu anmelden.'
|
||||
export const FORBIDDEN_TEXT = 'Dafür fehlt dir die Berechtigung.'
|
||||
|
||||
// Zeitgrenze je Abruf (JWT holen, senden, Antwort lesen): ohne sie bliebe ein Abruf auf toter Verbindung (Rechner im Ruhezustand,
|
||||
// Netzwechsel) minutenlang offen und der Feed (ein Abruf je Schleife) stünde still, ohne Fehlermeldung.
|
||||
export const REQUEST_TIMEOUT_MS = 20000
|
||||
// Nach einer 401-Folge (frisches JWT wird ebenfalls abgelehnt oder lässt sich nicht erzeugen) wird so lange kein frisches JWT mehr
|
||||
// erzwungen: Appwrite begrenzt createJWT je Nutzer, und die Schleifen (5 s / 15 s) würden das Limit binnen Minuten aufbrauchen
|
||||
// und damit auch Serverpuls, Finanzen und Vorschauen sperren. 2 Minuten ergeben höchstens 30 Versuche je Stunde und Tab.
|
||||
export const AUTH_COOLDOWN_MS = 120000
|
||||
|
||||
// Pult-Status des Dienstes (Übersicht §4.11)
|
||||
export const PULT_STATUS = Object.freeze(['offen', 'in_arbeit', 'gesendet', 'verworfen', 'veraltet', 'erledigt'])
|
||||
// Erlaubte Aktionen am Pult-Eintrag; alles andere wird abgewiesen, bevor ein Pfad entsteht
|
||||
@@ -46,13 +54,35 @@ function pultStatusList(status) {
|
||||
return list
|
||||
}
|
||||
|
||||
export function createWorkersApi({ getJwt, fetchImpl = (...args) => globalThis.fetch(...args) }) {
|
||||
export function createWorkersApi({
|
||||
getJwt,
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
timeoutMs = REQUEST_TIMEOUT_MS,
|
||||
authCooldownMs = AUTH_COOLDOWN_MS,
|
||||
now = Date.now,
|
||||
}) {
|
||||
let freshBlockedUntil = 0 // bis dahin wird nach einer 401-Antwort kein frisches JWT erzeugt (AUTH_COOLDOWN_MS)
|
||||
|
||||
// Führt fn(signal) mit Zeitgrenze aus: nach timeoutMs scheitert der Aufruf mit DOWN_TEXT (status 0) und das Signal wird
|
||||
// abgebrochen. Das gilt auch, wenn fn das Signal nicht beachtet (hängendes createJWT, fetchImpl ohne Abbruch-Unterstützung).
|
||||
function withDeadline(fn) {
|
||||
const controller = new AbortController()
|
||||
let timer
|
||||
const expired = new Promise((_, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
reject(statusError(DOWN_TEXT, 0)) // zuerst ablehnen, dann abbrechen: das Abbruch-Ergebnis von fn soll nicht gewinnen
|
||||
controller.abort()
|
||||
}, timeoutMs)
|
||||
})
|
||||
return Promise.race([fn(controller.signal), expired]).finally(() => clearTimeout(timer))
|
||||
}
|
||||
|
||||
// fetch wirft (Netz, Dienst aus) ⇒ DOWN_TEXT mit status 0
|
||||
async function send(path, method, body, jwt) {
|
||||
async function send(path, method, body, jwt, signal) {
|
||||
const headers = { 'X-Appwrite-JWT': jwt }
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json'
|
||||
try {
|
||||
return await fetchImpl(`${BASE}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) })
|
||||
return await fetchImpl(`${BASE}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal })
|
||||
} catch {
|
||||
throw statusError(DOWN_TEXT, 0)
|
||||
}
|
||||
@@ -66,13 +96,8 @@ export function createWorkersApi({ getJwt, fetchImpl = (...args) => globalThis.f
|
||||
}
|
||||
}
|
||||
|
||||
async function request(path, { method = 'GET', body } = {}) {
|
||||
let res = await send(path, method, body, await jwtFor())
|
||||
if (res.status === 401) {
|
||||
// einmal mit frischem JWT wiederholen, danach gilt die Sitzung als abgelaufen
|
||||
res = await send(path, method, body, await jwtFor({ fresh: true }))
|
||||
if (res.status === 401) throw statusError(SESSION_TEXT, 401)
|
||||
}
|
||||
// Antwort auswerten (kein 401): Daten oder Error mit deutscher Meldung
|
||||
async function read(res) {
|
||||
let data
|
||||
try {
|
||||
data = await res.json()
|
||||
@@ -90,6 +115,36 @@ export function createWorkersApi({ getJwt, fetchImpl = (...args) => globalThis.f
|
||||
return data ?? {}
|
||||
}
|
||||
|
||||
// Ein Versuch mit Zeitgrenze: JWT holen, senden, auswerten. { unauthorized: true } bei 401, sonst { data }.
|
||||
function attempt(path, method, body, options) {
|
||||
return withDeadline(async (signal) => {
|
||||
const res = await send(path, method, body, await jwtFor(options), signal)
|
||||
if (res.status === 401) return { unauthorized: true }
|
||||
freshBlockedUntil = 0 // der Dienst hat das JWT angenommen: die nächste 401-Folge darf wieder einmal erneuern
|
||||
return { data: await read(res) }
|
||||
})
|
||||
}
|
||||
|
||||
async function request(path, { method = 'GET', body } = {}) {
|
||||
let out = await attempt(path, method, body)
|
||||
if (out.unauthorized) {
|
||||
// Während der Sperre gilt die 401 sofort als abgelaufene Sitzung (kein createJWT, keine zweite Anfrage)
|
||||
if (now() < freshBlockedUntil) throw statusError(SESSION_TEXT, 401)
|
||||
// sonst einmal mit frischem JWT wiederholen, danach gilt die Sitzung als abgelaufen und die Sperre beginnt
|
||||
try {
|
||||
out = await attempt(path, method, body, { fresh: true })
|
||||
} catch (err) {
|
||||
if (err && err.status === 401) freshBlockedUntil = now() + authCooldownMs // auch das frische JWT ließ sich nicht erzeugen
|
||||
throw err
|
||||
}
|
||||
if (out.unauthorized) {
|
||||
freshBlockedUntil = now() + authCooldownMs
|
||||
throw statusError(SESSION_TEXT, 401)
|
||||
}
|
||||
}
|
||||
return out.data
|
||||
}
|
||||
|
||||
return {
|
||||
state: () => request('/state'),
|
||||
events: (after, limit = EVENTS_LIMIT) => request(`/events${query([['after', after], ['limit', clampLimit(limit, EVENTS_LIMIT)]])}`),
|
||||
|
||||
Reference in new issue
Block a user