CodeGym /Kurse /Spring REST & MVC /@RequestHeader und Cookies: Vertrag und Rauschen

@RequestHeader und Cookies: Vertrag und Rauschen

Spring REST & MVC
Level 6, Lektion 2
Verfügbar

1. Rolle von Headern und Cookies neben Pfad und Abfrage

Mit der Adressierung und Listen-Parametern ist bereits mehr oder weniger klar: path steht für eine bestimmte Ressource, query für die Auswahlbedingungen. Aber die Anfrage endet nicht mit der URL. Sie hat noch zwei weniger offensichtliche Kanäle – Header und Cookies.

Und hier kann man leicht Lärm erzeugen. Header und Cookies sind nützlich, wenn man einen technischen Kontext der Anfrage übermitteln möchte, aber sie sind fast immer ein schlechter Kandidat für die primäre Geschäftssemantik. Wenn man dort Filter, Suchmodi oder die Auswahl einer bestimmten Aufgabe versteckt, wird der Vertrag nicht mehr auf einen Blick lesbar.

2. Header als Teil des Vertrags: Was in Headern übermitteln

Header werden oft als „eine Art Technik, die man ignorieren kann“ angesehen. Das genau bis zu dem Moment, in dem Ihr JSON plötzlich nicht mehr funktioniert, weil der Client einen seltsamen Accept-Header gesendet hat, oder Sie einen halben Tag damit verbringen herauszufinden, warum eine Anfrage „manchmal anders“ ist und sich ein benutzerdefinierter X-...-Header darin befindet. Header sind Teil des Vertrags, nur weniger offensichtlich als die URL, und erfordern daher Disziplin.

Die wichtigste Aussage dieser Lektion ist: Header sind ein guter Kanal für den technischen Kontext der Anfrage, aber ein schlechter Kanal für die primäre Geschäftssemantik. Beispielsweise eignen sich Header hervorragend zur Beschreibung des Formats (was wir akzeptieren und was wir zurückgeben), für Caching, für Autorisierung und für diagnostische Dinge wie eine Korrelations-ID. Aber wenn Sie anfangen, in Headern „einen Filter nach Aufgabenstatus“ oder „welche Aufgabe geöffnet werden soll“ zu verstecken, verwandeln Sie die API in eine Schnitzeljagd.

Warum? Weil die URL mit den Augen lesbar ist und leicht in die Dokumentation und .http-Dateien kopiert werden kann. Header dagegen nicht. Sie müssen sie sich merken, separat angeben, und wenn man einen Header vergisst, kann sich der Server anders verhalten. Der Vertrag wird „unsichtbar“.

Der aussagekräftigste Header: Accept

Accept sagt dem Server: „In welchem Format möchte der Client die Antwort erhalten.“ Für unser Projekt ist das erwartete und funktionale Format eines – application/json: Es bildet die Grundlage für DTOs, .http-Anfragen und weitere Beispiele. Aber Accept ist dennoch wichtig, denn das Antwortformat ist Teil des Vertrags, keine dekorative Zeile in den Headern.

Einfluss von Accept auf das Antwortformat

Spring MVC wählt die Art und Weise der Antwortkonvertierung über den Mechanismus der Message Converter. In der Dokumentation wird dies direkt beschrieben: Spring MVC verwendet HttpMessageConverter zur Konvertierung von Anfragen und Antworten.

Für diesen Kurs ist es wichtig, ein einfaches und genaues Bild zu behalten: Das Antwortformat wird nicht durch die Annotation @RestController selbst bestimmt, sondern dadurch, welche Converter in der Anwendung verfügbar sind. In unserer Ausgangsbasis ist die JSON-Konvertierung über Jackson eingebunden, daher ist das normale Szenario hier application/json. Wenn die Anwendung auch andere Media Types unterstützen würde, würde Accept ebenfalls bei deren Auswahl mitwirken, daher kann man ihn nicht als „dienstliches Rauschen“ ignorieren.

3. @RequestHeader in Spring MVC: Lesen von Headern in der Methode

Bis jetzt haben wir Eingabedaten aus Pfad und Abfrage geholt, und das erschien recht transparent: der Parameter ist in der URL, also ist er in der Signatur. Bei Headern ist es etwas weniger intuitiv, weil sie „nicht sichtbar“ sind. Aber in Spring MVC ist das Lesen von Headern genauso einfach: Sie geben explizit an, welchen Header Sie benötigen, und erhalten seinen Wert als Methodenargument.

Im einfachsten Fall verbindet @RequestHeader den Headernamen mit einem Java-Parameter der Methode. An dieser Stelle ist es wichtig, sich an zwei Dinge zu gewöhnen. Erstens: Wenn Sie einen Header erforderlich machen, dann verlangt Ihr Endpoint einen „geheimen Schlüssel im Header“, und das sollte nur dann getan werden, wenn es wirklich nicht anders geht. Zweitens: Wenn der Header optional ist, muss Ihr Code auf null (oder einen Standardwert) vorbereitet sein, sonst liefern Sie sich selbst einen Mini-DDoS aus NullPointerException.

Minibeispiel: Accept lesen

Ja, in einer echten API lesen wir normalerweise nicht manuell Accept – Spring entscheidet selbst, womit geantwortet wird. Aber als Lehrbeispiel ist Accept ideal: Es zeigt, dass ein Header der „Kontext der Anfrage“ ist, kein Filter nach Aufgaben.

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TaskController {

    @GetMapping("/api/v1/tasks")
    public List<String> findTasks(
            // Technischen Kontext der Anfrage aus dem Header holen (kein Geschäftsparameter)
            @RequestHeader(value = "Accept", required = false) String accept
    ) {
        // Zu Demonstrationszwecken geben wir den Wert aus; in einem realen Projekt verwendet man normalerweise einen Logger
        System.out.println("Client Accept = " + accept); // Client Accept = application/json
        return List.of("task-1", "task-2");
    }
}

Beachten Sie zwei Dinge. Erstens haben wir den Header als optional markiert, weil Accept möglicherweise nicht ankommt (oder in unerwarteter Form). Zweitens hat Accept auch hier keinen Einfluss auf die Aufgabenauswahl – es ist genau der technische Kontext der Anfrage.

Erforderlicher vs. optionaler Header

Bei @RequestHeader ist der Parameter required standardmäßig true. Das bedeutet: Wenn Sie @RequestHeader("X-Client-Id") String clientId schreiben, und der Client sendet den Header nicht, erreicht die Anfrage Ihre Logik nicht erwartungsgemäß. Vom Vertragsstandpunkt aus ist das in Ordnung, aber nur, wenn Sie den Header wirklich als Teil des obligatorischen Protokolls sehen.

Wenn der Header wichtig ist, Sie aber mit einem Standardwert leben können, ist es einfacher, defaultValue anzugeben. Dann wird der Vertrag weicher: „Wenn nicht gesendet – wird der Standardwert genommen, das Verhalten ist vorhersagbar“.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ClientInfoController {

    @GetMapping("/api/v1/debug/client")
    public String getClient(
            // defaultValue macht den Vertrag weicher: Der Header ist nicht erforderlich
            @RequestHeader(value = "X-Client-Name", defaultValue = "unknown") String clientName
    ) {
        // Wert zur Veranschaulichung zurückgeben (Demonstrations-Endpoint)
        return "clientName=" + clientName; // clientName=unknown
    }
}

Solche Debug-Endpoints leben in einem realen Projekt meist in einer anderen Form, aber als Lehrdemonstration sind sie gut: Wir sehen, dass ein Header eine weitere Eingangsquelle ist und kontrolliert werden kann.

Häufiger nützlicher Header: Korrelations-ID

Selbst in einer einfachen Lern-API kann es manchmal nützlich sein, eine „Anfrage-ID“ zu empfangen, um später einen bestimmten Aufruf in den Logs suchen zu können. Wir bauen hier keine vollständige Observability-Ebene, aber die Technik des Headers-Lesens kann man zeigen.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TaskDebugController {

    @GetMapping("/api/v1/debug/request")
    public String debugRequest(
            // Korrelations-ID: rein technischer Kontext, praktisch für Logs und Tracing
            @RequestHeader(value = "X-Request-Id", required = false) String requestId
    ) {
        // Wenn der Header nicht kommt – ist er null (und das ist erwartet, weil required=false)
        return "requestId=" + requestId; // requestId=4f2b...
    }
}

Der entscheidende Punkt ist, dass X-Request-Id genau der technische Kontext ist, nicht „welche Aufgabe geöffnet werden soll“. Wenn Sie beginnen, eine Aufgabe über den Header X-Task-Id auszuwählen, schaffen Sie selbst einen „geheimen Eingang“ zur API.

Lesen aller Header zum Debuggen

Technisch möglich und manchmal praktisch zum Debuggen. Aber als Vertragsdesign ist das selten nützlich: Wenn der Controller alle Header heranzieht und selbst entscheidet, „was wichtig ist“, ähnelt dies bereits verborgener Magie. Daher behalten wir dies als Diagnosewerkzeug, nicht als Entwicklungsstil.

import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HeadersController {

    @GetMapping("/api/v1/debug/headers")
    public String debugHeaders(
            // Alle Header auf einmal erhalten – praktisch für die Diagnose, aber gefährlich als API-Designstil
            @RequestHeader HttpHeaders headers
    ) {
        // Den konkreten gewünschten Header sicher lesen (erster Wert oder null)
        return "Accept=" + headers.getFirst(HttpHeaders.ACCEPT); // Accept=application/json
    }
}

4. Cookies und @CookieValue: Ein rutschiger Kanal für REST APIs

Cookies erscheinen als bequemer Mechanismus: Der Browser speichert selbst, der Browser sendet selbst, der Programmierer muss weniger denken. Und genau das macht Cookies für eine normale REST API gefährlich. Wenn Sie einen Vertrag aufbauen, ist Ihnen Explizitheit wichtig: Dass eine Anfrage reproduzierbar, verständlich, dokumentierbar und testbar ist. Cookie hingegen lebt oft „für sich“: Ein Benutzer trägt versehentlich ein altes Cookie wochenlang mit sich herum, ein anderer gar keine Cookies, ein dritter mit zehn Erweiterungen, die eigene hinzumischen.

Technisch gesehen ist ein Cookie der Header Cookie in der Anfrage (und Set-Cookie in der Antwort). Aber sinngemäß ist es ein „automatisch übermittelter Client-Zustand“. Unser Interaktionsstil (und die gesamte Ausrichtung des Kurses) basiert aber auf stateless‑Denken: Eine Anfrage sollte für sich selbst verständlich sein und nicht von einem verborgenen Zustand abhängen, den Sie nicht einmal sehen, wenn Sie nur auf die URL schauen.

Wie man Cookies in Spring MVC liest

In Spring MVC erfolgt dies über @CookieValue. Und auch hier sind wieder die zwei Modi wichtig: erforderliches Cookie (fast immer eine schlechte Idee für eine einfache API) und optionales Cookie (bei dem Sie auf das Fehlen vorbereitet sind).

import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CookieDemoController {

    @GetMapping("/api/v1/debug/theme")
    public String debugTheme(
            // Cookie – automatisch mitgelieferter Client-Zustand (oft aus dem Browser)
            @CookieValue(name = "theme", required = false) String theme
    ) {
        // Demonstration: Cookie wird gelesen, aber keine Geschäftslogik darauf aufgebaut
        return "theme=" + theme; // theme=dark
    }
}

In diesem Beispiel beeinflusst das Cookie kein Geschäftsszenario. Das ist wichtig: Das Cookie demonstriert hier nur den Eingangskanal.

Anti-Beispiel: Aufgabenfilter über Cookie

Das ist der Moment, in dem die API anfängt zu „rauschen“ und unvorhersehbar zu werden. Jemand schaut auf die URL und denkt: „GET /api/v1/tasks – das bedeutet ich bekomme die Aufgabenliste“. In Wirklichkeit erhält er „Liste der Aufgaben im Status TODO“, weil irgendwo im Browser ein Cookie status=TODO liegt. Das ist kein Vertrag, sondern ein Zaubertrick mit verschwindendem Kaninchen.

import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AntiPatternController {

    @GetMapping("/api/v1/tasks")
    public String findTasks(
            // Schlechte Idee: Geschäftsfilter im Cookie versteckt, Vertrag wird unsichtbar
            @CookieValue(name = "status") String status
    ) {
        // Schlechte Idee: Filter im Cookie versteckt, Vertrag wird unsichtbar
        return "Filtering by status=" + status;
    }
}

Wenn Sie einen Filter benötigen – sollte er im Query sein (?status=TODO). Dann ist die Anfrage lesbar, wiederholbar, dokumentierbar, und Tests hängen nicht davon ab, wer welche Cookies mit sich trägt.

5. Kanalauswahl: path vs. query vs. header vs. cookie

Wenn man mit dem API-Design beginnt, wünscht man sich eine „Regel für alle Fälle“. In der Realität ist es eher eine Reihe von Fragen an sich selbst als ein eisernes Gesetz. Aber die gute Nachricht: Diese Fragen werden schnell zur Gewohnheit, und Sie hören auf, Parameter willkürlich hineinzustecken. Betrachten Sie es als Fähigkeit, „Dinge in Schubladen zu legen“, damit Sie später nicht die Wohnungsschlüssel im Kühlschrank suchen (obwohl der Kühlschrank auch so eine Art Box ist).

Nachfolgend eine kompakte Richtlinientabelle. Sie behandelt bewusst nicht alle Nuancen der Welt, sondern das, was gerade wichtig ist – auf der Ebene des Entwurfs eines verständlichen Vertrags.

Eingangskanal Bedeutung Was in eine normale REST API gehört Beispiel Warum lesbar
path „Welche Ressource genau?“ Ressourcen-ID, Unterressource /api/v1/tasks/{taskId} URL beantwortet direkt die Frage „welche Aufgabe?“
query „Welcher Auswahlmodus?“ Filter, Paginierung, Sortierung /api/v1/tasks?status=TODO&page=0 Parameter sind sichtbar und reproduzierbar
headers „Welcher technische Kontext?“ Format (Accept), Body-Typ (Content-Type), Korrelation (X-Request-Id) Accept: application/json Verschmutzt nicht die URI, steuert aber das Protokoll
cookies „Was der Browser automatisch mitbringt?“ Normalerweise UI-Präferenzen und Sessions in der Browser-Welt Cookie: theme=dark Funktioniert im Browser, aber schlecht für eine explizite Vertrags-API

In dieser Tabelle steckt ein verstecktes Hauptkriterium: Kann ein anderer Entwickler die Anfrage verstehen, indem er sie einfach in der Dokumentation oder in einer .http-Datei sieht. Path und query sind normalerweise „ja“. Header – manchmal ja (wenn standardisiert und erwartet). Cookies – meistens „nein“, weil sie „aus der Luft kommen“ (genauer: aus dem Browser).

6. Beispiel einer Anfrage mit Headern und Cookies

Theorie wird viel verständlicher, wenn man „rohes“ HTTP sieht. Nicht als Spring-Magie, sondern als gewöhnlichen Anforderungstext, den man aus Postman oder einer .http-Datei senden kann. Jetzt ist es wichtig, genau zu sehen: Query in der URL-Zeile, Header in separaten Zeilen, Cookies in einer separaten Zeile (als Header).

Hier ist eine Beispielanfrage für die Aufgabenliste, bei der wir explizit Accept zeigen und eine diagnostische X-Request-Id hinzufügen:

# Query-Parameter direkt in der URL sichtbar
GET http://localhost:8080/api/v1/tasks?status=TODO
Accept: application/json
# Technischer Kontext für Logs / Tracing
X-Request-Id: 4f2b1c9a-3fb1-4a5a-8d8c-6b8c5d0c2d10

Und hier ein Beispiel, bei dem der Anfrage ein Cookie hinzugefügt wurde. Beachten Sie: Die URL ändert sich durch das Cookie überhaupt nicht. Das ist das Hauptproblem von Cookies als Eingangskanal für Geschäftsinhalte: Außen sieht die Anfrage gleich aus, aber „innen“ kann sie anders sein.

GET http://localhost:8080/api/v1/tasks
Accept: application/json
# Cookie kann automatisch vom Browser mitgeliefert werden – die URL ändert sich dabei nicht.
Cookie: theme=dark

Wenn Sie an diesem Punkt eine leichte Paranoia verspüren im Stil „so kann man alles verstecken“ – herzlichen Glückwunsch, Sie beginnen wie jemand zu denken, der einen Vertrag entwirft, und nicht nur „einfach Controller schreibt“.

In unserem Projekt ist das erwartete Arbeitsformat JSON: DTOs, .http-Anfragen und weitere Beispiele sind darauf ausgelegt. Aber Accept ist trotzdem nicht dekorativ: Spring wählt das Antwortformat über die verfügbaren Message Converter, daher ist es besser, den Media Type explizit zu vereinbaren, anstatt darauf zu hoffen, dass Client und Server „irgendwie selbst klarkommen“.

7. Typische Fehler im Umgang mit Headern und Cookies

Fehler Nr. 1: Geschäftsfilter in Headern verstecken.
Manchmal scheint „Header – das ist schön, wir überladen die URL nicht“. Aber wenn es ein Filter ist (z. B. status, assigneeName, tag), sollte er im Query leben. Sonst schaffen Sie einen versteckten Teil des Vertrags: Der Client muss nicht nur den URI kennen, sondern auch den „geheimen Satz von Headern“, ohne die der Endpoint „anders“ funktioniert.

Fehler Nr. 2: Geschäftsfilter in Cookies verstecken.
Dieser Fehler ist noch unangenehmer, weil Cookies oft automatisch vom Browser gesetzt werden. Infolgedessen machen zwei Benutzer dieselbe Anfrage GET /api/v1/tasks, erhalten aber unterschiedliche Ergebnisse. Solche APIs sind sehr schwer zu debuggen, weil das Problem nicht in den Anfrageparametern (die es scheinbar nicht gibt) liegt, sondern im „unsichtbaren Zustand“.

Fehler Nr. 3: Einen nicht standardmäßigen Header „vorsichtshalber“ erforderlich machen.
required=true kann standardmäßig leicht Ihren Endpoint in „funktioniert nur, wenn man in das Geheimnis eingeweiht ist“ verwandeln. Erforderliche benutzerdefinierte Header sind zulässig, aber das bedeutet fast immer, dass Sie Ihr eigenes Protokoll auf HTTP aufsetzen. In einem Studienprojekt und einer normalen CRUD-API ist dies meist eine überflüssige Komplikation.

Fehler Nr. 4: „Alle Header nacheinander“ lesen und daraus Logik ableiten.
Technisch können Sie HttpHeaders erhalten und alles analysieren, was angekommen ist. Aber wenn im Controller eine Logik wie „wenn dieser Header kommt – mach so, wenn ein anderer – anders“ auftaucht, programmieren Sie sehr schnell „versteckte Modi“ der API. Behalten Sie diese Technik als Debug-Methode, nicht als Vertragsstil.

Fehler Nr. 5: Rollen von Headern und Query vermischen, eines durch das andere ersetzen.
Zum Beispiel sind Accept und Content-Type technische Teile des Austauschs, und ihr Platz ist natürlich in den Headern. Aber Aufgabenstatus, Priorität und Tag sind Geschäftssemantik und gehören in den Query. Wenn diese Bereiche vermischt werden, sieht die API aus wie eine Sammlung willkürlicher Vereinbarungen und nicht wie ein vorhersagbarer Vertrag.

1
Aufgabe
Spring REST & MVC, Level 6, Lektion 2
Gesperrt
Lesen des Headers `X-Request-Id`
Lesen des Headers `X-Request-Id`
1
Aufgabe
Spring REST & MVC, Level 6, Lektion 2
Gesperrt
Lesen des Cookies `theme`
Lesen des Cookies `theme`
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION