CodeGym /Kursy /Go SELF /Podstawy HTTP — metody, kody statusu, idempotentność

Podstawy HTTP — metody, kody statusu, idempotentność

Go SELF
Poziom 56, Lekcja 0
Dostępny

1. HTTP jako kontrakt: żądanie → odpowiedź

Kiedy ludzie po raz pierwszy słyszą „HTTP”, często myślą: „no tak, to przecież o stronach internetowych”. I tak, strony też. Ale dla programisty HTTP to przede wszystkim bardzo ścisły sposób porozumienia między dwoma programami: jeden wysyła żądanie (client), drugi odpowiada (server). Ta umowa nazywa się kontraktem i obejmuje nie tylko „jaki URL wywołać”, ale też „jaka metoda”, „jakie kody sukcesu/błędów”, „jaki format ciała”, „co uważa się za poprawne dane wejściowe”.

W Go jest to szczególnie przyjemne: Go historycznie jest bardzo mocnym językiem do serwerów i kodu sieciowego. Nawet w materiałach o ewolucji Go podkreśla się, że Go projektowano do pisania serwerów.

Mentalny model „koperty”

Wyobraź sobie, że HTTP to poczta.

  • List = żądanie (request).
  • Koperta = metadane (metoda, ścieżka, nagłówki).
  • Treść listu = ciało żądania (na przykład JSON).
  • List zwrotny = odpowiedź (response).
  • Znaczek na odpowiedzi = kod statusu (200, 404, 500…), który krótko i jednoznacznie mówi „co się wydarzyło”.

Brzmi trochę staroświecko, ale jako model mentalny działa idealnie: serwer nie „zgaduje po nastroju”, tylko odpowiada formalnie.

Schemat żądania i odpowiedzi

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: HTTP Request (method + path + headers + body)
    S->>S: validate + compute
    S-->>C: HTTP Response (status + headers + body)

Najważniejsza myśl z tego schematu: na wyjściu zawsze jest kod statusu, a ciało odpowiedzi może być, ale nie musi. I klient powinien umieć żyć według kodu statusu, a nie próbować „parsować tekst błędu wzrokiem”.

Endpoint: metoda + ścieżka

Żeby przestać mówić o HTTP jak o magii, wygodnie jest wprowadzić mały termin: endpoint — to konkretne „miejsce wejścia” do API. I składa się on z dwóch części: metody HTTP (co robimy) i ścieżki (z czym pracujemy).

Na przykład, „pobierz listę zadań” często wygląda jak GET /api/v1/tasks.

Teraz nie uruchamiamy serwera i nie piszemy handlerów. Robimy to, co robią dojrzali inżynierowie na początku projektu: ustalamy kontrakt. I do tego można nawet napisać parę struktur w Go — nie dlatego, że „trzeba kodu”, ale po to, żeby myśli stały się precyzyjniejsze.

Mini-typ do opisu endpointu

package main

type Endpoint struct {
	Method string
	Path   string
}

var listTasks = Endpoint{Method: "GET", Path: "/api/v1/tasks"}
var createTask = Endpoint{Method: "POST", Path: "/api/v1/tasks"}

Tu ważna jest nie „ładność kodu”, lecz dyscyplina myślenia: endpoint to dokładnie para (method, path). Jeśli zmieniasz metodę, zmieniasz sens. Jeśli zmieniasz ścieżkę, zmieniasz zasób.

2. Metody HTTP: sens działania i obietnice dla klienta

O metodach wygodnie myśleć jak o czasownikach z bardzo ścisłymi zasadami. Jeśli używasz metod „jak popadnie”, klient musi zgadywać zachowanie, a API zaczyna przypominać quest „zgadnij przycisk”. Jeśli natomiast trzymasz się standardowej semantyki, klient (i inni programiści) rozumieją zachowanie bez czytania źródeł serwera. Oszczędza to godziny, nerwy i liczbę wiadomości „a dlaczego tu jest 200, skoro to błąd?”.

Poniżej — praktyczna tabela, którą naprawdę trzyma się w głowie podczas projektowania API.

Tabela podstawowej semantyki metod

Metoda Pomysł (po ludzku) Zwykle o czym Idempotentność (idea)
GET
„Daj dane” odczyt tak
POST
„Utwórz / wykonaj działanie” tworzenie/komenda zwykle nie
PUT
„Zastąp całość” pełna podmiana zasobu tak
PATCH
„Zmień częściowo” częściowa zmiana zwykle tak (jeśli zaprojektowane ostrożnie)
DELETE
„Usuń” usuwanie tak

Kilka komentarzy, żeby nie wyglądało to jak sucha teoria.

GET prawie zawsze jest bezpieczny w tym sensie, że nie powinien zmieniać stanu na serwerze. Jeśli GET nagle tworzy zadanie, pobiera pieniądze i wysyła list do mamy, to nie jest GET, tylko scenariusz do horroru.

POST często używa się do tworzenia: „utwórz zadanie”, „utwórz użytkownika”. Ale czasem POST to „komenda”, która nie mieści się w „zastąp stan”. Na przykład: „uruchom przeliczenie raportu”. W naszym szkolnym domenie zadań będziemy starać się traktować POST jako „utwórz zadanie”, bo tak jest prościej.

PUT i PATCH: jeśli nie jesteś pewien, nie śpiesz się z ich użyciem. Ale rozumieć sens trzeba: PUT to „oto nowa wersja zasobu w całości”, PATCH to „oto mała zmiana”.

DELETE — usuń. I tu zaczyna się filozofia: co znaczy „usunąć”, jeśli zasób już został usunięty? Zwykle umowę robi się tak, aby powtórzenie DELETE nie psuło systemu.

3. Kody statusu: język odpowiedzi serwera bez zgadywania z tekstu

Jeśli metoda to „czego chcemy”, to kod statusu to „co się udało”. I to nie jest drugorzędny szczegół, tylko część kontraktu: klient pisze swoją logikę, kierując się kodami. W dobrym API klient nie musi „czytać ciągu błędu” i zgadywać, co miałeś na myśli. Widzi 404 i rozumie „nie znaleziono”. Widzi 400 i rozumie „coś wysłałem nie tak”.

Jest tu bardzo wygodny model mentalny: klasy kodów.

  • 2xx — sukces,
  • 4xx — problem w żądaniu (wina klienta albo przynajmniej klient może to naprawić),
  • 5xx — problem po stronie serwera (klient nie jest winny).

Mała funkcja „klasa kodu”

package main

func statusClass(code int) int {
	return code / 100
}

// 200 -> 2
// 404 -> 4
// 500 -> 5

Tak, to szkolna matematyka. Ale pomaga: widzisz statusClass == 4 — to znaczy „błąd żądania”. Widzisz statusClass == 5 — „coś się u nas zepsuło”.

Minimalny zestaw kodów, który trzeba rozumieć pewnie

W ramach naszego przyszłego API zadań wystarczy nam mała „podstawowa piątka” plus jeden:

Kod Nazwa Kiedy zwykle się używa Czy jest ciało odpowiedzi
200
OK udany odczyt/zmiana zwykle tak
201
Created zasób utworzony zwykle tak (często zwracany jest utworzony obiekt)
204
No Content sukces bez ciała nie (i to jest ważne!)
400
Bad Request nieprawidłowe dane wejściowe/format/walidacja zwykle tak (błąd w JSON)
404
Not Found zasób nie został znaleziony zwykle tak
500
Internal Server Error wewnętrzny błąd serwera zwykle tak (ale komunikat jest stabilny i bezpieczny)

Kluczowy niuans dotyczący 204: to nie jest „200, ale bez nastroju”. To jasny sygnał: „sukces, ciała nie ma”. Jeśli zwrócisz 204 i jednocześnie wyślesz JSON, niektórzy klienci po prostu go zignorują, a niektórzy zaczną zachowywać się nieprzewidywalnie. Czyli sam sobie zrobisz błąd, a potem będziesz go bohatersko naprawiać.

Szybki helper „czy to sukces?”

package main

func is2xx(code int) bool {
	return code >= 200 && code <= 299
}

4. Idempotentność: powtórzyłeś żądanie — świat nie stał się gorszy

Słowo „idempotentność” brzmi tak, jakby wymyślono je po to, żeby straszyć studentów na kolokwiach. W praktyce idea jest bardzo prosta: jeśli powtórzysz żądanie, efekt końcowy nie powinien się zmienić. To nie znaczy, że „nic się nie zmienia”. To znaczy: „powtórzenie nie doda zbędnego efektu”.

Przykład z życia: przycisk „usuń zadanie”. Jeśli użytkownik kliknął go dwa razy (albo sieć się zacięła i klient powtórzył żądanie), zadanie powinno pozostać usunięte, a nie „usunąć się dwa razy” (cokolwiek by to miało znaczyć).

Idempotentność jest ważna przez realny świat: sieć jest niestabilna, połączenia się zrywają, klient może nie dostać odpowiedzi i uznać, że trzeba powtórzyć żądanie. Jeśli projektujesz API tak, by powtórzenie było bezpieczne, zmniejszasz liczbę dziwnych duplikatów i „widmowych” stanów.

Surowa reguła jako pierwsze przybliżenie

Często przyjmuje się tak: uznaje się, że GET/PUT/PATCH/DELETE powinny być idempotentne, a POST — nie (bo „utworzyć” dwa razy = dwa zasoby). To nie jest absolutna prawda, ale dobra reguła startowa.

package main

func isIdempotent(method string) bool {
	switch method {
	case "GET", "PUT", "PATCH", "DELETE":
		return true
	default:
		return false
	}
}

Mała demonstracja, żeby to „poczuć”

package main

import "fmt"

func main() {
	fmt.Println(isIdempotent("GET"))    // true
	fmt.Println(isIdempotent("POST"))   // false
	fmt.Println(isIdempotent("DELETE")) // true
}

To oczywiście nie jest „prawdziwe bezpieczeństwo API”. Ale to dobry sposób, żeby przestać mylić idempotentność z „nic nie zmienia”.

5. Wiążemy to z domeną: szkic kontraktu HTTP dla zadań

Teraz zrobimy ważny krok: przełożymy abstrakcyjne słowa HTTP na konkretny słownik naszej aplikacji. Od dawna żyjemy w domenie zadań (tasks): dodajemy zadanie, oglądamy listę, oznaczamy jako wykonane. W stylu HTTP chcemy, aby ścieżka opisywała byt, a metoda opisywała działanie.

Uzgodnijmy, że API wersjonujemy przez ścieżkę. Wygląda to tak: /api/v1/.... Wersja w ścieżce to sposób powiedzenia: „kontrakt został ustalony, a jeśli go złamiemy, zrobimy to w innej wersji, a nie nagle w piątek wieczorem”.

Tabela endpointów jako szkic kontraktu

Znaczenie operacji Metoda Ścieżka Sukces
Lista zadań
GET
/api/v1/tasks
200
Utwórz zadanie
POST
/api/v1/tasks
201
Pobierz zadanie
GET
/api/v1/tasks/{id}
200
Oznacz jako „done”
PATCH
/api/v1/tasks/{id}/done
200 (albo 204, jeśli zdecydujemy się bez ciała)
Usuń zadanie
DELETE
/api/v1/tasks/{id}
204

Zwróć uwagę: na razie nie omawiamy formatów JSON ani jednego formatu błędów (to też jest część kontraktu, ale to osobny temat). Tutaj ćwiczymy „szkielet”: metoda, ścieżka, kod sukcesu — żeby nie było u nas POST /getTask i GET /deleteTask. Piszemy API tak, żeby dało się je czytać jak angielski, no może jak bardzo dziwny angielski.

Trochę kodu, żeby utrwalić metody i nie pisać ciągów ręcznie

Ręczne ciągi "GET" i "POST" działają, ale łatwo o pomyłkę. Dlatego nawet na wczesnym etapie warto wprowadzić stałe.

package main

type Method string

const (
	MethodGet    Method = "GET"
	MethodPost   Method = "POST"
	MethodPut    Method = "PUT"
	MethodPatch  Method = "PATCH"
	MethodDelete Method = "DELETE"
)

A teraz endpointy można już opisywać „typowo”:

package main

type Endpoint struct {
	Method Method
	Path   string
}

var listTasks = Endpoint{Method: MethodGet, Path: "/api/v1/tasks"}
var deleteTask = Endpoint{Method: MethodDelete, Path: "/api/v1/tasks/{id}"}

6. Typowe błędy przy projektowaniu HTTP API

Błąd nr 1: zmienianie stanu przez GET.
Zwykle dzieje się to „dla wygody”: „no bo ja tylko otworzę przez link...”. Ale GET to odczyt. Jeśli GET zmienia stan, psujesz cache, psujesz powtarzane żądania i zaskakujesz wszystkich klientów. W pewnym momencie trafisz na błąd „u nas zadanie tworzy się samo” — i nie będzie to magia, tylko GET.

Błąd nr 2: zawsze odpowiadanie 200 i ukrywanie błędów w tekście.
Czasem robi się tak: status 200, a wewnątrz JSON pole "error": "...". Dla klienta to koszmar: musi parsować ciało, żeby zrozumieć, czy to sukces, czy nie. Lepiej, żeby sukces był 2xx, błąd żądania był 4xx, a wewnętrzny błąd serwera był 5xx. Ciało błędu może być strukturalne, ale kod jest najważniejszy.

Błąd nr 3: mylenie 400 i 404.
Jeśli klient wysłał id = "abc" i nie możesz odczytać go jako liczby — to nie jest „nie znaleziono”, tylko „nieprawidłowe żądanie”, czyli 400. A 404 to sytuacja, gdy id jest poprawne, ale takiego zasobu nie ma. To rozróżnienie bardzo ułatwia życie klientom i testom.

Błąd nr 4: zwracanie ciała odpowiedzi przy 204.
204 oznacza „sukces, ale bez ciała”. Jeśli mimo to wyślesz JSON, część klientów go zignoruje. Powstanie API, które „czasem działa”. A „czasem działa” to szczególny rodzaj bólu, bo nie ma czego debugować: przecież ono „czasem”.

Błąd nr 5: brak myślenia o idempotentności i powtórzeniach.
W realnym świecie żądania się powtarzają. Jeśli projektujesz operacje tak, że powtórzenie DELETE nagle zamienia się w 500, albo powtórzenie PATCH tworzy duplikujące się zmiany, klienci zaczną bać się retry, a ty będziesz łapać „dziwne” stany. Idempotentność to nie ozdoba, tylko zabezpieczenie przed sieciową rzeczywistością.

1
Zadanie
Go SELF, poziom 56, lekcja 0
Niedostępne
Klasa odpowiedzi
Klasa odpowiedzi
1
Zadanie
Go SELF, poziom 56, lekcja 0
Niedostępne
Metoda idempotentna
Metoda idempotentna
1
Zadanie
Go SELF, poziom 56, lekcja 0
Niedostępne
Szkic kontraktu
Szkic kontraktu
1
Zadanie
Go SELF, poziom 56, lekcja 0
Niedostępne
Symulator kontraktu
Symulator kontraktu
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION