CodeGym /Kursy /Java Server /Konfiguracja HttpRequest

Konfiguracja HttpRequest

Java Server
Poziom 15, Lekcja 1
Dostępny

1. HttpRequest jako „paszport” żądania

Kiedy początkujący pisze klienta HTTP, często myśli tak: „Znam już URL, więc żądanie prawie gotowe”. Ale w rzeczywistości URL to tylko adres. Aby żądanie stało się żądaniem, musimy ustalić jeszcze metodę, nagłówki i, jeśli to nie GET, to także body. W Javie obiekt HttpRequest pełni właśnie rolę „paszportu” wywołania: jest w nim wprost zapisane, o co prosimy serwer i w jakiej formie.

Patrząc na to „z lotu ptaka”, HttpRequest to coś, co najpierw starannie składasz, a potem oddajesz do client.send(...). Dlatego im czytelniej wygląda budowa żądania, tym mniej masz zagadek poziomu „dlaczego serwer mnie nie rozumie”.

Mały schemat (bez prób stania się podręcznikiem RFC):

flowchart TD
    %% Ważne: sieć pojawia się dopiero na etapie send, do tego momentu po prostu "składamy paszport" żądania
    A["URI (dokąd idziemy)"] --> B["HttpRequest.Builder (jak dokładnie idziemy)"]
    B --> C["HttpRequest (ustalona konfiguracja)"]
    C --> D["HttpClient.send(...) (próba pójścia do sieci)"]
    D --> E["HttpResponse (status + headers + body)"]

Zwróć uwagę: sieć pojawia się dopiero na etapie send. Wszystko, co przed tym — to nasze decyzje, i wygodnie widzieć je wprost w kodzie.

Minimalny „szkielet” żądania wygląda tak:

import java.net.URI;
import java.net.http.HttpRequest;

// Dokąd idziemy: to tylko adres, nie "gotowe żądanie"
URI uri = URI.create("https://catalog.example/search?q=java");

// Składamy "paszport" żądania: metoda + (potem w razie potrzeby) nagłówki i body
HttpRequest request = HttpRequest.newBuilder(uri)
        .GET()   // Jawnie ustalamy metodę
        .build(); // Po build() żądanie staje się niemodyfikowalne

Tak, to już jest żądanie. Ale na razie zbyt „nieme”: nie ma w nim umowy co do formatu odpowiedzi, a w realnym projekcie takie żądania szybko zamieniają się w „działa… dopóki działa”.

2. HttpRequest.Builder: budowanie żądania

W API HttpClient bardzo udanie zrobiono coś, co początkującym zazwyczaj się podoba: żądanie składa się przez builder, a to oznacza, że można je czytać z góry na dół jak mały scenariusz. W każdej linii odpowiadasz na pytanie: dokąd idziemy, czego oczekujemy w odpowiedzi, czym (jeśli trzeba) obciążamy żądanie, jaką metodę wybieramy. To szczególnie przydatne w projekcie edukacyjnym, gdzie celowo nie chowamy mechaniki za frameworkami.

Ważny moment: HttpRequest po build() staje się niemodyfikowalny. To znaczy, że żądanie jest „zamrożone” i przypadkiem go nie zepsujesz. A HttpRequest.Builder to rzecz tymczasowa: złożyliśmy, zbudowaliśmy, zapomnieliśmy. Nie próbuj „reużywać buildera na wszystkie okazje życia”, bo dość szybko złapiesz nieoczywisty bałagan: gdzieś dodałeś nagłówek, a on niespodziewanie „przykleił się” do innego żądania.

Typowa czytelna budowa żądania GET w naszym stylu:

import java.net.URI;
import java.net.http.HttpRequest;

URI uri = URI.create("https://catalog.example/search?q=clean+code");

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json") // Umawiamy się co do formatu odpowiedzi
        .GET()                                // Ustalamy metodę
        .build();                             // "Zamrażamy" konfigurację żądania

Tu już widać trzy rzeczy: dokąd idziemy, co chcemy dostać, jaką metodą idziemy. To w zasadzie nasz mały „kontrakt” klienta z serwerem, tylko od strony klienta.

3. GET: nagłówek Accept i „uprzejme” oczekiwanie odpowiedzi JSON

Do tej pory często pracowaliśmy z odpowiedzią jak z surowym stringiem, i to normalne na start. Ale nawet gdy czytamy body jako string, nadal ważne, by umówić się z serwerem, jakiego formatu odpowiedzi się spodziewamy. W HTTP służy do tego nagłówek Accept. Oznacza on: „Serwerze, jeśli potrafisz, odpowiedz mi w takim formacie”.

W praktyce niektóre API zawsze odpowiadają JSON-em i ignorują Accept. Ale w projekcie edukacyjnym ważny jest nawyk: Accept to część normalnego kontraktu HTTP. Plus bywają API, które potrafią odpowiadać i JSON-em, i czymś jeszcze (np. HTML albo XML), i tam Accept realnie decyduje o tym, co dostaniesz.

Kontynuujmy nasz scenariusz wyszukiwania książek w katalogu. Załóżmy, że już zbudowaliśmy URI (z wykładu 1) i teraz robimy żądanie:

import java.net.URI;
import java.net.http.HttpRequest;

// W URI są już zaszyte parametry query (to część adresu)
URI uri = URI.create("https://catalog.example/search?q=clean+code&limit=5");

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json") // Prosimy o JSON (jeśli serwer potrafi)
        .GET()                                // GET bez body — normalny scenariusz
        .build();

Zwróć uwagę: nie ustawiamy Content-Type dla GET. Dlaczego? Bo Content-Type opisuje ciało żądania, a w GET ciała zwykle nie ma. Tak, bywają egzotyczne przypadki „GET z body”, ale jeśli nie chcesz cierpieć — nie trzeba. Budujemy przewidywalnego klienta, a nie rebus.

Czasem warto dodać jeszcze User-Agent. Niektóre zewnętrzne serwisy traktują go kapryśnie, a czasem po nim łatwiej zdiagnozować, że żądanie przyszło właśnie od naszej aplikacji. Ale nie zamieniaj nagłówków w kolekcję znaczków: dodajemy tylko to, co rozumiemy.

Na przykład tak:

import java.net.URI;
import java.net.http.HttpRequest;

URI uri = URI.create("https://catalog.example/search?q=java");

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json")           // Co chcemy dostać
        .header("User-Agent", "readlater-starter/1.0")  // Kim jesteśmy (do diagnostyki i kapryśnych API)
        .GET()
        .build();

4. Accept i Content-Type: różnice

Mylenie Accept i Content-Type to prawie obowiązkowy „rytuał inicjacji” w HTTP. Nic strasznego: te nagłówki rzeczywiście mają podobny wygląd, ale ich sens jest różny. By przestać mylić, wygodnie trzymać w głowie prostą myśl: jeden nagłówek mówi o tym, co chcesz dostać, a drugi — o tym, co wysyłasz.

Można nawet wyobrazić sobie dialog:

- Accept: „Umiem przyjąć JSON, proszę odpowiedz JSON-em”.
- Content-Type: „Wysyłam ci JSON. Nie myśl, że to plain text albo coś innego”.

I to działa zarówno dla żądań, jak i dla odpowiedzi. W odpowiedzi serwera Content-Type oznacza: „Oto w jakim formacie leży body odpowiedzi”.

Zbierzmy różnice w krótką tabelkę (realnie oszczędza czas na starcie):

Nagłówek Gdzie występuje O czym jest Przykład
Accept w żądaniu klienta jaki format odpowiedzi klient preferuje/potrafi Accept: application/json
Content-Type w żądaniu klienta w jakim formacie client wysyła body Content-Type: application/json
Content-Type w odpowiedzi serwera w jakim formacie server zwraca body Content-Type: application/json; charset=utf-8

Jeśli chcesz całkiem życiowej analogii, to Accept to jak fraza „poproszę kawę bez mleka”, a Content-Type to naklejka na pojemniku „w środku zupa”. Niby oba o jedzeniu, ale jeden o oczekiwaniu, drugi o faktycznej zawartości.

5. POST z body JSON: BodyPublishers.ofString

POST dziś używamy w celach edukacyjnych, bo w realnych katalogach książek wyszukiwanie często robione jest GET-em z parametrami query. Ale wysyłanie body JSON to na tyle częsta historia w integracjach backendowych, że nie da się przejść obok niej obojętnie. Nawet jeśli dziś nie będziesz realnie wysyłać POST do naszego zewnętrznego katalogu, mechanika musi stać się znajoma: metoda + headers + body.

W HttpRequest body zadaje się przez BodyPublisher. Najprostsza opcja to wysłać string, czyli BodyPublishers.ofString(...). I tu pojawiają się dwa praktyczne niuanse: string JSON trzeba zbudować bez błędów, i trzeba ustawić Content-Type: application/json.

W Javie 25 bardzo pomagają nam text blocks. Oszczędzają nerwy, bo nie trzeba escape'ować każdego cudzysłowu w JSON. Zamiast „cudzysłowowego piekła” piszemy prawie normalny JSON.

Przykład „edukacyjnego żądania”, które wysyła JSON na umowny endpoint /preview:

import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.nio.charset.StandardCharsets;

String baseUrl = "https://catalog.example";

// Ciało żądania: JSON wygodnie trzymać jako text block, żeby nie escape'ować cudzysłowów
String json = """
        {
          "query": "clean code",
          "limit": 5
        }
        """;

HttpRequest request = HttpRequest.newBuilder(URI.create(baseUrl + "/preview"))
        .header("Accept", "application/json")        // Chcemy dostać odpowiedź JSON
        .header("Content-Type", "application/json")  // Informujemy, że wysyłamy JSON
        .POST(BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // Ustalamy method + body + kodowanie
        .build();

Tu ważne, by zobaczyć „szkielet” żądania POST na własne oczy:

1) POST(...) robi żądanie właśnie POST-em i jednocześnie ustawia body.
2) Content-Type mówi serwerowi, że wewnątrz body jest JSON.
3) Accept mówi serwerowi, że chcemy dostać JSON w odpowiedzi.

Jeśli zapomnisz Content-Type, niektóre serwery będą próbowały interpretować body jako „nie wiadomo co” i dostaniesz błąd, który wygląda jak „serwer jest zepsuty”. Tak naprawdę po prostu nie zrozumiał, co mu wysłałeś.

Jeszcze drobiazg: wiele API oczekuje Content-Type: application/json; charset=utf-8. We współczesnym świecie JSON prawie zawsze jest w UTF‑8, i wiele serwerów rozumie to domyślnie. Ale jeśli widzisz dziwne problemy z cyrylicą lub znakami specjalnymi, warto wskazać charset jawnie. W naszych przykładach zostawimy application/json dla prostoty, a kodowanie zapewnimy przez ofString(...).

6. Powtarzające się nagłówki: mały helper

Kiedy robisz dwa-trzy żądania, wszystko wydaje się proste. Ale gdy tylko pojawia się „wyszukiwanie” i „szczegóły”, a potem jeszcze parę wariantów, nagle odkrywasz, że w każdym miejscu kopiujesz to samo: Accept, czasem User-Agent, czasem Content-Type. Kopiuj-wklej w kodzie transportowym jest niebezpieczny nie filozoficznie, a bardzo praktycznie: jedna linia gdzieś uciekła — i „z jakiegoś powodu jedno żądanie działa, a inne nie”.

Przy tym nie chcemy budować „uniwersalnego frameworka żądań”. Potrzebujemy małego, uczciwego helpera, by budowa żądania stała się krótsza, ale nie straciła czytelności.

Najprostszy wariant — metoda, która zwraca skonfigurowany HttpRequest.Builder dla odpowiedzi JSON:

import java.net.URI;
import java.net.http.HttpRequest;

private static HttpRequest.Builder jsonRequest(URI uri) {
    return HttpRequest.newBuilder(uri)
            .header("Accept", "application/json")          // Bazowa umowa: chcemy JSON
            .header("User-Agent", "readlater-starter/1.0");// Bazowa identyfikacja klienta
}

Wtedy GET staje się całkiem czytelny:

import java.net.URI;
import java.net.http.HttpRequest;

URI uri = URI.create("https://catalog.example/search?q=java");

HttpRequest request = jsonRequest(uri) // Bierzemy bazowe nagłówki (Accept + User-Agent)
        .GET()                         // Dodajemy metodę dla konkretnego żądania
        .build();

A POST — też:

import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.nio.charset.StandardCharsets;

// Krótki JSON też można trzymać jako text block, by nie kombinować z escape'owaniem
String json = """
        {
          "query": "java",
          "limit": 3
        }
        """;

HttpRequest request = jsonRequest(URI.create("https://catalog.example/preview"))
        .header("Content-Type", "application/json") // Dla POST z body to ważne: co dokładnie wysyłamy
        .POST(BodyPublishers.ofString(json, StandardCharsets.UTF_8)) // Body + kodowanie
        .build();

Zauważ, że nie próbujemy ukrywać „POST z body” gdzieś w głębinach. Po prostu usunęliśmy powtarzające się kawałki, a sens żądania nadal czyta się w 5–6 linijkach.

7. Typowe błędy przy budowie HttpRequest

Błąd nr 1: mylić Accept i Content-Type.
Najczęstsza historia wygląda tak: robisz POST, ustawiasz Accept: application/json, myślisz „no powiedziałem o JSON” i dziwisz się, dlaczego serwer narzeka na body. Serwerowi jest wszystko jedno, co chcesz dostać w odpowiedzi; jemu trzeba zrozumieć, co wysłałeś. Po to istnieje Content-Type.

Błąd nr 2: wysłać POST bez body (albo z pustym body) przypadkiem.
Czasem chce się napisać po prostu .POST(...), ale „co tam podać” nie wiadomo, i ręka sięga do czegoś typu BodyPublishers.noBody(). Formalnie to możliwe, ale w scenariuszach JSON zwykle bez sensu: serwer czekał na JSON, a ty wysłałeś „nic”. Jeśli scenariusz naprawdę o JSON, body musi być jawne.

Błąd nr 3: złożyć JSON stringiem z cudzysłowami i złamać go bez powodu.
String typu "{\"query\":\"clean code\"}" wygląda jak kara za grzechy z poprzedniego życia. Łatwo w nim zapomnieć o backslashu, zamknąć cudzysłów nie tam i dostać „nieprawidłowy JSON”. W projekcie edukacyjnym lepiej albo używać Java text block (""" ... """), albo trzymać JSON krótki i sprawdzalny.

Błąd nr 4: ustawić Content-Type na GET „na wszelki wypadek”.
Content-Type opisuje ciało żądania. W GET ciała nie ma, i nagłówek staje się dziwnym szumem. Czasem serwery są cierpliwe, ale czasem trafisz na API, które robi ścisłą walidację i zaczyna się zachowywać nieprzewidywalnie. „Na wszelki wypadek” w HTTP to częsta droga do niespodzianek.

Błąd nr 5: próbować reużyć jeden HttpRequest.Builder dla różnych żądań.
Builder wygodnie wygląda jak „obiekt-konfigurator” i pojawia się myśl: „stworzę jeden, potem będę zmieniać URI”. W praktyce szybko zapominasz, jakie nagłówki już dodałeś, i dostajesz losowy zestaw headers w różnych żądaniach. Twórz builder od nowa dla każdego żądania, a wspólne minimum wynoś do małego helpera (jak zrobiliśmy wyżej).

1
Zadanie
Java Server, poziom 15, lekcja 1
Niedostępne
Konfiguracja żądania GET bez wysyłania do sieci
Konfiguracja żądania GET bez wysyłania do sieci
1
Zadanie
Java Server, poziom 15, lekcja 1
Niedostępne
POST-żądanie z JSON-body
POST-żądanie z JSON-body
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION