HTTP dla backendowca — metody, statusy i nagłówki w .NET

Jako backendowiec .NET spędzisz karierę na obsłudze HTTP — ASP.NET Core ukrywa protokół za atrybutami i metodami, dopóki coś nie pójdzie nie tak. Klient dostaje 401 zamiast 403, przeglądarka blokuje żądanie przez CORS, POST wykonuje się dwa razy przy podwójnym kliknięciu. Każdy z tych problemów to nie bug we frameworku, tylko nieporozumienie z protokołem. Ten post daje Ci model HTTP, na którym stoi każde REST API, zanim napiszesz pierwsze API w ASP.NET Core.
HTTP w dwóch zdaniach
HTTP to tekstowy protokół żądanie–odpowiedź: klient wysyła request, serwer odsyła response i połączenie logicznie się kończy. Protokół jest bezstanowy — serwer nie pamięta poprzedniego żądania, więc cały kontekst (kim jesteś, co robisz) musi jechać w każdym żądaniu od nowa.
To jedna decyzja projektowa, z której wynika połowa Twojej pracy backendowej: skoro serwer nic nie pamięta, tożsamość użytkownika musisz przesyłać w każdym żądaniu (token w nagłówku Authorization), a stan aplikacji trzymać w bazie danych — nie w pamięci procesu.
HTTP jest bezstanowy. Jeśli Twój endpoint zakłada, że „użytkownik przed chwilą coś zrobił”, zakłada coś, czego protokół nie gwarantuje.
Anatomia żądania
Surowe żądanie HTTP wygląda tak:
POST /api/zamowienia HTTP/1.1
Host: api.sklep.pl
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
{"produktId": 42, "ilosc": 2}Cztery części: metoda (POST), ścieżka (/api/zamowienia), nagłówki (klucz–wartość) i opcjonalne body. W ASP.NET Core dokładnie te same dane odczytasz z obiektu HttpRequest:
app.MapPost("/api/zamowienia", (HttpRequest request) =>
{
string metoda = request.Method; // "POST"
string sciezka = request.Path; // "/api/zamowienia"
string? typ = request.Headers.ContentType; // "application/json"
string? auth = request.Headers.Authorization; // "Bearer eyJ..."
return Results.Ok();
});W praktyce rzadko czytasz HttpRequest ręcznie — framework mapuje body na obiekt C# za Ciebie. Ale gdy debugujesz „czemu to żądanie nie działa”, wracasz do tych czterech części.
Metody HTTP — i dlaczego semantyka jest zobowiązaniem
Metoda mówi, co robisz z zasobem. REST przypisuje im konkretne znaczenie, a klienci (przeglądarki, proxy, CDN) na tym znaczeniu polegają.
| Metoda | Zastosowanie | Bezpieczna? | Idempotentna? |
|---|---|---|---|
GET | Pobierz zasób | ✅ tak | ✅ tak |
POST | Utwórz nowy zasób | ❌ nie | ❌ nie |
PUT | Zastąp cały zasób | ❌ nie | ✅ tak |
PATCH | Zmień część zasobu | ❌ nie | ❌ nie |
DELETE | Usuń zasób | ❌ nie | ✅ tak |
Bezpieczna znaczy „nie zmienia stanu serwera” — GET można wołać dowolnie, także wielokrotnie z cache. Idempotentna znaczy „N identycznych wywołań daje ten sam efekt co jedno” — DELETE /zamowienia/42 wywołane trzy razy usuwa zamówienie 42 raz, a kolejne wywołania nie psują niczego.
POST jako jedyna operacja tworząca nie jest idempotentna — i to jest źródłem klasycznej pułapki, do której wrócimy niżej.
GETktóry zmienia dane (GET /usun?id=42) to nie skrót — to bomba. Crawler Google, prefetch przeglądarki albo antywirus skanujący linki wywoła go bez Twojej wiedzy.
Kody statusu — kontrakt odpowiedzi
Serwer odpowiada trzycyfrowym kodem, którego pierwsza cyfra to klasa:
| Zakres | Znaczenie | Najważniejsze |
|---|---|---|
2xx | Sukces | 200 OK, 201 Created, 204 No Content |
3xx | Przekierowanie | 301 Moved Permanently, 304 Not Modified |
4xx | Błąd klienta | 400, 401, 403, 404, 409, 422 |
5xx | Błąd serwera | 500 Internal Server Error, 503 |
W REST API dobór kodu to część kontraktu — klient podejmuje decyzje na jego podstawie. W ASP.NET Core zwracasz je przez Results:
app.MapGet("/api/produkty/{id}", (int id, Sklep sklep) =>
{
var produkt = sklep.Znajdz(id);
return produkt is null
? Results.NotFound() // 404 — nie ma takiego zasobu
: Results.Ok(produkt); // 200 + body
});
app.MapPost("/api/produkty", (Produkt nowy, Sklep sklep) =>
{
sklep.Dodaj(nowy);
return Results.Created($"/api/produkty/{nowy.Id}", nowy); // 201 + nagłówek Location
});Kilka reguł, które odróżniają poprawne API od „wszystko zwraca 200 i pole error“:
201 CreatedpoPOST, który utworzył zasób — z nagłówkiemLocationwskazującym, gdzie ten zasób teraz żyje.204 No ContentpoDELETEalboPUT, który się udał, ale nie ma czego zwracać.400 Bad Requestgdy żądanie jest źle sformułowane (brakuje pola, zły typ).422 Unprocessable Entitygdy składnia jest OK, ale dane łamią regułę biznesową (data w przeszłości, ujemna cena).409 Conflictgdy operacja koliduje ze stanem (próba utworzenia czegoś, co już istnieje).
Pułapka #1: 401 vs 403
Najczęstsze pomylenie w API — i pytanie rekrutacyjne:
401 Unauthorizedznaczy tak naprawdę *unauthenticated* — „nie wiem, kim jesteś”. Brak tokenu, token wygasły, zły podpis. Klient powinien się zalogować i spróbować ponownie.403 Forbiddenznaczy *authenticated, ale bez uprawnień* — „wiem, kim jesteś, i nie wolno Ci tego”. Ponowne logowanie nic nie da; użytkownik po prostu nie ma prawa do tego zasobu.
Zwrócenie 403 przy wygasłym tokenie zapętli klienta (nie odświeży sesji, bo dostał sygnał „to nie kwestia logowania”). Zwrócenie 401 przy braku uprawnień ujawni, że zasób istnieje, i wyśle użytkownika w kółko na ekran logowania.
401= nie wiem, kim jesteś.403= wiem i nie wolno Ci. Mylenie ich to nie kosmetyka — to zepsuty przepływ logowania.
Nagłówki, których używasz codziennie
Nagłówki to metadane żądania i odpowiedzi. Garść, bez której nie zbudujesz API:
| Nagłówek | Kierunek | Rola |
|---|---|---|
Content-Type | oba | Format body (application/json) |
Accept | żądanie | Format, jakiego klient oczekuje w odpowiedzi |
Authorization | żądanie | Token/poświadczenia (Bearer ...) |
Location | odpowiedź | Gdzie znaleźć nowo utworzony zasób (po 201) |
Cache-Control | oba | Czy i jak długo cache’ować |
Content-Type to częste źródło błędu 415 Unsupported Media Type: wysyłasz JSON, ale bez nagłówka Content-Type: application/json serwer nie wie, że ma go sparsować, i odrzuca żądanie, zanim dotrze do Twojego kodu.
Wysyłanie żądań z C# — HttpClient
Backendowiec nie tylko obsługuje żądania — często sam je wysyła (do innego serwisu, API płatności, zewnętrznego dostawcy). Służy do tego HttpClient:
var odpowiedz = await httpClient.GetAsync("https://api.nbp.pl/api/exchangerates/rates/A/EUR");
if (odpowiedz.IsSuccessStatusCode) // true dla każdego 2xx
{
var tresc = await odpowiedz.Content.ReadAsStringAsync();
Console.WriteLine(tresc);
}
else
{
Console.WriteLine($"Błąd: {(int)odpowiedz.StatusCode}"); // np. 404
}Pułapka #2: HttpClient tworzony w pętli
Kuszące jest napisać using var client = new HttpClient(); przy każdym żądaniu. To klasyczny błąd wyczerpujący gniazda (socket exhaustion): każdy HttpClient trzyma połączenie, które po Dispose() ląduje w stanie TIME_WAIT na kilkadziesiąt sekund. Przy ruchu produkcyjnym serwer wysyca pulę portów i nowe żądania zaczynają się wywalać.
Rozwiązanie w .NET to IHttpClientFactory — rejestrujesz go raz i wstrzykujesz przez wstrzykiwanie zależności (DI):
builder.Services.AddHttpClient();
// ...
public class KursyWalut(HttpClient httpClient) // wstrzyknięty, zarządzany pulą połączeń
{
public Task<HttpResponseMessage> Pobierz(string waluta) =>
httpClient.GetAsync($"https://api.nbp.pl/api/exchangerates/rates/A/{waluta}");
}Nie twórz
new HttpClient()na żądanie. ZarejestrujAddHttpClient()i wstrzykuj — pula połączeń jest problemem frameworka, nie Twoim.
Pułapka #3: podwójny POST
Skoro POST nie jest idempotentny, podwójne kliknięcie „Zamów” albo ponowienie po timeoucie potrafi utworzyć dwa zamówienia. Protokół Ci tu nie pomoże — musisz to obsłużyć sam: klucz idempotentności (unikalny identyfikator żądania, który serwer zapamiętuje), ograniczenie unikalności w bazie albo 409 Conflict przy duplikacie. To temat, który wraca w Clean Architecture w .NET 10 — REST API, gdy operacje zapisu trafiają za warstwę aplikacji.
Checklist — HTTP w API, które nie wstydzi się code review
GETniczego nie zmienia (bezpieczny i idempotentny)POSTzwraca201 Created+ nagłówekLocationDELETE/PUTudane bez treści →204 No Content400dla złej składni,422dla złamanej reguły biznesowej401gdy brak/zły token,403gdy brak uprawnieńContent-Type: application/jsonprzy wysyłaniu JSON-aHttpClientzIHttpClientFactory, nigdyneww pętli- Zapis (
POST) zabezpieczony przed podwójnym wykonaniem
Podsumowanie
HTTP to bezstanowy protokół żądanie–odpowiedź, w którym metoda deklaruje intencję (i zobowiązuje Cię do jej semantyki — bezpieczeństwa i idempotentności), a kod statusu jest kontraktem odpowiedzi, na którym polega klient. Najczęstsze pułapki backendowca nie biorą się z frameworka: mylenie 401 z 403, GET zmieniający dane, POST bez ochrony przed duplikatem i HttpClient tworzony w pętli. Gdy rozumiesz te cztery części żądania i klasy statusów, ASP.NET Core przestaje być magią, a staje się cienką warstwą nad protokołem, który już znasz.
Co dalej
Masz model HTTP — czas zbudować na nim serwer. W Pierwsze REST API w ASP.NET Core postawisz działający endpoint od zera, a w Minimal API i routing zobaczysz, jak ASP.NET Core dopasowuje ścieżkę żądania do Twojego kodu. Jeśli chcesz najpierw zrozumieć, na czym to wszystko działa, wróć do czym jest .NET (CLR, BCL).
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →