mitmario.dev

Was ist Middleware?

Node.js Sandbox 5 Min Lesezeit 3 BeispieleLektion 1 von 7

In Abschnitt 9 hast du app.use schon zweimal benutzt, ohne dass ich erklärt habe, was es eigentlich tut: einmal für den Standardfall ganz unten, einmal für express.static. Beide Male war es dasselbe Werkzeug, und dieses Werkzeug hat einen Namen.

Eine Middleware ist eine Funktion, die zwischen der Anfrage und der Antwort sitzt. Express sammelt alle, die du anmeldest, in einer Liste und arbeitet sie von oben nach unten ab.

Ein Formular, das über mehrere Schreibtische wandert

Stell dir einen Antrag vor, der durch ein Büro geht. Der erste Schreibtisch trägt ihn ins Eingangsbuch ein und schiebt ihn weiter. Der zweite prüft, ob alles ausgefüllt ist, macht einen Haken dran und schiebt ihn weiter. Der dritte bearbeitet ihn und schickt eine Antwort zurück.

Drei Stellen, jede darf etwas ergänzen, jede darf zurückweisen, und wer nichts zu tun hat, reicht einfach weiter.

Drei Schreibtische in Reihe
import express from "express";

const app = express();

// Erster Schreibtisch: sieht jede Anfrage und reicht weiter.
app.use((req, res, next) => {
  console.error(`${req.method} ${req.url}`);
  next();
});

// Zweiter Schreibtisch: ergaenzt etwas und reicht weiter.
app.use((req, res, next) => {
  req.geprueft = true;
  next();
});

// Dritter Schreibtisch: antwortet und reicht nichts mehr weiter.
app.get("/daten", (req, res) => {
  res.json({ geprueft: req.geprueft === true, werte: [1, 2, 3] });
});

app.listen(3000);

Schau dir die Signatur an: (req, res, next). Die ersten beiden kennst du aus Abschnitt 8 und 9. Das dritte ist neu, und es ist der ganze Unterschied.

next() heißt: ich bin fertig, mach du weiter. Es antwortet nicht, es beendet nichts, es gibt nur den Staffelstab an den nächsten Eintrag in der Liste.

Beachte auch, dass der zweite Schreibtisch sein Ergebnis an req hängt. Das ist kein Zufall, sondern das übliche Muster: req ist das Objekt, das die ganze Kette entlangwandert, und alles, was später jemand wissen soll, gehört daran. Lektion 10.2 geht darauf genauer ein.

Was passiert, wenn niemand weitergibt

Und jetzt der Fall, für den du diese Lektion wirklich brauchst.

Der Schreibtisch, der nichts weitergibt
import express from "express";

const app = express();

app.use((req, res, next) => {
  console.log("Schreibtisch 1: weitergereicht");
  next();
});

// Weder eine Antwort noch next(). Hier endet die Anfrage, ohne zu enden.
app.use((req, res, next) => {
  console.log("Schreibtisch 2: liegen geblieben");
});

app.get("/daten", (req, res) => {
  console.log("Schreibtisch 3: nie erreicht");
  res.json({ werte: [1, 2, 3] });
});

app.listen(3000);

Der zweite Schreibtisch ruft weder next() auf noch antwortet er. Damit passiert schlicht nichts. Kein Fehler, keine Warnung, kein Absturz. Die Anfrage liegt da und wartet, bis der Aufrufer aufgibt. Im Reiter „Browser” siehst du genau das: Die Seite lädt und hört nicht auf damit.

Das ist derselbe Fehler wie das vergessene res.end aus Lektion 8.1, nur mit einem anderen Gesicht. Und beide äußern sich gleich: Der Browser dreht sein Rädchen, curl läuft in seinen Timeout, und nichts von beidem sagt dir, was los ist. Schlimmer noch: Dein Protokoll sagt es auch nicht. Der erste Schreibtisch hat die Anfrage ja gesehen und ordentlich notiert, und danach hört es einfach auf.

In der Challenge unten kannst du dir das ansehen, denn dort läuft dein Server wirklich. Nimm das next() aus dem pruefer heraus und tipp im Terminal:

curl -s -S -m 2 http://localhost:3000/daten

Das -m 2 heißt „gib nach zwei Sekunden auf”, sonst wartest du, bis dir langweilig wird. Und genau dahin läuft der Aufruf: curl: (28) Operation timed out after 2000 milliseconds with 0 bytes received. Im Terminal steht derweil GET /daten, also die Zeile des Protokollanten.

Genau diese Kombination ist das Erkennungszeichen: Die Anfrage ist angekommen, sie ist protokolliert, und trotzdem kommt nichts zurück. Setz das next() wieder ein, und derselbe Befehl antwortet sofort mit {"geprueft":true,"werte":[1,2,3]}.

Merk dir die Diagnose, sie erspart dir viel Sucherei: Wenn eine Anfrage hängt, fehlt irgendwo ein next() oder eine Antwort. Du findest die Stelle, indem du von oben Zeile für Zeile eine Ausgabe einbaust und schaust, wo die letzte noch kommt.

Ohne eine einzige eingebaute Ausgabe geht es auch, nämlich im Reiter „Debug”. Drück dort auf Aufzeichnen, und die Schritte stehen je Anfrage untereinander. In der Challenge unten sind es bei richtiger Reihenfolge fünf, und sie laufen durch alle drei Funktionen: erst der Protokollant, dann der Prüfer, dann der Antworter. Nimmst du das next() aus dem Prüfer heraus, sind es drei, der letzte steht im Prüfer, und danach kommt nichts mehr. Und im Ausgangsstand, wo der Antworter oben steht, ist es genau ein Schritt: Er antwortet, und die beiden anderen kommen nie dran. Dass die Liste aufhört, ist hier die ganze Auskunft.

Eine Route ist auch nur eine Middleware

app.get("/daten", ...) sieht aus wie etwas völlig anderes als app.use(...). Ist es aber nicht. Beides trägt einen Eintrag in dieselbe Liste ein. Der einzige Unterschied: Der app.get-Eintrag kommt nur dran, wenn Methode und Pfad passen, der app.use-Eintrag bei jeder Anfrage.

Deshalb liegen die beiden viel näher beieinander, als ihre Namen vermuten lassen. Und deshalb gilt für beide dieselbe Regel.

Die Reihenfolge ist die ganze Semantik

Oben steht, was zuerst passiert. Dieser eine Satz erklärt fast alles, was dir mit Middleware schiefgehen kann.

Derselbe Protokollant, oben und unten
import express from "express";

const app = express();

// Das Protokoll liegt hier in einer Liste statt im Log, damit du es
// unter /protokoll nachlesen kannst.
const protokoll = [];

// Derselbe Protokollant, zweimal notiert. Einmal ganz oben.
app.use((req, res, next) => {
  protokoll.push(`oben sieht ${req.url}`);
  next();
});

app.get("/daten", (req, res) => res.json({ werte: [1, 2, 3] }));

app.get("/protokoll", (req, res) => res.json(protokoll));

// Und einmal ganz unten, unter der Route.
app.use((req, res, next) => {
  protokoll.push(`unten sieht ${req.url}`);
  next();
});

app.listen(3000);

Ruf nacheinander /daten und /nix auf und danach /protokoll. Dort steht, was die beiden gesehen haben, und es ist nicht dasselbe.

Zweimal derselbe Code, zweimal derselbe Aufruf, und trotzdem sehen die beiden völlig Verschiedenes. Der obere protokolliert beide Anfragen. Der untere sieht nur /nix, denn bei /daten hat die Route darüber schon geantwortet und die Kette endet dort.

Ein Protokollant ganz oben sieht alles. Ganz unten sieht er nur noch das, was durchgefallen ist. Beides kann richtig sein, aber du musst wissen, was du willst.

Nebenbei siehst du auf /nix, dass die Anfrage trotz des unteren next() mit 404 endet: Nach dem letzten Eintrag in der Liste kommt Express’ eingebauter Standardfall, den du in Lektion 9.2 ersetzt hast.

Warum das Express zu einem Baukasten macht

Ohne dieses Konzept wäre Express eine Bibliothek, die Pfade auf Funktionen abbildet, und mehr nicht. Mit ihm ist es ein Bausatz: Protokollierung, Body lesen, statische Dateien, Anmeldung, Zeitmessung, Fehlerbehandlung. Alles davon ist eine Funktion mit drei Argumenten, alles davon meldest du mit app.use an, und die Reihenfolge entscheidest du.

Genau das machen die nächsten fünf Lektionen. Und beim ersten Mal, wenn du in einer fremden Express-Anwendung einen Stapel app.use-Zeilen siehst, weißt du jetzt, dass du ihn von oben nach unten lesen musst.

Zum Mitnehmen

Eine Middleware bekommt drei Argumente, und das dritte ist der Unterschied: next gibt weiter. Wer weder next aufruft noch antwortet, lässt die Anfrage hängen.

Jetzt du

Basis Konto, kostenlos

Zu 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.