Abschnitt 10 · Lektion 2
Eigene Middleware
Das Muster ist immer dasselbe: prüfen oder ergänzen, dann next(). Was du dabei ergänzt,
gehört an req.
Prüfen oder ergänzen, dann weitergeben
import express from "express";
const app = express();
let zaehler = 0;
// Das Protokoll liegt in einer Liste statt im Log, damit du es unter
// /protokoll nachlesen kannst.
const protokoll = [];
// Der Pfad steht beim Anmelden, nicht in der Funktion: diese Middleware gilt
// nur fuer Anfragen, die mit /api beginnen.
app.use("/api", (req, res, next) => {
zaehler += 1;
req.kennung = `a${zaehler}`;
res.setHeader("X-Anfrage-Id", req.kennung);
protokoll.push(`${req.kennung} kommt an: req.url=${req.url} req.originalUrl=${req.originalUrl}`);
const start = process.hrtime.bigint();
res.on("finish", () => {
const ms = Number(process.hrtime.bigint() - start) / 1e6;
protokoll.push(`${req.kennung} ist fertig: ${res.statusCode}, unter 100 ms: ${ms < 100}`);
});
next();
});
app.get("/api/status", (req, res) => res.json({ id: req.kennung, ok: true }));
app.get("/start", (req, res) => res.json({ id: req.kennung ?? "ohne Kennung" }));
app.get("/protokoll", (req, res) => res.json(protokoll));
app.listen(3000); Drei Dinge auf einmal, und alle drei siehst du in echten Projekten ständig.
req.kennung = ... hängt einen Wert an die Anfrage. Ab dieser Zeile kann jede Middleware und
jede Route darunter ihn lesen. Nur /api/status bekommt eine, weil die Middleware unter /api
angemeldet ist; /start läuft ohne. Deshalb steht dort req.kennung ?? "ohne Kennung", und deshalb
fehlt bei /start die Kopfzeile. Im Browser siehst du Kopfzeilen nicht, im Terminal schon:
curl -i http://localhost:3000/api/status
curl -i http://localhost:3000/start
res.setHeader funktioniert hier, obwohl die Antwort noch gar nicht geschrieben ist. Express
sammelt Kopfzeilen und schickt sie erst mit, wenn die Antwort rausgeht. Solange noch nichts gesendet
wurde, darfst du also welche setzen.
res.on("finish", ...) läuft, wenn die Antwort draußen ist. Das ist der richtige Ort für alles,
was das Ergebnis kennen muss: Statuscode, Dauer, Größe. Für eine Kopfzeile ist es zu spät, die sind
dann längst unterwegs. Merk dir die Trennung: Was in die Antwort soll, setzt du vorher. Was
über die Antwort geschrieben wird, protokollierst du nachher.
Das Beispiel schreibt unter 100 ms: true statt einer Millisekundenzahl. In deinem eigenen Protokoll
gehört dort die echte Zahl hin; hier wäre sie bei jedem Lauf eine andere und würde nur davon
ablenken, worum es geht.
Warum der Pfad beim Anmelden abgeschnitten wird
Ruf /api/status auf und danach /protokoll. In der ersten Zeile steht: req.url sagt /status,
req.originalUrl sagt /api/status.
Das ist kein Fehler, sondern Absicht: Innerhalb einer unter /api angemeldeten Middleware ist
/api bereits abgearbeitet. Dazu gibt es req.baseUrl mit dem abgeschnittenen Teil. Wenn du in
einem Protokoll die vollständige Adresse sehen willst, nimm also req.originalUrl, sonst wunderst
du dich irgendwann über Logzeilen, in denen der halbe Pfad fehlt.
Warum nicht einfach eine Variable daneben?
Das ist die Frage, die sich beim ersten Mal jeder stellt. Eine Variable außerhalb wäre kürzer, und sie funktioniert auch. Beim Ausprobieren.
import express from "express";
const app = express();
// Falsch: eine Variable ausserhalb, fuer alle Anfragen dieselbe.
let aktuellerName = "";
app.use("/aussen", (req, res, next) => {
aktuellerName = req.query.name;
next();
});
app.get("/aussen", async (req, res) => {
await new Promise((r) => setTimeout(r, Number(req.query.warten)));
res.json({ gruss: `Hallo ${aktuellerName}` });
});
// Richtig: an die Anfrage gehaengt, also fuer jede Anfrage ein eigener Wert.
app.use("/innen", (req, res, next) => {
req.name = req.query.name;
next();
});
app.get("/innen", async (req, res) => {
await new Promise((r) => setTimeout(r, Number(req.query.warten)));
res.json({ gruss: `Hallo ${req.name}` });
});
app.listen(3000); Rechts steht der harmlose Fall: eine Anfrage allein, und die Antwort stimmt. Zwei gleichzeitig
bekommst du in einem Browser-Tab nicht hin, im Terminal schon. Das & schickt den ersten Aufruf in
den Hintergrund, sleep lässt ihn anlaufen, und der zweite kommt dazwischen:
curl -s "http://localhost:3000/aussen?name=Ada&warten=300" & sleep 0.1; curl -s "http://localhost:3000/aussen?name=Grace&warten=10"; wait
Ada fragt zuerst und ihre Antwort dauert länger, Grace kommt hundert Millisekunden später dazwischen.
Die Middleware überschreibt die Variable, und als Adas Route endlich antwortet, steht dort Graces
Name. Beide Zeilen sagen Hallo Grace.
Derselbe Befehl mit /innen statt /aussen, und jede bekommt ihren eigenen Namen:
curl -s "http://localhost:3000/innen?name=Ada&warten=300" & sleep 0.1; curl -s "http://localhost:3000/innen?name=Grace&warten=10"; wait
Ada bekommt eine Antwort, die für jemand anderen gedacht war. Bei einem Namen ist das peinlich. Bei einer Nutzerkennung oder einem Warenkorb ist es ein Sicherheitsvorfall.
Und jetzt der eigentliche Punkt: Beim Entwickeln merkst du davon nichts. Du klickst dich allein durch deine Anwendung, es ist immer nur eine Anfrage unterwegs, und alles stimmt. Der Fehler erscheint erst, wenn mehrere Leute gleichzeitig da sind, also genau dann, wenn du am wenigsten Lust darauf hast.
Die Regel dazu ist kurz: Alles, was zu einer einzelnen Anfrage gehört, gehört an req. Eine
Variable außerhalb ist für Dinge da, die bewusst geteilt werden, ein Zähler zum Beispiel, wie im
ersten Beispiel.
Middleware mit einer Einstellung
Dir ist in Abschnitt 9 aufgefallen, dass manche Middleware mit Klammern angemeldet wird
(express.static("public")) und manche ohne. Der Unterschied ist einfacher, als er aussieht.
import express from "express";
// Eine Funktion, die eine Middleware zurueckgibt. Genau so sieht fast jedes
// Express-Paket aus, das man beim Anmelden mit Klammern aufruft.
function nurMitSchluessel(erwartet) {
return (req, res, next) => {
if (req.headers["x-schluessel"] !== erwartet) {
return res.status(401).json({ fehler: "Schluessel fehlt oder stimmt nicht" });
}
next();
};
}
const app = express();
app.use("/intern", nurMitSchluessel("geheim"));
app.get("/", (req, res) => res.send("offen fuer alle"));
app.get("/intern/zahlen", (req, res) => res.json({ zahlen: [1, 2, 3] }));
app.listen(3000); nurMitSchluessel ist keine Middleware, sondern eine Funktion, die eine zurückgibt. Beim
Anmelden rufst du sie auf, sie merkt sich den erwarteten Schlüssel und liefert die eigentliche
Funktion mit drei Argumenten.
Damit kannst du dieselbe Middleware mehrfach mit verschiedenen Einstellungen anmelden. Und du weißt jetzt, warum praktisch jedes Express-Paket so aussieht.
Rechts steht /, die offene Seite. /intern/zahlen in der Adresszeile gibt einen 401, denn der
Browser kennt den Schlüssel nicht. Im Terminal kannst du ihn mitschicken:
curl -i http://localhost:3000/intern/zahlen
curl -i -H "X-Schluessel: geheim" http://localhost:3000/intern/zahlen
Beachte auch die Zeile mit return res.status(401).... Die Middleware antwortet und ruft next()
nicht auf. Das ist völlig in Ordnung: Eine Middleware darf die Kette abbrechen, sie muss nur
eines von beiden tun. Was nicht geht, ist beides oder keines.
Was in eine Middleware gehört und was nicht
Hinein gehört, was querschnittlich ist, also für viele Routen gleich: Protokollierung, Zeitmessung, Anmeldung, Berechtigungen, das Lesen des Bodys.
Nicht hinein gehört die eigentliche Arbeit einer Route. Wenn eine Middleware anfängt, Bücher zu suchen oder Preise zu rechnen, ist das eine Route, die man an der falschen Stelle geschrieben hat. Die Frage, die dabei hilft: Würde ich das bei jeder zweiten Route wieder brauchen? Wenn ja, ist es Middleware.
Zum Mitnehmen
Ergebnisse gehören an req, nicht in eine Variable daneben. Die Variable wäre für alle gleichzeitigen Anfragen dieselbe, und das fällt erst unter Last auf.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.