mitmario.dev

Formulardaten annehmen

Node.js Sandbox 4 Min Lesezeit 3 BeispieleLektion 1 von 6

In Abschnitt 11 und 12 war dein Gegenüber immer ein Programm. Es schickte JSON, weil du es so verlangt hast. Ab hier sitzt ein Mensch davor, klickt auf einen Knopf in einer Webseite, und was dann bei dir ankommt, hat sich niemand ausgesucht: Das Format legt der Browser fest, seit den neunziger Jahren unverändert.

Was beim Abschicken über die Leitung geht

Ein <form method="post" action="/anmelden"> sammelt beim Klick auf den Knopf alle Felder ein, die ein name-Attribut haben, und schickt sie als eine einzige Zeile los.

Was der Browser wirklich abschickt
import express from "express";

const app = express();


// Ein Formular, damit du wirklich abschicken kannst. Genau das tut ein
// Browser, wenn jemand auf den Knopf drueckt.
app.get("/", (req, res) =>
  res.type("html").send(
    '<form method="post" action="/anmelden">' +
      '<p><input name="name" value="Mario"></p>' +
      '<p><label><input type="checkbox" name="newsletter" checked> Newsletter</label></p>' +
      '<p><label><input type="checkbox" name="thema" value="js" checked> JavaScript</label></p>' +
      '<p><label><input type="checkbox" name="thema" value="node" checked> Node</label></p>' +
      "<p><button>Abschicken</button></p>" +
      "</form>"
  )
);

// Kein Parser angemeldet: hier kommt der Koerper roh an, so wie ihn der
// Browser beim Abschicken auf die Leitung legt.
app.post("/anmelden", (req, res) => {
  let roh = "";
  req.on("data", (stueck) => {
    roh += stueck;
  });
  req.on("end", () => {
    res.type("text/plain").send(
      `Content-Type: ${req.headers["content-type"]}\nKoerper:      ${roh}\n`
    );
  });
});

app.listen(3000);

Rechts steht ein echtes Formular. Drück auf „Abschicken”, und in der Antwort steht Zeichen für Zeichen, was dein Browser gerade auf die Leitung gelegt hat.

Das ist die ganze Kodierung: Schlüssel, Gleichheitszeichen, Wert, dazwischen ein &. Sonderzeichen werden verpackt wie in einer Adresse, ein Leerzeichen wird also zu + oder %20. Der Name dafür steht in der Kopfzeile darüber: application/x-www-form-urlencoded.

Die Antwort zeigt dir den Körper nur, weil dieses Beispiel ihn ausdrücklich zurückschreibt. Ein richtiger Server tut das nicht, ansehen kannst du ihn trotzdem: Im Reiter Netzwerk steht der POST /anmelden, und aufgeklappt liest du dieselbe Zeile im Anfrage-Rumpf. Das ist dort die Beschriftung für das, was dieser Kurs den Körper nennt.

Zwei Dinge lohnen sich zu merken. Erstens ist name an einem Eingabefeld nicht die Beschriftung, sondern der Schlüssel im Körper. Ein Feld ohne name wird gar nicht erst mitgeschickt, egal was drinsteht, und das ist einer der stilleren Fehler beim Bauen eines Formulars. Zweitens taucht thema zweimal auf. Der Browser wirft nichts weg, wenn zwei Felder denselben Namen tragen.

Der Parser dafür heißt urlencoded

express.json() aus Lektion 10.3 hilft hier nicht weiter. Für Formulare gibt es express.urlencoded({ extended: true }), angemeldet als Middleware, ganz oben, genau wie sein Geschwister.

Derselbe Körper, von urlencoded gelesen
import express from "express";

const app = express();

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

// Ein Formular, damit du wirklich abschicken kannst. Genau das tut ein
// Browser, wenn jemand auf den Knopf drueckt.
app.get("/", (req, res) =>
  res.type("html").send(
    '<form method="post" action="/anmelden">' +
      '<p><input name="name" value="Mario"></p>' +
      '<p><label><input type="checkbox" name="newsletter" checked> Newsletter</label></p>' +
      '<p><label><input type="checkbox" name="thema" value="js" checked> JavaScript</label></p>' +
      '<p><label><input type="checkbox" name="thema" value="node" checked> Node</label></p>' +
      "<p><button>Abschicken</button></p>" +
      "</form>"
  )
);

app.post("/anmelden", (req, res) => {
  res.json({
    body: req.body,
    typVonThema: Array.isArray(req.body.thema) ? "Array" : typeof req.body.thema,
  });
});

app.listen(3000);

Dasselbe Formular, diesmal mit dem Parser davor. Nimm die Häkchen einzeln heraus und schick jedes Mal neu ab, dann siehst du die drei Fälle nacheinander.

Der erste Fall ist der Normalfall: ein Objekt mit den Feldnamen als Schlüssel. Interessant sind die beiden anderen.

Ein Feld ist einmal Text und einmal Liste

Schau dir thema in den drei Fällen an. Bei zwei angehakten Kästchen bekommst du ein Array, bei einem einzigen einen ganz normalen String. Der Parser rät nicht, ob du eine Liste erwartest, er berichtet nur, was angekommen ist.

Das ist die häufigste Überraschung in diesem Bereich, und sie ist heimtückisch, weil dein Code trotzdem läuft. Wer die Anzahl mit req.body.thema.length bestimmt, bekommt bei zwei Themen eine 2 und freut sich. Bei einem einzigen Thema js bekommt er ebenfalls eine 2, denn das ist die Länge der Zeichenkette. Der Fehler zeigt sich also ausgerechnet im einfachsten Fall.

Der Griff dagegen ist eine Zeile: [].concat(req.body.thema ?? []) macht aus keinem Wert eine leere Liste, aus einem Wert eine Liste mit einem Eintrag und lässt eine Liste in Ruhe. Danach zählst du gefahrlos.

Das Kästchen, das nichts schickt

Im dritten Fall fehlt newsletter komplett. Nicht false, nicht ein leerer String, sondern der Schlüssel ist nicht da.

Ein Kontrollkästchen wird nur mitgeschickt, wenn es angehakt ist, und dann mit dem Wert aus seinem value-Attribut, ohne Angabe schlicht mit on. Frag also nie if (req.body.newsletter === false), sondern prüf auf Anwesenheit. Dieselbe Regel gilt für Auswahlfelder, bei denen nichts ausgewählt wurde.

Der falsche Parser tut gar nichts

Und weil es so leicht passiert, hier noch der Fall, in dem alles richtig aussieht und trotzdem nichts ankommt.

Mit express.json statt urlencoded
import express from "express";

const app = express();

// Der falsche Parser: express.json() ist fuer application/json zustaendig
// und laesst einen Formularkoerper unberuehrt liegen.
app.use(express.json());

// Ein Formular, damit du wirklich abschicken kannst. Genau das tut ein
// Browser, wenn jemand auf den Knopf drueckt.
app.get("/", (req, res) =>
  res.type("html").send(
    '<form method="post" action="/anmelden">' +
      '<p><input name="name" value="Mario"></p>' +
      '<p><label><input type="checkbox" name="newsletter" checked> Newsletter</label></p>' +
      '<p><label><input type="checkbox" name="thema" value="js" checked> JavaScript</label></p>' +
      '<p><label><input type="checkbox" name="thema" value="node" checked> Node</label></p>' +
      "<p><button>Abschicken</button></p>" +
      "</form>"
  )
);

app.post("/anmelden", (req, res) => {
  res.json({ typ: typeof req.body, name: req.body?.name ?? null });
});

app.listen(3000);

Wieder dasselbe Formular, wieder derselbe Knopf, und diesmal kommt nichts an.

req.body ist undefined, obwohl der Körper vollständig angekommen ist. Der Grund steht in den beiden Zeilen darunter und ist derselbe wie in 10.3: Jeder Parser sieht sich zuerst den Content-Type an und lässt liegen, wofür er nicht zuständig ist. Melde beide an, wenn dein Server beides bedienen soll, sie stören sich nicht.

Eine dritte Art gibt es noch. Sobald ein Formular ein Feld für Dateien enthält, wechselt der Browser auf multipart/form-data, weil eine Datei sich nicht sinnvoll in eine Zeile aus Gleichheitszeichen pressen lässt. Express bringt dafür nichts mit, das übernimmt ein Paket. Datei-Uploads bleiben in diesem Kurs draußen: Sie sind ein Thema für sich, vor allem beim Prüfen dessen, was da hochkommt.

Zum Mitnehmen

Ein Formular schickt kein JSON. Und ein nicht angehaktes Kästchen schickt nicht false, sondern gar nichts.

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.