1. Algorytm analizy wywołania HTTP
Do tego miejsca widać już cztery różne źródła bólu: żądanie może być niesamowystarczalne, operacja może zmieniać zasób, odpowiedzi może nie być, a otrzymana odpowiedź jeszcze nie musi pokrywać się z umową. Teraz warto zebrać te kawałki w jedną roboczą kolejność analizy, żeby nie diagnozować HTTP intuicyjnie i nie naprawiać nie tego problemu. Narzędzie może później ukryć część boilerplate'u, ale przyczyny awarii i tak rozkładają się na te same pytania.
Gdy coś poszło nie tak, mózg początkującego zwykle wykonuje heroiczny skok w środek historii: „O! W body jest jakiś dziwny string”, „O! Przyleciało 404 — czyli serwer umarł”, „O! Wyjątek — czyli bug w logice biznesowej”. Problem polega na tym, że to nie analiza, lecz zgadywanie, po prostu z ładniejszymi terminami.
Backendowe myślenie lubi porządek, bo porządek oszczędza czas. Jeśli za każdym razem analizujesz problemy według jednego schematu, szybciej znajdujesz przyczynę, mniej narzekasz na dostawcę (czasem niesłusznie) i rzadziej „naprawiasz nie to”. To szczególnie ważne dla ReadLater Starter: gdy tylko aplikacja zaczyna chodzić do zewnętrznego katalogu książek, trzeba odróżniać „serwis odpowiedział błędem” od „nie dostaliśmy odpowiedzi” i od „odpowiedź przyszła, ale nie taka, jaką obiecywał kontrakt”.
W skrócie: algorytm jest potrzebny nie dlatego, że nie jesteście mądrzy, lecz dlatego, że jesteście żywymi ludźmi. A żywym ludziom przydają się listy kontrolne, inaczej zaczynają debugować po horoskopie.
Trzy pytania: odpowiedź, status, kontrakt
Warto trzymać w głowie trzy pytania, które trzeba zadawać ściśle w kolejności. Ta kolejność jest ważniejsza, niż się wydaje: chroni przed najczęstszym błędem — „najpierw poleźliśmy w body, a potem okazało się, że odpowiedzi w ogóle nie było”. Schemat prosty, prawie jak „sprawdź, czy komputer jest podłączony do gniazdka”, tylko w HTTP.
Pierwsze pytanie: czy odpowiedź w ogóle była? Jeśli odpowiedzi nie było, to z definicji nie możesz analizować status-code — go nie ma. Drugie pytanie: jeśli odpowiedź była, jaki status? Status to szybka etykieta sensu: sukces, błąd żądania, brak zasobu, konflikt, wewnętrzny błąd. Trzecie pytanie: jeśli status oznacza sukces, czy odpowiedź odpowiada kontraktowi? Bo 200 OK to jeszcze nie gwarancja, że przysłano ci to, o czym się umawialiście.
To można przedstawić jako bardzo proste drzewo decyzyjne:
flowchart TD
A["Problematyczne wywołanie HTTP"] --> B{Odpowiedź otrzymana?}
B -- Nie --> C["Awaria sieciowa / brak odpowiedzi"]
B -- Tak --> D{"Status code"}
D -- 4xx/5xx --> E["Otrzymano error response"]
D -- 2xx --> F{Kontrakt spełniony?}
F -- Nie --> G["Naruszenie kontraktu"]
F -- Tak --> H["Sukces"]
Zwróć uwagę na ważną rzecz psychologiczną: 404 znajduje się w gałęzi „odpowiedź otrzymana”. To znaczy, że serwer żyje, sieć dostarczyła żądanie i zwróciła response. To już użyteczna informacja i kardynalnie różni się od sytuacji „brak odpowiedzi w ogóle”.
2. Krok 1 — sprawdzenie obecności odpowiedzi
Analizę trzeba zaczynać od najbardziej nudnego, ale najbardziej zbawiennego pytania: „Czy odpowiedź była?”. Nudno — bo chce się od razu czytać body i szukać „co tam jest nie tak”. Zbawienne — bo czasem żaden HTTP-response nie istnieje, a ty próbujesz analizować widmo.
Jeśli odpowiedzi nie ma, dalej nie ma o czym dyskutować na poziomie statusów: nie masz ani 404, ani 500, ani nagłówków, ani ciała. Przyczyny mogą być różne — serwis niedostępny, niewłaściwy adres, sieć odpadła, połączenie nie nawiązało się — ale gałąź diagnostyki jest jedna: najpierw przyznajemy, że response nie został odebrany, i dopiero potem wyjaśniamy dlaczego.
Żeby zafiksować myśl w prostym przykładzie Javy, można wyobrazić sobie, że wynik wywołania to albo response, albo błąd. Teraz nie robimy prawdziwego kodu HTTP (będzie później), ale możemy zamodelować formę wyniku — to pożyteczne dla myślenia.
package com.example.readlater;
// Wynik wywołania: albo jest HTTP-odpowiedź, albo wystąpił błąd na drodze do niej (sieć, DNS, timeout itp.)
public record CallOutcome(RemoteResponse response, Exception error) {
// Najważniejsze w tym obiekcie: oddzielamy "brak odpowiedzi" od "odpowiedź z błędnym statusem".
public boolean hasResponse() {
// Jeśli response == null — znaczy HTTP-odpowiedzi nie było jako klasy (nie ma czego analizować po statusie/nagłówkach).
return response != null;
}
}
Jeśli hasResponse() zwrócił false, nie trzeba szukać „właściwego statusu” i tym bardziej analizować body: jesteś w innej gałęzi problemu. Tutaj wyjaśnia się, czy była awaria sieciowa, czy nie pomyliliśmy host/port/path i czy nie zderzyliśmy się z zewnętrzną infrastrukturą.
I tu warto pamiętać semantykę operacji. Dla odczytu brak odpowiedzi zwykle oznacza utracony wynik. Dla żądania modyfikującego bardziej nieprzyjemne jest co innego: klient już nie wie, czy po prostu nie doczekał odpowiedzi, czy serwer zdążył coś zmienić.
3. Krok 2 — status jako klasa problemu
Gdy upewniłeś się, że odpowiedź rzeczywiście została odebrana, najwyższy czas zrobić to, co wielu pomija: spojrzeć na status code i odebrać go jako „klasę sytuacji”. To nie tylko cyfra, lecz uzgodniony sygnał między klientem a serwerem: „mi się udało”, „masz problem w żądaniu”, „zasobu nie ma”, „konflikt”, „mam wewnętrzną awarię”.
Ważny moment: error response to też wynik kontraktu. Jeśli serwer odpowiedział 400 albo 404, to już „normalna forma komunikacji”: zdołał obsłużyć twoje żądanie na tyle, żeby zwrócić strukturyzowaną odpowiedź (choćby z błędem). To różni się od sytuacji, gdy odpowiedzi nie ma wcale.
Żeby „przyziemnić” to do małego kodu, potrzebujemy minimalnego obiektu odpowiedzi. Zrobimy rekord, gdzie jest status, content-type i body. Tak, body na razie stringiem: dla tej diagnostyki ważniejsze jest dla nas odróżnienie statusu i formy odpowiedzi niż rozkładanie JSON-a po polach.
package com.example.readlater;
// Minimalny model HTTP-odpowiedzi do diagnostyki.
// Tutaj ważne są: status, Content-Type i ciało (na razie stringiem, bez parsowania JSON).
public record RemoteResponse(int status, String contentType, String body) {
// 2xx — klasa "sukces": serwer uważa, że żądanie zostało obsłużone.
public boolean is2xx() {
return status >= 200 && status < 300;
}
}
Teraz można wyodrębnić bardzo proste sprawdzenie: „status to błąd czy nie”.
package com.example.readlater;
public class Statuses {
// 4xx/5xx — to error response: odpowiedź jest, ale serwer informuje o błędzie.
public static boolean isError(int status) {
return status >= 400 && status < 600;
}
}
Tu wystarczy utrzymać jedną podporę: 404 i 500 już należą do gałęzi „response otrzymany”. To różne problemy, ale obydwa różnią się od sytuacji, gdzie statusu w ogóle nie ma.
Żeby łatwiej było to trzymać w głowie, oto niewielka tabela-interpretator (bez zamiany wykładu w podręcznik RFC):
| Co obserwujesz | To… | Czego NIE warto robić w pierwszej kolejności |
|---|---|---|
| Brak response (jest wyjątek/zerwanie) | Awaria sieciowa / niedostępność | Szukać „właściwego statusu” i analizować body |
| Response ze statusem 4xx | Błąd żądania / problem klienta | Ogłaszać „serwis padł” (on właśnie odpowiedział) |
| Response ze statusem 5xx | Błąd po stronie serwera | Uważać, że „na pewno wszystko dobrze wysłałeś” (czasem to ty sprowokowałeś problem) |
| Response ze statusem 2xx | Pomyślne dostarczenie odpowiedzi | Uważać, że kontrakt na pewno spełniony (to następny krok sprawdzenia) |
W kontekście projektu ReadLater Starter będzie to szczególnie użyteczne, gdy zaczniemy chodzić do zewnętrznego katalogu książek. Zewnętrzny dostawca może uczciwie odpowiedzieć 404 na niewłaściwą ścieżkę (np. zrobiłeś literówkę w URL), może odpowiedzieć 400 na nieprawidłowy parametr, może odpowiedzieć 500, bo ma problemy. I wszystkie te trzy przypadki to odpowiedzi, a nie „brak odpowiedzi”.
4. Krok 3 — sprawdzenie kontraktu
I tu wszystko nareszcie się składa: po sprawdzeniu response i status pozostaje sprawdzić, czy otrzymany „sukces” naprawdę przypomina to, o czym się umawialiśmy.
Teraz nadejdzie moment, który często łamie romantyczne wyobrażenie o 200 OK: czasem serwer odpowiada 200, ale robi to „jakoś nie tak”. I to już nie po prostu „błąd żądania” i nie „wewnętrzny błąd” — to często znak, że kontrakt został naruszony (albo źle go zrozumieliśmy).
Kontrakt to nie tylko „pola w JSON”. To jeszcze Content-Type, i oczekiwana forma odpowiedzi, i dopuszczalne statusy dla tego endpointu. Na przykład oczekujesz JSON-a, a przychodzi ci strona HTML z tekstem „Service temporarily unavailable” — i tak, czasem takie przylatuje ze statusem 200. Albo przychodzi puste body tam, gdzie oczekiwałeś obiektu. Albo nagłówki są takie, że klient nie może zrozumieć, jak to zinterpretować.
Pokażmy to na bardzo przyziemnym sprawdzeniu: uważamy, że „kontrakt przypomina JSON”, jeśli Content-Type — application/json, a w body jest przynajmniej oczekiwany marker. To nie „właściwa walidacja”, lecz szkoleniowa ilustracja idei: nawet sukces trzeba zestawić z oczekiwaniem.
package com.example.readlater;
public class ContractChecks {
// Bardzo szkoleniowe sprawdzenie: czy przypomina to odpowiedź JSON, której oczekujemy.
// To NIE jest "prawidłowy" parser JSON. Tu ważna jest idea: nawet przy 200 kontrakt może być złamany.
public static boolean looksLikeJson(RemoteResponse r) {
return "application/json".equals(r.contentType())
&& r.body() != null
// Zgrubny marker: w prawdziwym projekcie parsowałbyś JSON, a nie szukał "{".
&& r.body().contains("{");
}
}
Wyobraź sobie teraz dwie odpowiedzi.
Pierwsza — przypomina to, czego chcemy:
// "Normalna" odpowiedź: JSON i oczekiwana struktura.
RemoteResponse ok = new RemoteResponse(
200,
"application/json",
"""
{"items":[],"count":0}
"""
);
Druga — „pomyślna”, ale dziwna:
// "Dziwna" odpowiedź: status 200, ale Content-Type i ciało nie o JSON API.
RemoteResponse weird = new RemoteResponse(
200,
"text/html",
"""
<html><body>Oops</body></html>
"""
);
Obie formalnie 200, ale druga narusza oczekiwanie: klient myślał, że to JSON API, a przysłano mu HTML. To typowa sytuacja, gdy serwis stoi za proxy/stroną błędu, albo gdy trafiłeś nie tam (np. na „ludzką” stronę zamiast API). I twoja analiza musi umieć powiedzieć: „Status pomyślny, ale kontrakt nie pasuje”. To osobna kategoria i jest bardzo pożyteczna, bo pomaga nie tracić czasu na „dlaczego JSON się nie parsuje”, gdy w rzeczywistości JSON-a tam nawet nie nocowało.
Na razie wystarczy nam zobaczyć sam fakt niezgodności formy. Nawet gdy ciało odpowiedzi stanie się dla nas już nie po prostu stringiem, lecz zestawem konkretnych pól, kolejność myśli pozostanie ta sama: najpierw response, potem status, potem zgodność z umową.
Jeszcze jeden ważny moment: kontrakt może być naruszany nie tylko formą body, ale i statusem. Na przykład oczekiwałeś, że GET /books/search zawsze zwraca 200 (nawet jeśli nic nie znaleziono — pusta lista), a serwis nagle zaczął oddawać 404. Z punktu widzenia HTTP to „możliwe zachowanie”, ale z punktu widzenia waszej umowy — to zmiana reguł gry. I to trzeba widzieć właśnie jako „kontrakt pojechał”, a nie jako „no, znowu 404, czyli wszystko stracone”.
5. Mini-diagnostyka w kodzie
Teraz zrobimy mały, ale bardzo wymowny krok: zamienimy nasz schemat w kod, który klasyfikuje rezultat problematycznego wywołania. To nie „gotowa biblioteka”, nie „framework”, ani nawet nie prawdziwy klient HTTP — to szkoleniowy szkielet myślenia, który później (gdy pojawi się prawdziwe wywołanie) po prostu wypełnisz prawdziwymi danymi.
Zacznijmy od wyliczenia możliwych rezultatów. Wystarczą nam cztery:
package com.example.readlater;
public enum CallVerdict {
// HTTP-odpowiedzi nie ma (połączenie nie nawiązało się, timeout, DNS, zerwanie itp.)
NETWORK_FAILURE,
// HTTP-odpowiedź jest, ale status 4xx/5xx (serwer zwrócił błąd protokołowo).
HTTP_ERROR_RESPONSE,
// HTTP-odpowiedź "nie przypomina" tego, o czym się umawialiśmy (statusy/typy/ciało).
CONTRACT_VIOLATION,
// Wszystko OK: jest odpowiedź, status sukcesu, kontrakt się zgadza.
SUCCESS
}
Teraz złóżmy analizator. Będzie stosował dokładnie tę kolejność, którą omawialiśmy: najpierw błąd/brak odpowiedzi, potem status, potem kontrakt.
package com.example.readlater;
public class CallAnalyzer {
public CallVerdict analyze(CallOutcome outcome) {
// 1) Najważniejsze pierwsze pytanie: czy odpowiedź w ogóle była?
if (!outcome.hasResponse()) return CallVerdict.NETWORK_FAILURE;
RemoteResponse r = outcome.response();
// 2) Jeśli status 4xx/5xx — to "error response": serwer odpowiedział, ale poinformował o błędzie.
if (Statuses.isError(r.status())) return CallVerdict.HTTP_ERROR_RESPONSE;
// 3) Wszystko, co nie trafiło w 2xx i 4xx/5xx, dla tego szkoleniowego schematu uważamy za gałąź unexpected-status.
// Tutaj świadomie nie rozkładamy historii redirect i innych przypadków specjalnych.
if (!r.is2xx()) return CallVerdict.CONTRACT_VIOLATION;
// 4) Nawet przy 2xx sprawdzamy, czy odpowiedź odpowiada oczekiwaniom (kontraktowi).
if (!ContractChecks.looksLikeJson(r)) return CallVerdict.CONTRACT_VIOLATION;
return CallVerdict.SUCCESS;
}
}
Jest tu zamierzone uproszczenie: wszystko, co nie trafiło w oczekiwaną gałąź sukcesu lub jawnego błędu, tymczasowo uważamy za unexpected dla bieżącego kontraktu. Teraz ważna jest sama kolejność diagnostyki, bez osobnego rozkładania wszystkich specjalnych przypadków protokołu.
Zwróć uwagę na zamierzoną prostotę. Nie robimy „ładnej” hierarchii wyjątków, nie budujemy uniwersalnych abstrakcji, nie dodajemy retry. Naszym celem jest, byś utrwalił w głowie kolejność pytań. Ten kod można czytać prawie jak polski tekst: „jeśli odpowiedzi nie ma — awaria sieciowa; jeśli status jest błędny — error response; jeśli status jest dziwny — kontrakt; jeśli forma odpowiedzi nie ta — kontrakt; w przeciwnym razie sukces”.
Teraz można pokazać, jak to wygląda w mini-demo. Tak, to będzie „zabawka”, gdzie ręcznie tworzymy outcome, ale świetnie ilustruje klasyfikację.
package com.example.readlater;
public class ReadLaterApplication {
public static void main(String[] args) {
CallAnalyzer analyzer = new CallAnalyzer();
// Imitujemy pomyślną odpowiedź: odpowiedź jest, 200, ciało podobne do JSON.
CallOutcome ok = new CallOutcome(
new RemoteResponse(200, "application/json", """
{"items":[],"count":0}
"""),
null
);
System.out.println(analyzer.analyze(ok)); // SUCCESS
}
}
I jeszcze jeden przykład — 404 (odpowiedź jest, ale status błędny):
package com.example.readlater;
public class Demo404 {
public static void main(String[] args) {
// 404 — to NIE "brak odpowiedzi", to "odpowiedź jest, ale serwer informuje o błędzie żądania/zasobu".
CallVerdict v = new CallAnalyzer().analyze(
new CallOutcome(
new RemoteResponse(404, "application/json", """
{"message":"not found"}
"""),
null
)
);
System.out.println(v); // HTTP_ERROR_RESPONSE
}
}
I przykład „brak odpowiedzi w ogóle”:
package com.example.readlater;
public class DemoNetworkFailure {
public static void main(String[] args) {
// response == null: HTTP-odpowiedzi nie ma, jest tylko wyjątek po stronie klienta/sieci.
CallOutcome out = new CallOutcome(null, new IllegalStateException("Connection refused"));
System.out.println(new CallAnalyzer().analyze(out)); // NETWORK_FAILURE
}
}
Z punktu widzenia rozwoju ReadLater Starter to pożyteczne, bo później pojawi się prawdziwy „zewnętrzny katalog książek”. I gdy będziesz debugował wywołanie „wyszukaj książkę”, przyda ci się prosty nawyk: „Co to? Brak odpowiedzi? Jest odpowiedź ze statusem? Sukces, ale kontrakt zepsuty?”. To znacznie lepsze niż „coś jest nie tak, drukujmy wszystko po kolei”.
6. Typowe błędy przy analizie wywołań
Błąd nr 1: analiza zaczyna się od body.
Najpopularniejsza pułapka: od razu czytasz body, próbujesz zrozumieć „dlaczego tam nie to”, a potem okazuje się, że body to w ogóle nie to, co myślisz, albo go nie ma. Właściwa kolejność jest twarda: najpierw wyjaśniamy, czy istnieje response, potem status, i dopiero potem ciało.
Błąd nr 2: 404 jest traktowany jako „serwis niedostępny”.
404 to odpowiedź żywego serwisu. Tak, nie podoba ci się, ale oznacza, że sieć i serwer zadziałały na tyle dobrze, żeby zwrócić strukturyzowany sygnał. Jeśli nazywasz to „niedostępnością”, automatycznie zaczniesz szukać problemu „w infrastrukturze”, choć często to banalnie niewłaściwy path albo nieistniejący zasób.
Błąd nr 3: każdy wyjątek uważa się za 500 po stronie serwera.
Wyjątek po stronie klienta (lub w twojej aplikacji) bardzo często oznacza „brak odpowiedzi”, a nie „serwer zwrócił 500”. 500 ma konkretną cechę: dostałeś response i zobaczyłeś status. Jeśli response'a nie ma — to inna klasa problemów i trzeba ją oddzielać.
Błąd nr 4: 200 OK automatycznie oznacza „wszystko OK”.
200 oznacza, że serwer powiedział „sukces”, ale nie gwarantuje, że ty i serwer tak samo rozumiecie format odpowiedzi. Jeśli Content-Type nie ten, jeśli body jest puste, jeśli forma „pojechała”, to może być problem kontraktowy. I jest on szczególnie nieprzyjemny, bo wygląda jak sukces, ale łamie klienta.
Błąd nr 5: jednakowo łatwo powtarza się żądania odczytu i modyfikacji danych.
Jeśli masz problemy z siecią, powtórzyć GET psychologicznie jest prościej: odczyt nie powinien zmieniać stanu. A powtarzanie żądań „modyfikujących” (utworzenie/aktualizacja/usunięcie) trzeba traktować ostrożniej, bo przy awarii sieciowej nie zawsze wiesz, co serwer zdążył zrobić. Dziś nie budujemy systemu powtórzeń, ale właściwa ostrożność powinna pojawić się już teraz.
GO TO FULL VERSION