1. Rola nagłówków i cookies obok path i query
Z adresowaniem i parametrami listy jest już mniej więcej jasno: path odpowiada za konkretny zasób, query — za warunki selekcji. Ale żądanie nie kończy się na URL. Ma jeszcze dwa mniej widoczne kanały — nagłówki i cookies.
I właśnie tu łatwo narobić szumu. Nagłówki i cookies są przydatne, gdy trzeba przekazać techniczny kontekst żądania, ale prawie zawsze są złym kandydatem dla głównej semantyki biznesowej. Jeśli ukrywać tam filtry, tryby wyszukiwania czy wybór konkretnego zadania, kontrakt przestaje być czytelny gołym okiem.
2. Nagłówki jako część kontraktu: co wkładać do headers
Nagłówki są często postrzegane jako „jakaś technika, którą można ignorować". Dokładnie do momentu, gdy nagle przestaje działać JSON, bo klient przysłał dziwny Accept, albo spędzasz pół dnia szukając, dlaczego żądanie „czasem inne", a w nim okazuje się kustomowy nagłówek X-.... Nagłówki — to część kontraktu, tylko mniej oczywista niż URL, i właśnie dlatego wymagają dyscypliny.
Najważniejsza myśl tej lektury: nagłówki — to dobry kanał dla technicznego kontekstu żądania, ale zły kanał dla głównej semantyki biznesowej. Na przykład nagłówki świetnie nadają się do opisania formatu (co przyjmujemy i co oddajemy), do cachowania, do autoryzacji i do rzeczy diagnostycznych jak identyfikator korelacji. Ale jeśli zaczniesz ukrywać w nagłówkach „filtr po statusie zadania" albo „które zadanie otworzyć", zamieniasz API w quest.
Dlaczego tak? Bo URL czyta się wzrokiem i łatwo kopiuje do dokumentacji i plików .http. A nagłówki — nie. Trzeba je pamiętać, trzeba je pisać osobno, i jeśli zapomnisz jednego, serwer może zachowywać się inaczej. Kontrakt staje się „niewidoczny".
Najbardziej wymowny nagłówek: Accept
Accept mówi serwerowi: „w jakim formacie klient chce dostać odpowiedź". Dla naszego projektu roboczy i oczekiwany format to jeden — application/json: na nim opierają się DTO, żądania .http i dalsze przykłady. Ale Accept jest i tak ważny, bo format odpowiedzi — to część kontraktu, a nie dekoracyjny string w nagłówkach.
Wpływ Accept na format odpowiedzi
Spring MVC wybiera sposób konwersji odpowiedzi przez mechanizm message converters. W dokumentacji jest to opisane wprost: Spring MVC używa HttpMessageConverter do konwersji żądań i odpowiedzi.
Dla tego kursu ważny jest prosty i precyzyjny obraz: format odpowiedzi nie jest determinowany przez samą adnotację @RestController, lecz przez to, jakie konwertery w ogóle są dostępne aplikacji. W naszym baseline podłączona jest konwersja JSON przez Jackson, więc normalny scenariusz — application/json. Gdyby aplikacja obsługiwała też inne media types, Accept uczestniczyłby też w ich wyborze, więc ignorowanie go jako „służbowego szumu" nie wchodzi w grę.
3. @RequestHeader w Spring MVC: czytanie nagłówków w metodzie
Do tej pory wyciągaliśmy dane wejściowe z path i query, i wyglądało to dość przejrzyście: parametr jest w URL, więc jest w sygnaturze. Z nagłówkami jest trochę mniej intuicyjnie, bo nie są „na widoku". Ale w Spring MVC czytanie nagłówków jest tak samo proste: jawnie wskazujesz, który nagłówek potrzebujesz, i dostajesz jego wartość do argumentu metody.
W najprostszej formie @RequestHeader wiąże nazwę nagłówka z parametrem metody Java. W tym miejscu ważne jest przyzwyczajenie do dwóch rzeczy. Po pierwsze, jeśli robisz nagłówek obowiązkowym, twój endpoint zaczyna wymagać „sekretnego klucza w nagłówku", i należy to robić tylko wtedy, gdy naprawdę bez niego nie da się inaczej. Po drugie, jeśli nagłówek jest opcjonalny, twój kod musi być gotowy na null (albo wartość domyślną), inaczej zafundujesz sobie mini-DDoS z NullPointerException.
Mini-przykład: czytamy Accept
Tak, w prawdziwym API zazwyczaj nie czytamy Accept ręcznie — Spring sam decyduje, czym odpowiadać. Ale jako przykład edukacyjny Accept jest idealny: pokazuje, że nagłówek — to „kontekst żądania", a nie filtr po zadaniach.
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(
// Pobieramy techniczny kontekst żądania z nagłówka (to nie jest parametr biznesowy)
@RequestHeader(value = "Accept", required = false) String accept
) {
// Dla demonstracji edukacyjnej wypisujemy wartość; w prawdziwym projekcie częściej używa się loggera
System.out.println("Client Accept = " + accept); // Client Accept = application/json
return List.of("task-1", "task-2");
}
}
Zwróć uwagę na dwie rzeczy. Po pierwsze, oznaczyliśmy nagłówek jako opcjonalny, bo Accept może nie przyjść (albo przyjść w nieoczekiwanej formie). Po drugie, nawet tu Accept w żaden sposób nie wpływa na wybór zadań — to właśnie techniczny kontekst żądania.
Obowiązkowy vs opcjonalny nagłówek
W @RequestHeader parametr required domyślnie wynosi true. Oznacza to: jeśli napiszesz @RequestHeader("X-Client-Id") String clientId, a klient nagłówka nie prześle, żądanie nie dotrze do twojej logiki tak jak oczekujesz. Z punktu widzenia kontraktu to normalne, ale tylko jeśli naprawdę chcesz uczynić nagłówek częścią obowiązkowego protokołu.
Jeśli nagłówek jest ważny, ale gotów jesteś żyć z wartością domyślną, łatwiej ustawić defaultValue. Wtedy kontrakt staje się miększy: „jeśli nie przysłał — będzie wartość domyślna, zachowanie przewidywalne".
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 czyni kontrakt miększym: nagłówek nie jest obowiązkowy
@RequestHeader(value = "X-Client-Name", defaultValue = "unknown") String clientName
) {
// Zwracamy wartość dla poglądowości (demonstracyjny endpoint)
return "clientName=" + clientName; // clientName=unknown
}
}
Takie debug-endpointy w prawdziwym projekcie żyją częściej w innej formie, ale jako demonstracja edukacyjna są dobre: widzimy, że nagłówek — to jeszcze jedno źródło wejścia i może być zarządzany.
Przydatny nagłówek: identyfikator korelacji
Nawet w prostym API edukacyjnym czasem przydaje się przyjąć „identyfikator żądania", żeby potem szukać konkretnego wywołania w logach. Nie budujemy tu pełnej warstwy obserwowalności, ale sam chwyt czytania nagłówka można pokazać.
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(
// Identyfikator korelacji: czysto techniczny kontekst, wygodny do logów i śledzenia
@RequestHeader(value = "X-Request-Id", required = false) String requestId
) {
// Jeśli nagłówek nie przyszedł — będzie null (i to oczekiwane, bo required=false)
return "requestId=" + requestId; // requestId=4f2b...
}
}
Kluczowy sens: X-Request-Id — to właśnie techniczny kontekst, a nie „które zadanie otworzyć". Jeśli zaczniesz wybierać zadanie przez nagłówek X-Task-Id, sam stworzysz „ukryte wejście" do API.
Czytanie wszystkich nagłówków do debugowania
Technicznie można, i czasem jest to wygodne do debugowania. Ale jako projekt kontraktowy rzadko przydatne: jeśli kontroler ciągnie wszystkie nagłówki i sam decyduje „co jest ważne", to już przypomina ukrytą magię. Dlatego traktujemy to jako narzędzie diagnostyczne, a nie styl developmentu.
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(
// Pobieramy wszystkie nagłówki w całości — to wygodne do diagnostyki, ale niebezpieczne jako styl projektowania API
@RequestHeader HttpHeaders headers
) {
// Czytamy konkretny interesujący nagłówek bezpiecznie (pierwsza wartość lub null)
return "Accept=" + headers.getFirst(HttpHeaders.ACCEPT); // Accept=application/json
}
}
4. Cookies i @CookieValue: śliski kanał dla REST API
Cookies wyglądają jak wygodny mechanizm: przeglądarka sama zachowuje, przeglądarka sama wysyła, programista mniej myśli. I właśnie to czyni cookies niebezpiecznymi dla zwykłego REST API. Gdy budujesz kontrakt, zależy ci na jawności: żeby żądanie można było odtworzyć, zrozumieć, udokumentować i przetestować. A cookie często żyje „sama w sobie": jeden użytkownik przypadkowo nosi starą cookie tygodniami, inny — bez cookies w ogóle, trzeci — z dziesięcioma rozszerzeniami podmieszującymi swoje.
Technicznie cookie — to nagłówek Cookie w żądaniu (i Set-Cookie w odpowiedzi). Ale znaczeniowo — „automatycznie przekazywany stan klienta". A u nas styl interakcji (i cały nastrój kursu) jest o myśleniu stateless: żądanie musi być zrozumiałe samo z siebie, a nie zależeć od ukrytego stanu, którego w ogóle nie zobaczysz patrząc tylko na URL.
Jak czytać cookie w Spring MVC
W Spring MVC służy do tego @CookieValue. I znowu ważne są te same dwa tryby: obowiązkowa cookie (niemal zawsze zły pomysł dla prostego API) i opcjonalna (gdzie jesteś gotowy na brak).
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 — to automatycznie przynoszone przez klienta (często przez przeglądarkę)
@CookieValue(name = "theme", required = false) String theme
) {
// Demonstracja: cookie czytamy, ale logiki biznesowej na niej nie budujemy
return "theme=" + theme; // theme=dark
}
}
W tym przykładzie cookie nie wpływa na scenariusz biznesowy. To ważne: cookie tu po prostu demonstruje kanał wejścia.
Anty-przykład: filtr zadań przez cookie
To właśnie moment, gdzie API zaczyna „szumieć" i stawać się nieprzewidywalne. Człowiek patrzy na URL i myśli: „GET /api/v1/tasks — znaczy dostanę listę zadań". A w praktyce dostaje „listę zadań w statusie TODO", bo gdzieś w przeglądarce leży cookie status=TODO. To nie kontrakt, to sztuczka z znikającym królikiem.
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(
// Zły pomysł: filtr biznesowy ukryty w cookie, kontrakt staje się niewidoczny
@CookieValue(name = "status") String status
) {
// Zły pomysł: filtr ukryty w cookie, kontrakt staje się niewidoczny
return "Filtering by status=" + status;
}
}
Jeśli potrzebujesz filtra — powinien być w query (?status=TODO). Wtedy żądanie czyta się, powtarza, dokumentuje, a testy nie zależą od tego, jakie kto nosi ze sobą cookies.
5. Wybór kanału: path vs query vs header vs cookie
Gdy zaczynasz projektować API, bardzo chce się dostać „regułę na wszystkie przypadki życia". W rzeczywistości to raczej zestaw pytań do siebie samego niż żelazne prawo. Ale dobra wiadomość: te pytania szybko zamieniają się w nawyk, i przestajesz „wkładać parametry gdzie popadnie". Traktuj to jako umiejętność „składania rzeczy do szufladek", żeby potem nie szukać kluczy od mieszkania w lodówce (choć lodówka też jest skrzynką).
Poniżej — kompaktowa tabela-orientacyjna. Celowo nie o wszystkich niuansach świata, lecz o tym, co ważne właśnie teraz, na poziomie projektowania zrozumiałego kontraktu.
| Kanał wejścia | Co oznacza znaczeniowo | Co tam kłaść w normalnym REST API | Przykład | Dlaczego jest to czytelne |
|---|---|---|---|---|
| path | „Który dokładnie zasób?" | Identyfikator zasobu, pod-zasób | /api/v1/tasks/{taskId} | URL wprost odpowiada na pytanie „które zadanie?" |
| query | „Jaki tryb selekcji?" | Filtry, paginacja, sortowanie | /api/v1/tasks?status=TODO&page=0 | Parametry są widoczne i odtwarzalne |
| headers | „Jaki techniczny kontekst?" | Format (Accept), typ ciała (Content-Type), korelacja (X-Request-Id) | Accept: application/json | Nie zaśmieca URI, ale zarządza protokołem |
| cookies | „Co przeglądarka przynosi automatycznie?" | Zazwyczaj preferencje UI i sesje w świecie przeglądarki | Cookie: theme=dark | Działa w przeglądarce, ale źle dla jawnego kontraktowego API |
W tej tabeli jest ukryte główne kryterium: czy inny deweloper zrozumie żądanie, po prostu widząc je w dokumentacji lub w pliku .http. Path i query — zazwyczaj „tak". Nagłówki — czasem tak (jeśli standardowe i oczekiwane). Cookies — częściej „nie", bo przychodzą „z powietrza" (dokładniej — z przeglądarki).
6. Przykład żądania z nagłówkami i cookies
Teoria staje się znacznie bardziej zrozumiała, gdy widzisz „surowy" HTTP. Nie jako magię Springa, lecz jako zwykły tekst żądania, który można wysłać z Postmana albo z pliku .http. Teraz ważne jest zobaczenie właśnie tego: query — w stringu URL, nagłówki — osobnymi liniami, cookies — osobną linią (jako nagłówek).
Oto przykład żądania o listę zadań, gdzie jawnie pokazujemy Accept i dodajemy diagnostyczny X-Request-Id:
# Parametr query widoczny bezpośrednio w URL
GET http://localhost:8080/api/v1/tasks?status=TODO
Accept: application/json
# Techniczny kontekst do logów/śledzenia
X-Request-Id: 4f2b1c9a-3fb1-4a5a-8d8c-6b8c5d0c2d10
A oto przykład, gdzie do żądania dodano cookie. Zwróć uwagę: URL od cookie w ogóle się nie zmienia. To właśnie główny problem cookie jako kanału wejścia dla sensów biznesowych: z zewnątrz żądanie wygląda tak samo, a „w środku" może okazać się inne.
GET http://localhost:8080/api/v1/tasks
Accept: application/json
# Cookie może przyjść automatycznie z przeglądarki — URL przy tym się nie zmienia
Cookie: theme=dark
Jeśli na tym etapie czujesz lekką paranoję w stylu „tak można ukryć cokolwiek" — gratulacje, zaczynasz myśleć jak osoba projektująca kontrakt, a nie „po prostu pisząca kontrolery".
W naszym projekcie oczekiwany format roboczy — JSON: na tym opierają się DTO, żądania .http i dalsze przykłady. Ale Accept i tak nie jest dekoracyjny: Spring wybiera format odpowiedzi przez dostępne message converters, więc o media type lepiej uzgadniać jawnie, a nie liczyć, że klient i serwer „jakoś sami się dogadają".
7. Typowe błędy przy pracy z nagłówkami i cookies
Błąd №1: ukrywać filtry biznesowe w nagłówkach.
Czasem wydaje się, że „nagłówek — to elegancko, nie zaśmiecamy URL". Ale jeśli to filtr (na przykład status, assigneeName, tag), powinien żyć w query. Inaczej tworzysz ukrytą część kontraktu: klient musi znać nie tylko URI, ale i „sekretny zestaw nagłówków", bez których endpoint działa „inaczej".
Błąd №2: ukrywać filtry biznesowe w cookies.
Ten błąd jest jeszcze nieprzyjemniejszy, bo cookies często ustawia przeglądarka automatycznie. W efekcie dwóch użytkowników robi to samo żądanie GET /api/v1/tasks, a dostaje różne wyniki. Takie API jest bardzo trudne do debugowania, bo problemu nie ma w parametrach żądania (jakby ich nie było), lecz w „niewidocznym stanie".
Błąd №3: robić niestandardowy nagłówek obowiązkowym „na wszelki wypadek".
required=true domyślnie może łatwo zamienić twój endpoint w „działa tylko jeśli jesteś wtajemniczony w sekret". Obowiązkowe kustomowe nagłówki są dopuszczalne, ale prawie zawsze oznacza to, że wymyśliłeś własny protokół nad HTTP. W projekcie edukacyjnym i w zwykłym CRUD API to częściej zbędne komplikowanie.
Błąd №4: czytać „wszystkie nagłówki po kolei" i budować z tego logikę.
Technicznie możesz dostać HttpHeaders i analizować wszystko, co przyszło. Ale jeśli w kontrolerze pojawia się logika „jeśli przyszedł taki nagłówek — robimy tak, jeśli inny — inaczej", bardzo szybko zaczynasz programować „ukryte tryby" API. Traktuj ten chwyt jako debugowy, a nie kontraktowy.
Błąd №5: mieszać role nagłówków i query, podmieniając jedno drugim.
Na przykład Accept i Content-Type — to techniczna część wymiany, i ich miejsce w nagłówkach jest naturalne. Ale status zadania, priorytet i tag — to semantyka biznesowa, i jej miejsce jest w query. Gdy te strefy się mieszają, API zaczyna wyglądać jak zestaw przypadkowych umów, a nie przewidywalny kontrakt.
GO TO FULL VERSION