/* ===================================================================== DER GEMEINSAME ABRUF — und die Regel, wann ein 404 hinauswirft ===================================================================== ENTSTANDEN AUS EINEM ECHTEN FEHLER (22.09.2026). Filipe: „wenn ich auf diese sachen drücke werd ich auf die startseite geschickt, scheck das bitte ab." Gemeint waren die Stufen-Knöpfe auf der Team-Lage. Im Browser nachgestellt: Druck auf „Standard" → PUT /workspace/api/teamlage/stufe 404 → Adresse springt auf /workspace/start.html Die Ursache stand in ACHT Dateien, Zeichen für Zeichen dieselbe: if (a.status === 404) { location.assign('/workspace/start.html'); … } Gemeint war: „Du darfst diese Seite nicht sehen — hier ist der Ausgang." Geschrieben stand: „Irgendein Abruf hat 404 geliefert — raus mit dir." Eine einzelne Handlung, die nicht klappt, warf damit den Menschen von einer Seite, auf der alles andere in Ordnung war — und er sah nicht einmal, was schiefging, weil die Meldung auf der Seite erschienen wäre, die gerade verschwindet. --------------------------------------------------------------------- WARUM DAS EINE DAUERHAFTE PRÜFUNG BRAUCHT Weil die Regel eine ZEITLICHE ist und sich deshalb nicht ansehen lässt: „Ein 404 wirft nur hinaus, solange die Seite noch gar nichts bekommen hat." Wer die Datei liest, sieht drei Zeilen und findet sie plausibel. Ob sie stimmen, zeigt erst der Ablauf — und zwar in BEIDE Richtungen. Eine Prüfung, die nur den Rauswurf misst, wäre mit der alten, falschen Fassung ebenfalls grün gewesen. Deshalb misst diese Prüfung fünf Wege durch dieselbe Funktion und führt danach dieselben fünf Messungen noch einmal gegen die ALTE Fassung — sie MUSS dort durchfallen. Sonst wäre nicht bewiesen, dass sie den Unterschied überhaupt bemerkt. Und sie misst den zweiten Teil, der genauso wichtig ist: dass die acht Seiten die gemeinsame Datei wirklich laden, und zwar VOR ihrem eigenen Skript. Eine Regel, die auf einer Seite nicht geladen wird, ist dort keine Regel — sondern ein „holen is not a function". ===================================================================== */ import { readFileSync, readdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { createContext, runInContext } from "node:vm"; import { notbremse } from "./helfer-notbremse.mjs"; notbremse(60_000, "pruef-holen"); const HIER = dirname(fileURLToPath(import.meta.url)); const WORKSPACE = join(HIER, "..", "workspace"); const SKRIPTE = join(WORKSPACE, "assets", "js"); let fehler = 0, geprueft = 0, offen = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; const unklar = (t) => { offen++; console.log(" -- " + t); }; console.log("=== Der gemeinsame Abruf (holen.js) ==="); console.log(""); /* ===================================================================== 1. DAS VERHALTEN Die echte Datei wird ausgeführt, nicht nachgebaut. Ein Nachbau würde beweisen, dass der Nachbau stimmt. ===================================================================== */ console.log("--- Wie sich holen() verhält ---"); let quelle = null; try { quelle = readFileSync(join(SKRIPTE, "holen.js"), "utf8"); } catch (f) { unklar(`assets/js/holen.js ist nicht lesbar (${f.code || f.message})` + " — ohne die Datei lässt sich hier nichts messen."); } /* Die ALTE Fassung, wörtlich so, wie sie in den acht Dateien stand. Sie ist die Gegenprobe: Dieselben Messungen müssen an ihr scheitern. */ const ALTE_FASSUNG = ` window.holen = async function holen(pfad, optionen = {}) { const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('nicht erlaubt'); } return a; }; `; /** Führt eine Fassung aus und gibt eine Prüfbank zurück. */ function bank(js) { const gesprungen = []; const welt = { window: {}, location: { assign: (z) => gesprungen.push(z) }, fetch: null, console, }; welt.globalThis = welt; const raum = createContext(welt); runInContext(js, raum, { timeout: 5000 }); return { gesprungen, /** Setzt die nächste Antwort und ruft holen() auf. */ async ruf(status, optionen) { welt.fetch = async () => ({ status, ok: status >= 200 && status < 300 }); try { return { antwort: await welt.window.holen("/irgendwo", optionen), wurf: null }; } catch (f) { return { antwort: null, wurf: String(f && f.message) }; } }, }; } /** Die fünf Wege. Gibt zurück, was dabei herauskam — als Text, damit * beide Fassungen mit derselben Elle gemessen werden. */ async function fuenfWege(js) { const b = bank(js); const erg = {}; /* (1) Der erste Abruf einer Seite läuft ins Leere: Die Seite gibt es für diesen Menschen nicht. Hier IST der Sprung richtig. */ const a1 = await b.ruf(404, undefined); erg.ersterGetSpringt = b.gesprungen.at(-1) === "/workspace/start.html" && a1.antwort === null; /* (2) Eine Seite, die steht, und danach eine Handlung, die nicht klappt. Hier darf NICHTS springen. */ const b2 = bank(js); await b2.ruf(200, undefined); const vorher2 = b2.gesprungen.length; const a2 = await b2.ruf(404, { method: "PUT" }); erg.handlungSpringtNicht = b2.gesprungen.length === vorher2 && a2.antwort !== null && a2.antwort.status === 404; /* (3) Auch ein späterer GET darf nicht mehr werfen — die Seite steht ja. Ein Nachladen, das ins Leere geht, ist eine Meldung wert, kein Rauswurf. */ const b3 = bank(js); await b3.ruf(200, undefined); const vorher3 = b3.gesprungen.length; await b3.ruf(404, undefined); erg.spaeterGetSpringtNicht = b3.gesprungen.length === vorher3; /* (4) 401 wirft IMMER zur Anmeldung — auch mitten in der Arbeit. Wer abgemeldet ist, kann nirgends mehr etwas tun. */ const b4 = bank(js); await b4.ruf(200, undefined); const a4 = await b4.ruf(401, { method: "POST" }); erg.abgemeldetSpringtImmer = b4.gesprungen.at(-1) === "/workspace/" && a4.antwort === null; /* (5) Der Normalfall: eine Antwort kommt zurück, nichts springt. */ const b5 = bank(js); const a5 = await b5.ruf(200, undefined); erg.normalfallRuhig = b5.gesprungen.length === 0 && a5.antwort !== null && a5.antwort.status === 200; return erg; } const WEGE = { ersterGetSpringt: "ein 404 auf den ERSTEN Abruf wirft zur Startseite", handlungSpringtNicht: "ein 404 auf eine Handlung (PUT) wirft NICHT — die Seite bleibt stehen", spaeterGetSpringtNicht: "ein 404 auf einen späteren Abruf wirft ebenfalls nicht", abgemeldetSpringtImmer: "ein 401 wirft immer zur Anmeldung, auch mitten in der Arbeit", normalfallRuhig: "der Normalfall gibt die Antwort zurück und springt nirgends", }; let neuErg = null; if (quelle) { neuErg = await fuenfWege(quelle); for (const [schluessel, satz] of Object.entries(WEGE)) { ok(neuErg[schluessel] === true, satz); } } /* ---- Die Gegenprobe -------------------------------------------------- Dieselben fünf Messungen an der alten Fassung. Zwei davon MÜSSEN dort umschlagen — sonst misst diese Prüfung nicht, was sie behauptet. */ console.log(""); console.log("--- Gegenprobe: dieselben Messungen an der alten Fassung ---"); const altErg = await fuenfWege(ALTE_FASSUNG); const umgeschlagen = Object.keys(WEGE).filter((k) => altErg[k] !== true); ok(umgeschlagen.length === 2 && umgeschlagen.includes("handlungSpringtNicht") && umgeschlagen.includes("spaeterGetSpringtNicht"), `die alte Fassung fällt bei genau den zwei Wegen durch, um die es ging` + ` (${umgeschlagen.length}: ${umgeschlagen.join(", ") || "keinem"})`); ok(altErg.ersterGetSpringt === true && altErg.abgemeldetSpringtImmer === true && altErg.normalfallRuhig === true, "und sie besteht die drei anderen — gemessen wird also die REGEL," + " nicht einfach die Frage, ob es die neue Datei ist" + " (eine Prüfung, die jede alte Fassung verwirft, prüft nichts)"); /* ===================================================================== 2. DIE EINBINDUNG ABGELEITET, NICHT AUFGEZÄHLT: Welche Seitenskripte den gemeinsamen Abruf benutzen, steht in den Dateien selbst. Eine Liste hier wäre die, die beim neunten Skript nicht mitwächst. ===================================================================== */ console.log(""); console.log("--- Wird holen.js überall geladen, und zwar zuerst? ---"); const skriptNamen = readdirSync(SKRIPTE).filter((n) => n.endsWith(".js")); const nutzer = []; for (const name of skriptNamen) { if (name === "holen.js") continue; const text = readFileSync(join(SKRIPTE, name), "utf8"); if (/window\.holen\s*\(/.test(text)) nutzer.push({ name, text }); } ok(nutzer.length >= 8, `${nutzer.length} Seitenskripte benutzen den gemeinsamen Abruf` + ` (${nutzer.map((n) => n.name.replace(/\.js$/, "")).join(", ")})`); /* KEINE EIGENE KOPIE DER REGEL MEHR. --------------------------------------------------------------------- DIE ERSTE FASSUNG DIESER PRÜFUNG WAR ZU STRENG — und das ist selbst ein Befund wert. Sie verbot jedes `location.assign('/workspace/ start.html')` in einem Seitenskript und wurde sofort rot, bei bereich.js und team.js. Nachgesehen: Alle drei Stellen sind richtig und sollen bleiben. bereich.js `?b=` ist kein gültiger Bereichsname → es gibt die Seite nicht bereich.js der erste Abruf des Bereichs ist nicht ok → diese Person sieht diesen Bereich nicht team.js die Rolle passt nicht → leere Seite vermeiden Ein Sprung zur Startseite ist nicht das Problem. Das Problem war, dass die ZEITLICHE Regel („nur solange die Seite noch nichts hat") achtmal nachgebaut danebenstand. Eine Prüfung, die den gesunden Fall bestraft, wird abgeschaltet — und mit ihr die Warnung, auf die es ankommt. Verboten ist deshalb genau zweierlei: ein eigener `hole()`-Helfer neben dem gemeinsamen, und die Paarung „404 → Startseite" in einem Seitenskript. Beides ist die Dopplung, nichts sonst. --------------------------------------------------------------------- */ const eigenerHelfer = nutzer.filter((n) => /(async\s+)?function\s+hole\s*\(/.test(n.text)); ok(nutzer.length > 0 && eigenerHelfer.length === 0, `keines baut sich daneben einen eigenen hole()-Helfer` + ` (${eigenerHelfer.map((n) => n.name).join(", ") || "keines"})`); const mitEigenerRegel = nutzer.filter((n) => /status\s*===\s*404[\s\S]{0,120}?location\.assign\((['"])\/workspace\/start\.html\1\)/ .test(n.text)); ok(nutzer.length > 0 && mitEigenerRegel.length === 0, `und keines entscheidet selbst, was ein 404 bedeutet` + ` (${mitEigenerRegel.map((n) => n.name).join(", ") || "keines"})`); /* GEGENPROBE ZU DEN ZWEI REGELN OBEN. Zwei Suchen, die nichts finden, sehen genauso aus wie zwei Suchen, die nichts finden KOENNEN — das ist der leere `every()`-Fehler in anderer Gestalt. Also einmal an dem Text messen, den es zu verhindern gilt: der alten, acht Mal kopierten Fassung. Findet die Suche ihn nicht, ist sie kaputt. */ const ALTE_KOPIE = ` async function hole(pfad, optionen = {}) { const a = await fetch(pfad, { credentials: 'same-origin', ...optionen }); if (a.status === 401) { location.assign('/workspace/'); throw new Error('abgemeldet'); } if (a.status === 404) { location.assign('/workspace/start.html'); throw new Error('nicht erlaubt'); } return a; } `; ok(/(async\s+)?function\s+hole\s*\(/.test(ALTE_KOPIE) && /status\s*===\s*404[\s\S]{0,120}?location\.assign\((['"])\/workspace\/start\.html\1\)/ .test(ALTE_KOPIE), "Gegenprobe: beide Suchen finden die alte Kopie — sie koennen also anschlagen"); /* Und jetzt die Seiten. Welche HTML-Datei welches Skript lädt, steht in der HTML-Datei — auch das wird gelesen, nicht gepflegt. */ const seiten = readdirSync(WORKSPACE).filter((n) => n.endsWith(".html")); let geprueftePaare = 0; const ohneHolen = [], falscheReihenfolge = []; for (const seite of seiten) { const html = readFileSync(join(WORKSPACE, seite), "utf8"); for (const n of nutzer) { const stelle = html.indexOf(`assets/js/${n.name}`); if (stelle < 0) continue; geprueftePaare++; const holenStelle = html.indexOf("assets/js/holen.js"); if (holenStelle < 0) { ohneHolen.push(`${seite} → ${n.name}`); continue; } /* VOR dem Seitenskript. Beide tragen `defer`, also zählt die Reihenfolge im Quelltext — nicht die des Eintreffens. */ if (holenStelle > stelle) falscheReihenfolge.push(`${seite} → ${n.name}`); } } ok(geprueftePaare >= 8, `${geprueftePaare} Seite-Skript-Paare gefunden — es gibt also etwas zu prüfen`); ok(geprueftePaare > 0 && ohneHolen.length === 0, `jede dieser Seiten lädt holen.js (${ohneHolen.join(", ") || "keine Lücke"})`); ok(geprueftePaare > 0 && falscheReihenfolge.length === 0, `und immer VOR dem Seitenskript (${falscheReihenfolge.join(", ") || "überall richtig"})`); /* ---- Ende ------------------------------------------------------------ */ console.log(""); console.log(`${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"}` + ` -- ${geprueft} geprüft, ${fehler} Fehler, ${offen} nicht nachsehbar`); process.exit(fehler === 0 ? 0 : 1);