1. Die Rolle von Headern: Sinn, nicht Schönheit
Wenn Sie bisher Header als „irgendwelche Zeilen, die Tools in einem separaten Tab verstecken“ betrachtet haben, machen wir sie jetzt zu einem normalen Teil des Denkens. Header sind Metadaten: Sie sind nicht die eigentlichen Ressourcendaten, aber sie sagen, wie man die Anfrage und Antwort verstehen soll. Und oft entscheiden sie, ob der Body richtig geparst wird, ob der Client das Antwortformat verstehen kann und wie der Client nach der Erstellung einer Ressource weiterarbeiten kann.
Auf der Ebene des „Grundgerüsts“ sieht eine HTTP-Nachricht etwa so aus:
<start line> (request line oder status line)
<header>: <value>
<header>: <value>
... weitere Header ...
<leere Zeile>
<body> (kann fehlen)
Vergleichen Sie zwei Welten. In einem Konsolenprogramm übergeben wir Daten normalerweise direkt „über die Leitung“: method(arg). Niemand muss extra schreiben „Argument ist ein UTF-8-String“. In HTTP wird alles als Bytes übertragen, und ohne Metadaten beginnen beide Seiten zu raten. Header sind wie Aufkleber auf dem Paket: „GLAS“, „OBEN“, „NICHT WERFEN“, „DIESE SEITE ÖFFNEN“. Das Paket selbst ist der Body, aber die Aufkleber sind oft wichtiger, als es scheint.
Und noch ein wichtiger Punkt. Wir haben gestern und am Vormittag gesagt: Der Statuscode ist eine eigene Bedeutungsebene. Nun, Header sind ebenfalls eine eigene Bedeutungsebene. Keine „Dekoration“, sondern ein Teil des Vertrags. Wenn Status und Body sagen „ich habe dies und das getan“, beantworten Header oft die Frage „in welcher Form“ und „was als nächstes tun“.
2. Content-Type: Was genau im Body liegt
Content-Type ist ein Header, der eine sehr bodenständige Frage beantwortet: „In welchem Format wird der Body gesendet“. Und das gilt sowohl für Request als auch Response. Wenn ein Body vorhanden ist, sollte fast immer klar sein, was darin ist: Text, JSON, Binärdatei, etwas anderes. Wenn kein Body vorhanden ist (z.B. GET ohne Body oder 204 No Content), dann ist der Sinn von Content-Type meist verloren.
Beginnen wir mit dem wichtigsten Gedanken: Content-Type sagt nicht aus, „was ich erhalten möchte“, sondern was ich tatsächlich gesendet habe. Wenn ich JSON gesendet habe, schreibe ich das so:
Content-Type: application/json
Wenn ich einfachen Text gesendet habe (z. B. eine sehr lehrreiche /health-Antwort als Zeichenkette), könnte das sein:
Content-Type: text/plain
In der echten Entwicklung werden Sie am häufigsten application/json sehen (und das wird fast immer Ihr Standard für eine API sein), aber es ist hilfreich, auch andere Varianten zu kennen – zumindest um nicht überrascht zu sein.
Minitabelle „Was Sie am häufigsten antreffen werden“
| Body-Format | Typischer Content-Type | Wo tritt es häufig auf |
|---|---|---|
| JSON | application/json | fast jede Backend-API |
| Einfacher Text | text/plain | einfache Health-/Debug-Antworten, Prototypen |
| HTML | text/html | Browser-Seiten (nicht für unseren Kurs) |
Beachten Sie: Wir haben JSON als Format noch nicht kennengelernt (das kommt auf einer eigenen Stufe), aber das hindert uns nicht daran, die Rolle des Headers zu verstehen. Selbst wenn Sie nicht wissen, was drin ist, verstehen Sie bereits, dass es sich um einen „Datentyp“ handelt.
### Ohne Content-Type: alles fällt auseinander
Stellen Sie sich vor, der Client sendet einen POST mit Body, aber der Server erwartet JSON. Wenn der Client keinen Content-Type angibt, muss der Server raten. Manchmal versucht der Server zu raten, manchmal lehnt er ab, manchmal „frisst“ er es als Text und scheitert dann beim Parsen. Am Ende haben Sie die Situation: Body vorhanden, aber keine Bedeutung.
Und umgekehrt kann der Server einen Body zurückgeben, aber wenn er keinen Content-Type angibt, versteht der Client nicht, wie er ihn parsen soll. Ja, manche Clients versuchen zu raten (anhand der ersten Zeichen, mittels Heuristiken, nach „das sieht doch nach JSON aus“), aber das ist eine schlechte Angewohnheit. Der Vertrag muss explizit sein.
### Einfaches Java-Beispiel
Auch wenn wir noch keinen HTTP-Client in Java schreiben und keinen Server betreiben, können wir das Denken in „Verträgen“ direkt in der ReadLaterApplication üben. Einfach als sauberes Modell: Welche Header sollten wir haben.
import java.util.Map;
public class ReadLaterApplication {
public static void main(String[] args) {
// Header sind Teil des Vertrags: wie man den Body versteht und was man in der Antwort erwartet
Map<String, String> requestHeaders = Map.of(
"Content-Type", "application/json", // was genau wir im Body gesendet haben
"Accept", "application/json" // welches Antwortformat wir erhalten möchten
);
// Werte über den Headernamen auslesen – genauso wie es der echte HTTP-Stack macht
System.out.println(requestHeaders.get("Content-Type")); // application/json
System.out.println(requestHeaders.get("Accept")); // application/json
}
}
Das ist kein „echtes HTTP“, aber es ist eine nützliche Disziplin: Sie beginnen zu sehen, dass Header kein zufälliges Rauschen sind, sondern obligatorische Felder eines Vertrags.
### Eine feine, aber wichtige Nuance: Content-Type ist nicht immer sinnvoll
Wenn Sie einen GET ohne Body ausführen, ist Content-Type im Request normalerweise nicht nötigt. Er beschreibt den Request-Body, aber es gibt keinen Body – also gibt es nichts zu beschreiben. Gleiches gilt für die Antwort 204 No Content: Da kein Body vorhanden ist, ist Content-Type zumindest bedeutungslos. Manche Server setzen ihn trotzdem „aus Gewohnheit“, aber in einem guten Vertrag versucht man, solche Details zu vermeiden, um den Client nicht in die Irre zu führen.
3. Accept: Welches Antwortformat der Client erwartet
Accept ist fast spiegelbildlich, aber das Schlüsselwort ist hier „erwartet“. Wenn Content-Type sagt „in welchem Format ich den Body gesendet habe“, dann sagt Accept „in welchem Format ich den Body erhalten möchte“. Es ist also ein Request-Header, der dem Server hilft, das Antwortformat zu wählen, falls der Server überhaupt mehrere Formate beherrscht.
In der Praxis ist bei einer kleinen Lern-API (und den meisten echten APIs) das Format meistens JSON. Aber selbst dann ist Accept als Teil der Disziplin und als explizite Vereinbarung nützlich: Der Client „hofft“ nicht, sondern erklärt seine Erwartung.
Ein typischer Lese-Request könnte so aussehen:
GET /api/v1/reading-list HTTP/1.1
Accept: application/json
Hier gibt es keinen Body, daher ist Content-Type im Request nicht nötig, aber Accept ist sinnvoll: „Server, bitte gib mir JSON zurück.“
### Accept garantiert nicht, sondern vereinbart
Es ist wichtig, die Psychologie dieses Headers zu verstehen. Accept ist kein Befehl im Stil von „mach das so, sonst bin ich beleidigt“, sondern eher „ich kann das so verarbeiten, es ist mir so am liebsten“. Wenn der Server das angegebene Format nicht unterstützt, kann er ein anderes zurückgeben (was schlecht ist) oder ehrlich mitteilen, dass er es nicht kann (in der Realität gibt es dafür eigene Statuscodes, aber heute gehen wir nicht in die Code-Enzyklopädie).
Im Rahmen unseres Kurses können wir eine einfache Regel aufstellen: Der Client setzt Accept: application/json, der Server antwortet mit Content-Type: application/json. Das macht den Vertrag lesbar und vorhersagbar, selbst ohne tiefe Theorien zur „Inhaltsaushandlung“.
### Mini-Falle für Anfänger: Accept und Content-Type sind unterschiedliche Seiten
Ein sehr häufiger Fehler: zu denken, dass Accept den Request-Body beschreibt. Tut es nicht. Accept bezieht sich auf die Antwort. Im Request beschreibt nur Content-Type den Body.
Wenn Sie sich eine kleine Eselsbrücke merken, wird es einfacher: Accept bedeutet „ich akzeptiere solche Formate“ (also was ich „verdauen“ kann), und Content-Type bedeutet „hier drin ist das“ (also was Sie mir gebracht haben).
### Java-Beispiel: Lernmodell
import java.util.LinkedHashMap;
import java.util.Map;
public class ReadLaterApplication {
public static void main(String[] args) {
// LinkedHashMap wird hier nur wegen der stabilen Reihenfolge bei der Ausgabe benötigt
Map<String, String> headers = new LinkedHashMap<>();
// Accept beschreibt, welches Antwortformat der Client „verdauen“ kann
headers.put("Accept", "application/json");
// Ausgabe – nur um die Idee „Header als Wörterbuch“ visuell zu festigen
System.out.println(headers); // {Accept=application/json}
}
}
Hier verwenden wir LinkedHashMap, damit die Reihenfolge in der Ausgabe stabiler aussieht (eine Kleinigkeit, aber manchmal angenehm für den Geist, der sich gerade mit Verträgen anfreundet).
4. Location: Adresse der erstellten Ressource
Location ist ein Response-Header, der meistens zusammen mit 201 Created auftritt. Sein Sinn ist sehr praktisch: Wenn der Client eine neue Ressource erstellt hat (normalerweise über POST), sollte der Server dem Client helfen zu verstehen, wo diese Ressource jetzt lebt. Nicht „irgendwo in Ihrem Speicher“, sondern unter welcher URI sie abgerufen werden kann.
Im Kontext unserer zukünftigen ReadLater-API passt das perfekt. Der Client sendet POST /api/v1/reading-list – „erstelle mir ein Element der Leseliste“. Der Server erstellt es, vergibt eine id, und sagt in der Antwort: „erstellt, hier ist die Adresse der neuen Ressource“.
Beispiel (als „rohe“ Antwort):
HTTP/1.1 201 Created
Location: /api/v1/reading-list/43
Content-Type: application/json
{"id":43,"title":"Clean Code","author":"Robert C. Martin","status":"PLANNED"}
Wir analysieren noch keine JSON-Struktur – uns interessiert etwas anderes: Der Client kann sofort den Location-Header nehmen und einen GET auf diese Adresse ausführen, ohne raten zu müssen, wohin der Server die Ressource gelegt hat.
### Location im Header, nicht im JSON
Man könnte fragen: „Warum nicht ein Feld url in den Body aufnehmen?“ Das kann man auch (manchmal wird es so gemacht), aber Location ist der standardisierte „Sprach“-Mechanismus von HTTP für solche Situationen. Er ist gut, weil er nicht an das Body-Format gebunden ist. Heute haben Sie JSON, morgen (theoretisch) ein anderes Format – der Header bleibt ein Header und trägt weiterhin seine Bedeutung.
Außerdem harmoniert er schön mit dem Status. 201 sagt „erstellt“, Location sagt „hier ist es“. Das fühlt sich wie ein zusammenhängender Vertrag an, nicht wie eine zufällige Sammlung von Feldern.
### Relativer und absoluter Location
In Lehrbeispielen wird oft ein relativer Pfad verwendet:
Location: /api/v1/reading-list/43
Das ist in Ordnung, besonders wenn der Client bereits host und port kennt. In großen Systemen bevorzugt man möglicherweise eine absolute URI (vollständig mit https://...), aber für unser Niveau reicht es, das Prinzip zu verstehen: Location gibt die Adresse der neuen Ressource an.
### Mini-Beispiel in Java: „Location aus der ID zusammenbauen“
public class LocationHeaderDemo {
public static void main(String[] args) {
// Die ID der gerade erstellten Ressource (wird normalerweise von der Datenbank/dem Server vergeben)
long id = 43L;
// Location ist die Adresse, unter der die Ressource später per GET abgerufen werden kann
String location = "/api/v1/reading-list/" + id;
// Ausgabe, um die resultierende Header-Zeichenfolge zu sehen
System.out.println(location); // /api/v1/reading-list/43
}
}
Das klingt banal, aber genau diese Logik werden Sie später in der Serverphase schreiben, wenn Sie echte Antworten erstellen. Jetzt halten wir nur fest: Location ist Teil des Vertrags „Ressourcenerstellung“.
5. Allow und 405 Method Not Allowed
Der Header Allow taucht normalerweise auf, wenn der Server mit dem Status 405 Method Not Allowed antwortet. Das ist eine recht subtile, aber sehr nützliche Situation: Der Pfad existiert, der Server versteht, um welche Ressource/Route es sich handelt, aber die gewählte HTTP-Methode wird für diesen Pfad nicht unterstützt.
Wichtig: Das ist nicht dasselbe wie 404 Not Found. 404 bedeutet „Ressource/Route nicht gefunden“, 405 bedeutet „Route gefunden, aber Sie kommen mit der falschen Methode“.
Ein Beispiel aus unserer zukünftigen API. Angenommen, für die Sammlung /api/v1/reading-list ist es sinnvoll, GET (Liste lesen) und POST (Element erstellen) zu unterstützen. Wenn der Client versehentlich einen DELETE dorthin sendet, könnte der Server antworten:
HTTP/1.1 405 Method Not Allowed
Allow: GET, POST
Der Sinn von Allow ist sehr freundlich (und sogar leicht passiv-aggressiv, wenn man es mit einer bestimmten Betonung liest): „Lieber Client, hier ist die Liste der Methoden, die hier überhaupt Sinn ergeben.“ Der Client muss nicht raten, was er als nächstes tun soll – der Server listet explizit die zulässigen Optionen auf.
### Warum das selbst für einen Junior nützlich ist
Erstens macht es die API vorhersagbar. Der Client kann sich schnell korrigieren. Zweitens ist es eine gute frühe Gewohnheit für Genauigkeit: Sie vermischen nicht „falsche Methode“ mit „nicht gefunden“ oder „irgendwas ist kaputt“. Drittens ist es einer der Momente, in denen Spring MVC später wirklich hilft: Das Framework kann 405 und Allow korrekt zurückgeben, wenn Sie Ihre Handler richtig deklarieren. Aber um die Automatisierung zu schätzen, ist es hilfreich, das Konzept einmal manuell verstanden zu haben.
### Mini-Beispiel: Allow im Code
import java.util.Set;
import java.util.StringJoiner;
public class AllowHeaderDemo {
public static void main(String[] args) {
// Menge der Methoden, die auf dem jeweiligen Pfad tatsächlich erlaubt sind
Set<String> allowedMethods = Set.of("GET", "POST");
// Füge die Methoden zu einem String im Format "GET, POST" zusammen – so sieht der Allow-Wert aus
StringJoiner joiner = new StringJoiner(", ");
for (String method : allowedMethods) {
// Jede Methode ist ein separates Element der Liste
joiner.add(method);
}
// Dies wird der Wert des Allow-Headers in der Server-Antwort sein
String allowHeaderValue = joiner.toString();
System.out.println(allowHeaderValue); // GET, POST (die Reihenfolge kann variieren)
}
}
Das ist nur eine Demonstration der Idee: Allow ist eine Aufzählung von Methoden. Später werden wir über Reihenfolge, Stabilität und die Frage, wo diese Logik gespeichert wird, nachdenken, aber heute ist uns die Semantik wichtig.
6. Wie das in den ReadLater-Szenarien aussieht
Jetzt starten wir noch keinen eigenen HTTP-Server und schreiben keinen HTTP-Client in Java – das kommt viel später im Kurs. Aber es ist bereits heute nützlich, die Header auf unsere zukünftigen ReadLater Starter-Szenarien „anzuprobieren“, damit sie nicht abstrakt bleiben. Wir betrachten sie als Teil einer Vereinbarung: Was der Client verspricht/erwartet und was der Server bestätigt/mitteilt.
Unten ist eine kleine Tabelle als „Spickzettel“ speziell für die Header, ohne den Versuch, alles andere zu analysieren (Methoden und Statuscodes haben Sie bereits in den Lektionen 1–2 gesehen, und die Platzierung von Daten in Path/Query/Body kommt in der nächsten Lektion).
| Szenario | Request-Header (Minimum) | Response-Header (Minimum) |
|---|---|---|
| Liste lesen | Accept: application/json | Content-Type: application/json |
| Element erstellen | Content-Type: application/json und Accept: application/json | Content-Type: application/json, und bei 201 zusätzlich Location: /api/v1/reading-list/{id} |
| Element löschen | (normalerweise reicht Accept) | bei 204 normalerweise kein Content-Type und kein Body |
| Nicht unterstützte Methode auf vorhandenem Pfad | (beliebig) | Allow: ... zusammen mit 405 |
Wenn Sie diese Tabelle als „Vertragsgewohnheiten“ betrachten, ergeben sich einfache Regeln. Accept bedeutet „ich möchte dieses Format“, Content-Type bedeutet „ich habe dieses Format gesendet/zurückgegeben“, Location bedeutet „hier ist die Adresse von dem, was Sie gerade erstellt haben“, und Allow bedeutet „hier ist, was man hier überhaupt tun kann“.
Und ja, das ist der Moment, in dem viele Anfänger zunächst versuchen, nur mit dem Body zu leben: Sie senden JSON, erhalten JSON und denken, das sei genug. Aber dann stoßen sie plötzlich auf eine Situation, in der der Server „verstehe ich nicht“ oder „falsche Methode“ sagt, und es stellt sich heraus, dass ohne Status und Header der Vertrag überhaupt nicht lesbar ist.
7. Typische Fehler beim Arbeiten mit Headern
Fehler mit Headern sind oft tückisch, weil sie „nicht ernst“ aussehen: eine Zeile geändert, na und. Aber in der Praxis kann genau eine Zeile dazu führen, dass Sie eine Stunde Debugging verlieren und anfangen, eine universelle Verschwörung, Java, Gradle und das Wetter draußen zu verdächtigen. Daher ist es besser, im Voraus zu wissen, wo die häufigsten Fallstricke liegen.
Fehler Nr. 1: Accept und Content-Type verwechseln.
Das klassischste Szenario: Sie senden einen POST mit JSON, setzen aber nur Accept: application/json und vergessen Content-Type. Infolgedessen haben Sie ehrlich mitgeteilt, welche Antwort Sie haben möchten, aber dem Server überhaupt nicht gesagt, was Sie im Body gesendet haben. Der Server versucht möglicherweise zu raten, lehnt ab oder „liest es als Text“. In jedem Fall wird der Vertrag implizit und damit zerbrechlich.
Fehler Nr. 2: Content-Type verspricht etwas, aber der Body enthält etwas anderes.
Das ist fast so, als würde man eine Schachtel mit „Tee“ beschriften, Kaffee hineinlegen und sich wundern, dass der Empfänger „etwas Seltsames“ aufbrüht. Zum Beispiel Content-Type: application/json, aber der Body ist einfach die Zeichenkette title=Clean Code. Selbst wenn der Server es „irgendwie schluckt“, erschweren Sie sich das Leben: Eine stabile Wartung von Client und Server wird unmöglich, weil die Vereinbarung verletzt ist.
Fehler Nr. 3: Content-Type setzen, wo kein Body ist (und erwarten, dass es etwas ändert).
Manchmal setzen Anfänger Content-Type in einen GET-Request „für alle Fälle“. Das ist keine Katastrophe, aber es ist Rauschen. Der Header beschreibt einen Body, der nicht existiert – also führt er in die Irre und erzeugt das Gefühl, dass irgendwo ein Body sein sollte. Solche Kleinigkeiten werden später zu „Vertragsstaub“, der schwer zu entfernen ist.
Fehler Nr. 4: Location als dekoratives Band „für jeden Erfolg“ verwenden.
Location ist keine „zusätzliche Information in der Antwort“, sondern eine spezifische Bedeutung: die Adresse einer Ressource (meistens einer gerade erstellten). Wenn Sie Location als Antwort auf GET oder PUT zurückgeben, beginnt der Client zu denken: „Hm, was habe ich jetzt erstellt? Wohin werde ich gerufen?“ Manchmal ist Location auch in anderen Szenarien sinnvoll, aber auf unserem Niveau ist es besser, eine strenge Regel zu befolgen: POST hat eine Ressource erstellt – also 201 und Location.
Fehler Nr. 5: Allow entspricht nicht der Realität.
Wenn der Server Allow: GET, POST sagt, aber POST funktioniert tatsächlich nicht (oder umgekehrt), geht es dem Client schlechter, als wenn Allow ganz fehlen würde. Der Header wird zu einem falschen Hinweis. Daher ist Allow kein „Text fürs Häkchen“, sondern Teil des Vertrags: Zählen Sie nur das auf, was für diesen Pfad wirklich unterstützt wird.
GO TO FULL VERSION