🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

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

HTTP dla backendowca — żądanie POST i odpowiedź 201 Created z klasami kodów statusu w ASP.NET Core

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ą.

MetodaZastosowanieBezpieczna?Idempotentna?
GETPobierz zasób✅ tak✅ tak
POSTUtwórz nowy zasób❌ nie❌ nie
PUTZastąp cały zasób❌ nie✅ tak
PATCHZmień część zasobu❌ nie❌ nie
DELETEUsuń 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.

GET któ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:

ZakresZnaczenieNajważniejsze
2xxSukces200 OK, 201 Created, 204 No Content
3xxPrzekierowanie301 Moved Permanently, 304 Not Modified
4xxBłąd klienta400, 401, 403, 404, 409, 422
5xxBłąd serwera500 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 Created po POST, który utworzył zasób — z nagłówkiem Location wskazującym, gdzie ten zasób teraz żyje.
  • 204 No Content po DELETE albo PUT, który się udał, ale nie ma czego zwracać.
  • 400 Bad Request gdy żądanie jest źle sformułowane (brakuje pola, zły typ). 422 Unprocessable Entity gdy składnia jest OK, ale dane łamią regułę biznesową (data w przeszłości, ujemna cena).
  • 409 Conflict gdy 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 Unauthorized znaczy tak naprawdę *unauthenticated* — „nie wiem, kim jesteś”. Brak tokenu, token wygasły, zły podpis. Klient powinien się zalogować i spróbować ponownie.
  • 403 Forbidden znaczy *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łówekKierunekRola
Content-TypeobaFormat body (application/json)
AcceptżądanieFormat, jakiego klient oczekuje w odpowiedzi
AuthorizationżądanieToken/poświadczenia (Bearer ...)
LocationodpowiedźGdzie znaleźć nowo utworzony zasób (po 201)
Cache-ControlobaCzy 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. Zarejestruj AddHttpClient() 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

  • GET niczego nie zmienia (bezpieczny i idempotentny)
  • POST zwraca 201 Created + nagłówek Location
  • DELETE/PUT udane bez treści → 204 No Content
  • 400 dla złej składni, 422 dla złamanej reguły biznesowej
  • 401 gdy brak/zły token, 403 gdy brak uprawnień
  • Content-Type: application/json przy wysyłaniu JSON-a
  • HttpClient z IHttpClientFactory, nigdy new w 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).

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

Dodaj komentarz

czytanie to początek

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ę →