mitmario.dev

Weiterleiten nach POST

Node.js Sandbox 4 Min Lesezeit 4 BeispieleLektion 2 von 6

Deine Route aus der letzten Lektion nimmt das Formular entgegen und antwortet mit dem Ergebnis. Das funktioniert, solange niemand F5 drückt.

Was beim Neuladen passiert

Ein Browser merkt sich, wie die Seite entstanden ist, die gerade im Fenster steht. Kam sie aus einem POST, dann wiederholt das Neuladen genau diesen POST, mitsamt Körper.

Der doppelte Eintrag
import express from "express";

const app = express();
app.use(express.urlencoded({ extended: true }));

const notizen = [];

app.get("/", (req, res) =>
  res.type("html").send(
    '<form method="post" action="/notizen">' +
      '<p><input name="text" value="Milch"></p>' +
      "<p><button>Speichern</button></p>" +
      "</form>"
  )
);

// So nicht: der POST antwortet direkt mit dem Ergebnis.
app.post("/notizen", (req, res) => {
  notizen.push(req.body.text);
  res.type("text/plain").send(`Notizen: ${notizen.length}`);
});

app.listen(3000);

Schick das Formular rechts ab: Notizen: 1. In deinem eigenen Browser würde F5 jetzt denselben POST noch einmal schicken, nach einer Rückfrage, und aus der 1 würde eine 2. Der Knopf Seite neu laden hier schickt dagegen ein GET, und darauf antwortet dieser Server unter /notizen nur mit einer 404. Das Wiederholen zeigt dir deshalb das Terminal: Schick den POST aus der Befehlsleiste zweimal ab, und der Bestand wächst jedes Mal.

Zweimal dieselbe Anfrage, zwei Einträge. Im Browser hängt zwar meist noch eine Rückfrage davor („Formular erneut senden?”), aber erstens klickt die jeder weg, und zweitens gibt es genug Wege, ganz ohne Rückfrage dort hinzukommen: der Zurück-Knopf, ein wiederhergestellter Tab, ein Klick auf die Adresszeile plus Eingabetaste.

Was dabei jedes Mal hinausgeht, kannst du dir ansehen. Im Reiter Netzwerk hat der POST /notizen vom Formular eine eigene Zeile, und aufgeklappt liest du im Anfrage-Rumpf die Notiz, die du getippt hast. Genau dieser Rumpf ginge beim Neuladen unverändert noch einmal los. Das ist das „mitsamt Körper” von weiter oben.

Bei einer Notiz ist das ärgerlich. Bei einer Bestellung oder einer Überweisung ist es ein Supportfall, und zwar einer, den niemand nachstellen kann, weil er von der Bediengewohnheit abhängt.

Post, Redirect, Get

Das Muster dagegen ist so alt wie das Problem und heißt nach seinen drei Schritten: Post, Redirect, Get. Die POST-Route ändert etwas und antwortet dann nicht mit Inhalt, sondern mit einer Weiterleitung auf eine Seite, die per GET erreichbar ist.

Anlegen und dann weiterschicken
import express from "express";

const app = express();
app.use(express.urlencoded({ extended: true }));

const notizen = [];

// So: anlegen, dann mit 303 auf eine Seite schicken, die per GET
// erreichbar ist.
app.post("/notizen", (req, res) => {
  notizen.push(req.body.text);
  res.redirect(303, "/notizen");
});

app.get("/notizen", (req, res) => {
  res.type("text/plain").send(`Notizen: ${notizen.length}`);
});

app.listen(3000);

Rechts steht die Liste unter /notizen. Den POST schickst du im Terminal, mit -i siehst du die Weiterleitung selbst, mit -L folgst du ihr:

curl -i -X POST -d "text=Milch" http://localhost:3000/notizen

Nach dem Abschicken steht in der Adresszeile /notizen, und dort steht ein GET dahinter. Neuladen holt dieselbe Seite noch einmal, und ein GET darf man beliebig oft wiederholen, ohne dass sich etwas ändert. Genau dafür gibt es die Unterscheidung zwischen den Methoden aus Lektion 11.1.

Beachte die Antwort auf den POST: Sie hat keinen nennenswerten Inhalt. Location sagt, wo es weitergeht, und mehr braucht sie nicht. Express legt zwar einen Satz hinein, See Other. Redirecting to /notizen, aber den liest nie jemand, denn der Browser ist da längst unterwegs. Er folgt der Weiterleitung von selbst, deshalb bekommst du die 303 dort nie zu sehen; curl bleibt ohne -L genau davor stehen.

Im ersten Beispiel hattest du das Formular, im zweiten die Weiterleitung als curl-Zeile. Das nächste bringt beides zusammen: ein Formular, dessen POST mit 303 antwortet, und ein Browser, der der Weiterleitung folgt, ohne zu fragen. Dort kannst du das Muster anfassen.

Dasselbe im Browser, mit einem echten Formular
import express from "express";

const app = express();
app.use(express.urlencoded({ extended: true }));

const notizen = [];

app.get("/", (req, res) => {
  res.send([
    "<!doctype html>",
    '<meta charset="utf-8">',
    "<title>Notizen</title>",
    "<h1>Neue Notiz</h1>",
    '<form method="post" action="/notizen">',
    '  <p><input name="text" value="Milch"></p>',
    "  <p><button>Speichern</button></p>",
    "</form>",
    `<p>Im Speicher liegen ${notizen.length} Notizen.</p>`,
  ].join("\n"));
});

app.post("/notizen", (req, res) => {
  notizen.push(req.body.text);
  res.redirect(303, "/notizen");
});

app.get("/notizen", (req, res) => {
  res.send([
    "<!doctype html>",
    '<meta charset="utf-8">',
    "<title>Notizen</title>",
    `<h1>${notizen.length} Notizen</h1>`,
    "<ul>",
    ...notizen.map((text) => `  <li>${text}</li>`),
    "</ul>",
    '<p><a href="/">Noch eine schreiben</a></p>',
  ].join("\n"));
});

app.listen(process.env.PORT ?? 3000);

Schick das Formular ab. Die Adresse wechselt auf /notizen, und dann lad die Seite noch einmal, ruhig mehrmals. Die Liste wird nicht länger, denn geladen wird jedes Mal ein GET.

Neu laden geht hier anders. Dafür gibt es den Knopf Seite neu laden; F5 lädt die ganze Lektionsseite und nicht die Seite daneben. Die Rückfrage „Formular erneut senden?” bekommst du dabei nie zu sehen, und zwar aus einem schlichten Grund: Der Knopf holt die Adresse aus dem Feld, schickt also ein GET, und ein zweites Abschicken kommt gar nicht erst in Frage. Wie die Rückfrage aussieht, weißt du aus deinem eigenen Browser.

Warum ausgerechnet 303

Es gibt drei Weiterleitungen, die hier in Frage kommen, und ihr Unterschied liegt nicht im Ziel, sondern darin, was der Client danach tun soll.

Welche Methode beim Ziel ankommt
import express from "express";

const app = express();

app.get("/", (req, res) =>
  res
    .type("text/plain")
    .send(
      [301, 302, 303, 307]
        .map((code) => `curl -i -L -X POST http://localhost:3000/start-${code}`)
        .join("\n") + "\n"
    )
);

// Vier Weiterleitungen, sonst gleich. Das Ziel meldet, welche Methode
// tatsaechlich bei ihm angekommen ist.
for (const code of [301, 302, 303, 307]) {
  app.post(`/start-${code}`, (req, res) => res.redirect(code, `/ziel-${code}`));
  app.all(`/ziel-${code}`, (req, res) =>
    res.type("text/plain").send(`${code} -> beim Ziel kam an: ${req.method}`)
  );
}

app.listen(3000);

Die vier Befehle stehen im Reiter „Browser”. Schick sie nacheinander ab, das -L folgt der Weiterleitung, und das Ziel sagt jedes Mal, welche Methode bei ihm angekommen ist.

301 heißt „diese Adresse gilt für immer nicht mehr” und wird von Browsern gespeichert. Nimm sie nie für eine Aktion, sonst schickt der Browser die nächste Anfrage gar nicht erst an dich. 302 heißt „im Moment woanders”. 303 heißt „schau dir das Ergebnis unter dieser Adresse an, und zwar per GET”. 307 ist das Gegenstück und behält die Methode bei, wie der letzte Fall im Beispiel zeigt.

In der Praxis machen 301, 302 und 303 alle dasselbe. Der Unterschied ist trotzdem keine Haarspalterei: Bei 301 und 302 tun die Clients das aus Gewohnheit, weil es sich in den neunziger Jahren so eingebürgert hat, und die Norm erlaubt ihnen auch etwas anderes. Bei 303 steht es geschrieben. Nimm den Code, der meint, was du sagen willst.

Die Rückmeldung, die keine Adresse hat

Eine Frage bleibt offen: Wo steht „Notiz gespeichert”? In der Antwort auf den POST kann sie nicht stehen, die sieht niemand mehr.

Der naheliegende Weg ist ein Anhängsel an die Adresse, also /notizen?gespeichert=1. Das funktioniert, hat aber zwei Nachteile: Die Meldung bleibt beim Neuladen stehen, obwohl gerade nichts gespeichert wurde, und sie steht in jedem Lesezeichen und jedem geteilten Link.

Der saubere Weg legt die Meldung kurz beim Server ab, zeigt sie einmal und wirft sie dann weg. Dafür braucht es einen Platz, der zu diesem einen Besucher gehört, und genau den baust du in Lektion 13.4.

Zum Mitnehmen

Ein POST, der direkt mit der fertigen Seite antwortet, macht aus jedem Neuladen eine zweite Bestellung. Das Gegenmittel hat einen Namen und drei Zeilen Code.

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 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe, dein Code läuft auf einem Server

    Öffnet sich mit dem Basis Konto.