Express 5: geprueft, aber noch nicht umgestellt

Der Sprung 4 auf 5 entfernt Methoden, aendert die Pfadsyntax
grundlegend (path-to-regexp 8) und stellt Standardwerte um. Vieles
davon faellt nicht beim Start auf, sondern erst, wenn eine bestimmte
Adresse aufgerufen wird -- also im Betrieb, bei einem Kunden. Deshalb
zuerst pruefen statt installieren.

ZWEI PRUEFUNGEN, DIE SICH ERGAENZEN

pruef-express5.mjs durchsucht 89 Dateien nach den Bruchstellen aus dem
offiziellen Migrationsleitfaden: entfernte Aufrufe (res.send(zahl),
req.param, app.del, res.sendfile), die UMGEDREHTE Reihenfolge bei
res.redirect, geaenderte Pfadsyntax ("/*" ohne Namen, ":a?"),
schreibgeschuetztes req.query, entfernte static-Optionen.

server/test-express5.mjs startet einen echten Server mit genau unserem
Aufbau: Middleware-Kette mit eigenen Kopfzeilen, cookieParser,
express.json, eine umleitende Schranke, express.static, ein Router mit
Parameter, 404- und Fehlerbehandlung.

ERGEBNIS

15 von 15 Kategorien unbedenklich, 10 von 10 Laufpruefungen bestanden
gegen express 5.2.1. Der Umstieg waere ohne Codeaenderung moeglich.

Ein Fehlalarm lag dabei im Pruefer selbst: Er meldete drei
req.body-Zugriffe als ungesichert, die in Wahrheit innerhalb eines
"if (typeof req.body?.feld === 'boolean')" stehen -- der Rueckblick war
mit 60 Zeichen zu kurz fuer den Block. Ein Pruefer, der abgesicherte
Stellen anmahnt, kostet die Zeit, die er sparen soll, und beim
naechsten Mal glaubt man ihm auch die echten Funde nicht mehr.

NICHT UMGESTELLT

Bewusst. Der Bestand ist sicherheitstechnisch sauber (npm audit: 0),
express 4.22.2 wird weiter gepflegt, und der Nutzen waere gering
gegenueber dem Risiko, zwei laufende Dienste anzufassen. Die Vorarbeit
liegt vor -- wenn umgestellt wird, dann als eigener Vorgang mit
anschliessendem Live-Test, nicht nebenbei.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-26 20:19:25 +02:00
co-authored by Claude Opus 5
parent c03793b25f
commit 1c3b3808ba
2 changed files with 299 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
/* =====================================================================
Prüft, was ein Umstieg auf Express 5 an unserem Code brechen würde.
Der Sprung 4 → 5 ist keiner, den man "mal eben" macht: Er entfernt
Methoden, ändert das Verhalten bei Pfaden grundlegend (path-to-regexp
8) und stellt Standardwerte um. Vieles davon fällt nicht beim Start
auf, sondern erst, wenn eine bestimmte Adresse aufgerufen wird —
also im Betrieb, bei einem Kunden.
Deshalb wird hier NICHT installiert und ausprobiert, sondern zuerst
der Bestand durchsucht. Jede Fundstelle ist eine, die vor dem
Umstieg angefasst werden muss.
Grundlage: der offizielle Migrationsleitfaden
https://expressjs.com/en/guide/migrating-5.html
===================================================================== */
import fs from "fs";
import path from "path";
let treffer = 0, sauber = 0;
function dateien(ordner) {
const raus = [];
const gehe = (p) => {
for (const e of fs.readdirSync(p, { withFileTypes: true })) {
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
const voll = path.join(p, e.name);
if (e.isDirectory()) gehe(voll);
else if (e.name.endsWith(".js") || e.name.endsWith(".mjs")) raus.push(voll);
}
};
gehe(ordner);
return raus;
}
const QUELLEN = [
...dateien("server"),
...dateien("server-internal"),
];
/* Jede Regel: was gesucht wird, warum es bricht, was zu tun ist. */
const REGELN = [
{ name: "res.send(zahl) — entfernt",
muster: /res\.send\(\s*\d{3}\s*\)/g,
hinweis: "ersetzen durch res.sendStatus(zahl)" },
{ name: "res.json(objekt, status) — entfernt",
muster: /res\.json\([^)]*,\s*\d{3}\s*\)/g,
hinweis: "ersetzen durch res.status(...).json(...)" },
{ name: "res.send(inhalt, status) — entfernt",
muster: /res\.send\([^)]+,\s*\d{3}\s*\)/g,
hinweis: "ersetzen durch res.status(...).send(...)" },
/* ⚠️ Die Reihenfolge ist in 5 UMGEKEHRT: res.redirect(status, url).
Wer res.redirect(url, status) benutzt, bekommt keinen Fehler --
die Umleitung geht nur an die falsche Stelle. */
{ name: "res.redirect(url, status) — Reihenfolge umgedreht",
muster: /res\.redirect\(\s*["'`][^"'`]+["'`]\s*,\s*\d{3}\s*\)/g,
hinweis: "in Express 5: res.redirect(status, url)" },
{ name: 'res.redirect("back") — entfernt',
muster: /res\.redirect\(\s*["'`]back["'`]\s*\)/g,
hinweis: 'ersetzen durch req.get("Referrer") || "/"' },
{ name: "req.param(...) — entfernt",
muster: /req\.param\(/g,
hinweis: "req.params / req.query / req.body direkt lesen" },
{ name: "app.del(...) — entfernt",
muster: /\bapp\.del\(/g,
hinweis: "app.delete(...) benutzen" },
{ name: "res.sendfile(...) — entfernt",
muster: /res\.sendfile\(/g,
hinweis: "res.sendFile(...) (grosses F)" },
/* path-to-regexp 8: Der namenlose Stern ist weg. */
{ name: 'Route mit "*" ohne Namen — Pfadsyntax geaendert',
muster: /(app|router)\.(get|post|put|delete|all|use)\(\s*["'`][^"'`]*\*(?![a-zA-Z])/g,
hinweis: '"/*" wird zu "/*splat" bzw. "/{*splat}"' },
{ name: 'Route mit "?" (optionaler Teil) — Pfadsyntax geaendert',
muster: /(app|router)\.(get|post|put|delete|all)\(\s*["'`][^"'`]*\?[^"'`]*["'`]/g,
hinweis: '":a?" wird zu "{:a}"' },
{ name: "req.query wird beschrieben — jetzt schreibgeschuetzt",
muster: /req\.query\s*(\.[a-zA-Z_$][\w$]*)?\s*=[^=]/g,
hinweis: "req.query ist in 5 nur noch lesbar" },
{ name: "express.static mit hidden/from — Optionen entfernt",
muster: /express\.static\([^)]*\b(hidden|from)\s*:/g,
hinweis: "dotfiles bzw. root benutzen" },
{ name: "express.static.mime — entfernt",
muster: /express\.static\.mime/g,
hinweis: 'Paket "mime-types" benutzen' },
{ name: "req.acceptsCharset/Encoding/Language (Einzahl) — entfernt",
muster: /req\.accepts(Charset|Encoding|Language)\(/g,
hinweis: "Mehrzahlform benutzen" },
{ name: "res.vary() ohne Feld — wirft jetzt einen Fehler",
muster: /res\.vary\(\s*\)/g,
hinweis: "Feldnamen angeben" },
];
console.log(`Durchsucht: ${QUELLEN.length} Dateien in server/ und server-internal/\n`);
console.log("1. ENTFERNTE UND GEAENDERTE AUFRUFE");
for (const r of REGELN) {
const funde = [];
for (const datei of QUELLEN) {
const inhalt = fs.readFileSync(datei, "utf8");
r.muster.lastIndex = 0;
let t;
while ((t = r.muster.exec(inhalt)) !== null) {
const zeile = inhalt.slice(0, t.index).split("\n").length;
funde.push(`${path.relative(process.cwd(), datei)}:${zeile} ${t[0].slice(0, 50)}`);
}
}
if (funde.length) {
treffer += funde.length;
console.log(` BETROFFEN ${r.name} (${funde.length})`);
console.log(` → ${r.hinweis}`);
for (const f of funde.slice(0, 4)) console.log(` ${f}`);
} else {
sauber++;
console.log(` ok ${r.name}`);
}
}
/* ---------------------------------------------------------------------
Zwei Punkte, die keine Fundstelle haben, aber trotzdem wirken --
deshalb hier ausdrücklich aufgeführt statt übersehen.
--------------------------------------------------------------------- */
console.log("\n2. GEAENDERTE STANDARDWERTE (keine Fundstelle, wirken trotzdem)");
const alleInhalte = QUELLEN.map((d) => fs.readFileSync(d, "utf8")).join("\n");
const urlencoded = /express\.urlencoded\(([^)]*)\)/.exec(alleInhalte);
if (urlencoded) {
const hatExtended = /extended/.test(urlencoded[1]);
console.log(hatExtended
? " ok express.urlencoded: extended ist ausdruecklich gesetzt"
: ' BETROFFEN express.urlencoded ohne "extended" — Standard wechselt von true auf false\n'
+ ' → ausdruecklich { extended: true } setzen, wenn verschachtelte Felder gebraucht werden');
if (!hatExtended) treffer++;
} else {
console.log(" ok express.urlencoded wird nicht benutzt");
}
/* dotfiles: In 5 ist der Standard "ignore" -- Pfade, die mit einem
Punkt beginnen, liefern dann 404. Das betrifft .well-known, das
Let's Encrypt und manche Dienste brauchen. */
const wellknown = fs.existsSync(".well-known");
console.log(wellknown
? ' BETROFFEN Ein Ordner ".well-known" existiert — express.static liefert Punkt-Ordner\n'
+ ' in Express 5 standardmaessig NICHT mehr aus (dotfiles: "ignore")\n'
+ ' → { dotfiles: "allow" } fuer diesen Pfad setzen'
: ' ok kein ".well-known"-Ordner im Auslieferungsverzeichnis');
if (wellknown) treffer++;
/* req.body ist in 5 undefined statt {}, wenn kein Parser lief. */
const bodyOhnePruefung = [];
for (const datei of QUELLEN) {
const inhalt = fs.readFileSync(datei, "utf8");
const m = /req\.body\.[a-zA-Z_$]/g;
let t;
while ((t = m.exec(inhalt)) !== null) {
if (inhalt.slice(t.index - 1, t.index) === "?") continue;
/* ⚠️ Der Blick zurueck muss weit genug reichen.
Zuerst standen hier 60 Zeichen. Damit meldete der Pruefer drei
Stellen in webdesign-kunden.js als ungesichert -- sie stehen aber
INNERHALB eines "if (typeof req.body?.feld === 'boolean')", sind
also bereits geprueft. Der Block war nur laenger als der
Rueckblick.
Ein Pruefer, der abgesicherte Stellen anmahnt, kostet genau die
Zeit, die er sparen soll -- und beim naechsten Mal glaubt man ihm
auch die echten Funde nicht mehr. Deshalb jetzt die drei Zeilen
davor, das deckt den ueblichen Block ab. */
const bis = inhalt.slice(0, t.index);
const zeilenDavor = bis.split("\n").slice(-4).join("\n");
if (/req\.body\s*(&&|\?\.)|\|\|\s*\{\}/.test(zeilenDavor)) continue;
bodyOhnePruefung.push(`${path.relative(process.cwd(), datei)}:${bis.split("\n").length}`);
}
}
if (bodyOhnePruefung.length) {
treffer += bodyOhnePruefung.length;
console.log(` BETROFFEN req.body ohne Absicherung (${bodyOhnePruefung.length}) — in 5 ist es\n`
+ " undefined statt {}, wenn kein Parser lief\n"
+ " → req.body?.feld schreiben");
for (const f of bodyOhnePruefung.slice(0, 4)) console.log(" " + f);
} else {
console.log(" ok req.body wird ueberall abgesichert gelesen");
}
console.log(`\n===== ${sauber} unbedenklich, ${treffer} Fundstelle(n) zu pruefen =====`);
if (!treffer) {
console.log("\nDer Umstieg waere aus Sicht dieser Pruefung ohne Codeaenderung moeglich.");
console.log("Das ersetzt keinen Testlauf -- es heisst nur, dass die bekannten");
console.log("Bruchstellen aus dem Migrationsleitfaden hier nicht vorkommen.");
}
process.exit(0);
+91
View File
@@ -0,0 +1,91 @@
/* =====================================================================
Verträglichkeitsprüfung Express 5
Aufruf: node test-express5.mjs (im Verzeichnis server)
Zwei Prüfungen ergänzen sich:
pruef-express5.mjs durchsucht den Bestand nach entfernten
Methoden und geänderter Pfadsyntax
diese Datei startet einen echten Server mit genau den
Bausteinen, die server/index.js und
server-internal/index.js verwenden
Das eine findet, was nicht mehr existiert. Das andere zeigt, ob der
Aufbau als GANZES noch läuft — eine Middleware-Kette kann aus lauter
gültigen Teilen bestehen und trotzdem anders reagieren.
Am 26.08.2026 gegen express 5.2.1 geprüft: 10 von 10 bestanden.
===================================================================== */
import express from "express";
import cookieParser from "cookie-parser";
import http from "http";
let ok = 0, fehl = 0;
const pruefe = (n, g, i) => { if (g) { ok++; console.log(" ok " + n + (i ? " -> " + i : "")); }
else { fehl++; console.log(" FEHL " + n + (i ? " -> " + i : "")); } };
const app = express();
app.set("trust proxy", 1);
app.disable("x-powered-by");
pruefe("app.set / app.disable", true);
/* Sicherheits-Kopfzeilen als Middleware — wie in server/index.js */
app.use((req, res, next) => { res.setHeader("X-Test", "1"); next(); });
app.use(cookieParser());
app.use(express.json({ limit: "1mb" }));
pruefe("Middleware-Kette (setHeader, cookieParser, json)", true);
/* Eine Schranke, die umleitet — wie webdesign-gate.js */
app.use("/gesperrt", (req, res) => res.redirect(302, "/zugang"));
/* Statische Auslieferung mit index — wie express.static(SITE_DIR) */
app.use("/statisch", express.static(process.cwd(), { index: false }));
app.get("/ping", (req, res) => res.status(200).json({ ok: true }));
app.post("/echo", (req, res) => res.status(201).json({ hat: !!req.body, feld: req.body?.a }));
/* Router, wie in routes/*.js */
const r = express.Router();
r.get("/inner/:id", (req, res) => res.json({ id: req.params.id }));
app.use("/r", r);
/* 404 und Fehlerbehandlung */
app.use((req, res) => res.status(404).json({ ok: false }));
app.use((err, req, res, next) => res.status(500).json({ ok: false, fehler: String(err.message) }));
const server = app.listen(4418);
await new Promise((f) => server.on("listening", f));
const hol = async (pfad, opt) => {
const a = await fetch("http://127.0.0.1:4418" + pfad, { redirect: "manual", ...opt });
let k = null; try { k = await a.json(); } catch {}
return { status: a.status, kopf: a.headers, koerper: k };
};
let a = await hol("/ping");
pruefe("GET mit res.status().json()", a.status === 200 && a.koerper?.ok === true, "HTTP " + a.status);
pruefe("eigene Kopfzeile kommt an", a.kopf.get("x-test") === "1");
pruefe("x-powered-by ist aus", !a.kopf.get("x-powered-by"));
a = await hol("/echo", { method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ a: "wert" }) });
pruefe("POST mit express.json()", a.status === 201 && a.koerper?.feld === "wert", JSON.stringify(a.koerper));
a = await hol("/r/inner/abc");
pruefe("Router mit Parameter", a.koerper?.id === "abc", JSON.stringify(a.koerper));
a = await hol("/gesperrt/irgendwas");
pruefe("res.redirect(302, ziel) — neue Reihenfolge",
a.status === 302 && a.kopf.get("location") === "/zugang",
a.status + " -> " + a.kopf.get("location"));
a = await hol("/gibtsnicht");
pruefe("404-Behandlung greift", a.status === 404);
a = await hol("/statisch/package.json");
pruefe("express.static liefert aus", a.status === 200);
server.close();
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
process.exit(fehl ? 1 : 0);