mitmario.dev

Eingaben prüfen

Node.js Sandbox 3 Min Lesezeit 3 BeispieleLektion 5 von 8

Bis hierhin hat deine Anlegeroute alles genommen, was ankam. Das geht so lange gut, wie du selbst der einzige Aufrufer bist. Sobald jemand anderes deine Schnittstelle benutzt, gilt der Satz, der den ganzen Rest dieses Kurses trägt: Alles, was von außen kommt, ist erst einmal falsch.

Nicht weil die Leute böse sind. Meistens ist es ein Formular mit einem Tippfehler, eine App mit einem alten Feldnamen oder ein Skript, das eine Zahl als Text schickt.

Was ohne Prüfung im Bestand landet

Was ohne Prüfung im Bestand landet
import express from "express";

const app = express();
app.use(express.json());

app.get("/", (req, res) =>
  res.type("text/plain").send(
    `curl -i -X POST -H "Content-Type: application/json" -d '{"titel":"","jahr":"gestern"}' http://localhost:3000/buecher\n`
  )
);

const buecher = [];

// Keine Pruefung. Was ankommt, wird gespeichert.
app.post("/buecher", (req, res) => {
  const buch = { id: buecher.length + 1, titel: req.body.titel, jahr: req.body.jahr };
  buecher.push(buch);
  res.status(201).json(buch);
});

app.listen(3000);

Der Befehl rechts schickt absichtlich Unsinn: einen leeren Titel und ein Jahr, das ein Wort ist. Der Server nimmt beides an und gibt es zurück, als wäre nichts.

Drei Anfragen, drei Datensätze, und keiner davon ist brauchbar. Der erste hat gar keinen Titel, der zweite ein Jahr aus Buchstaben, der dritte einen Titel, der eine Zahl ist.

Und alle drei haben mit 201 geantwortet. Der Server hat gemeldet, dass alles in Ordnung ist. Der Schaden fällt erst später auf, an einer ganz anderen Stelle, wenn jemand nach Jahr sortiert oder den Titel in eine Seite schreibt.

Vier Fragen an jede Eingabe

Vier Fragen an jede Eingabe
const MAX_TITEL = 200;

function pruefe(koerper) {
  const fehler = [];

  // Ist es da, und hat es den richtigen Typ?
  if (typeof koerper.titel !== "string") {
    fehler.push({ feld: "titel", meldung: "fehlt oder ist kein Text" });
  } else if (koerper.titel.trim() === "") {
    fehler.push({ feld: "titel", meldung: "darf nicht leer sein" });
  } else if (koerper.titel.length > MAX_TITEL) {
    // Ist es nicht zu lang?
    fehler.push({ feld: "titel", meldung: `hoechstens ${MAX_TITEL} Zeichen` });
  }

  if (!Number.isInteger(koerper.jahr)) {
    fehler.push({ feld: "jahr", meldung: "fehlt oder ist keine ganze Zahl" });
  } else if (koerper.jahr < 1450 || koerper.jahr > 2100) {
    // Ist es plausibel?
    fehler.push({ feld: "jahr", meldung: "liegt ausserhalb von 1450 bis 2100" });
  }

  return fehler;
}

const proben = [
  { titel: "Node in der Praxis", jahr: 2024 },
  {},
  { titel: "Ok", jahr: "zweitausend" },
  { titel: "   ", jahr: 900 },
  { titel: "x".repeat(5000), jahr: 2024 },
];

for (const probe of proben) {
  const gezeigt =
    typeof probe.titel === "string" && probe.titel.length > 40
      ? `{ titel: ${probe.titel.length} Zeichen, jahr: ${probe.jahr} }`
      : JSON.stringify(probe);
  console.log(gezeigt);
  console.log(`  ${JSON.stringify(pruefe(probe))}`);
}

Ist es da? Ein fehlendes Feld ist in JavaScript undefined, und damit rechnet es sich erstaunlich weit, bevor es knallt.

Stimmt der Typ? "zweitausend" ist ein gültiger JSON-Wert und ein ungültiges Jahr. Achte auf Number.isInteger: Es fängt Text, null, Kommazahlen und NaN in einem Aufruf ab.

Ist der Wert plausibel? Ein Jahr 900 ist eine Zahl und trotzdem Unsinn für ein Buch. Diese Prüfung kommt aus dem Fachgebiet, nicht aus der Technik, und deshalb kann sie dir niemand abnehmen.

Ist er nicht zu lang? Das klingt nach Kosmetik und ist Sicherheit. Ein Titel mit zwei Megabyte ist kein Titel, sondern ein Versuch, deinen Speicher oder deine Datenbank vollzuschreiben. Was du speicherst, bekommt eine Obergrenze.

Alle Verstöße auf einmal, nicht einer nach dem anderen

Sieh dir den zweiten Fall im Beispiel an: Bei einem leeren Körper kommen beide Meldungen zurück, nicht nur die erste.

Das ist bequemer, als es klingt. Wer bei jedem Absenden genau einen Fehler erfährt, korrigiert ihn, schickt erneut und erfährt den nächsten. Bei vier Feldern sind das vier Runden. Eine Prüfung, die sammelt statt abzubrechen, kostet dich eine Zeile mehr und dem Aufrufer drei Runden weniger.

Dazu gehört, dass jede Meldung sagt, welches Feld und was daran falsch ist. Ein blankes „ungültige Eingabe” zwingt zum Raten.

Wo die Prüfung hingehört

Die Prüfung an der Tür
import express from "express";

const app = express();
app.use(express.json());

app.get("/", (req, res) =>
  res.type("text/plain").send(
    `curl -i -X POST -H "Content-Type: application/json" -d '{"titel":"","jahr":"gestern"}' http://localhost:3000/buecher\n` +
      `curl -i -X POST -H "Content-Type: application/json" -d '{"titel":"HTTP verstehen","jahr":2024}' http://localhost:3000/buecher\n`
  )
);

const buecher = [];
let naechsteId = 1;

function pruefe(koerper) {
  const fehler = [];

  if (typeof koerper.titel !== "string" || koerper.titel.trim() === "") {
    fehler.push({ feld: "titel", meldung: "fehlt oder ist leer" });
  }

  if (!Number.isInteger(koerper.jahr)) {
    fehler.push({ feld: "jahr", meldung: "fehlt oder ist keine ganze Zahl" });
  }

  return fehler;
}

app.post("/buecher", (req, res) => {
  // Ganz oben in der Route, vor allem anderen. Nichts Ungeprueftes
  // geht weiter.
  const fehler = pruefe(req.body);

  if (fehler.length > 0) {
    return res.status(400).json({ fehler });
  }

  const buch = { id: naechsteId++, titel: req.body.titel, jahr: req.body.jahr };
  buecher.push(buch);
  res.status(201).json(buch);
});

app.listen(3000);

Rechts stehen zwei Befehle, einer mit Unsinn und einer mit gültigen Angaben. Schick beide ab.

Ganz vorn, an der Stelle, an der die Daten hereinkommen: oben in der Route oder in einer eigenen Middleware davor. Nicht verstreut. Wenn eine Route prüft, ob der Titel da ist, und irgendwo tiefer noch einmal jemand nachsieht, weiß am Ende niemand mehr, worauf er sich verlassen darf.

Der Gewinn ist, dass alles hinter dieser Zeile mit gültigen Daten arbeitet. Das ist der ganze Sinn einer Prüfung an der Tür, und es macht den Code dahinter kürzer statt länger.

Eine Einschränkung gibt es: Bei Anmeldedaten wird man absichtlich wortkarg. „Passwort zu kurz” ist hilfreich, „Diese E-Mail-Adresse kennen wir nicht” verrät einem Fremden, wer bei dir ein Konto hat. Darauf kommt Lektion 14.3 zurück.

Und in einem echten Projekt?

Dort nimmt man dafür eine Bibliothek, meistens Zod. Man beschreibt die erwartete Form einmal, und sie prüft, meldet und liefert nebenbei die Typen dazu.

Der Kurs macht es trotzdem von Hand, und zwar genau einmal. Wer diese Schleife selbst geschrieben hat, sieht in einem Zod-Schema danach keine Zauberei mehr, sondern dieselben vier Fragen in kürzerer Schreibweise.

Zum Mitnehmen

Alles, was von außen kommt, ist erst einmal falsch. Vier Fragen an jeden Wert: Ist er da? Stimmt der Typ? Ist er plausibel? Ist er nicht zu lang?

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.