Idempotentność REST i bezpieczny retry w .NET

Przemysław Kwiatkowski 25 czerwca 2026
Klucze wpadają do tarczy, symbolizując bezpieczne, **rest idempotent** operacje. Po przejściu przez ochronę, dane są zatwierdzane.

Spis treści

Żądanie do API może zakończyć się błędem sieci, mimo że serwer zdążył już zapisać zmianę. Gdy klient spróbuje ponownie, właśnie wtedy decyduje się, czy system pozostanie spójny, czy utworzy duplikat zamówienia, płatności albo wiadomości. W tym artykule wyjaśniam, czym jest rest idempotent, jak działają idempotentne metody HTTP oraz jak bezpiecznie wdrożyć ten mechanizm w aplikacjach .NET i Azure.

Idempotentne API daje bezpieczniejszą obsługę ponowień

  • Idempotentność oznacza, że wielokrotne wykonanie tej samej operacji kończy się takim samym stanem jak pojedyncze wykonanie.
  • GET, HEAD, PUT i DELETE są z założenia idempotentne, ale nie oznacza to, że wszystkie są bezpieczne.
  • POST i PATCH nie są automatycznie idempotentne, dlatego wymagają świadomego projektu.
  • Idempotency-Key, unikalny identyfikator operacji i ograniczenie w bazie chronią przed duplikatami.
  • Idempotentność nie gwarantuje, że odpowiedź HTTP będzie identyczna przy każdym wywołaniu.

Co naprawdę oznacza idempotentność w REST

Operacja jest idempotentna, gdy jej wielokrotne wykonanie nie powoduje kolejnych zmian poza efektem pierwszego poprawnego wywołania. Najprostszy przykład to ustawienie adresu użytkownika na konkretną wartość. Wysłanie tej samej reprezentacji raz, pięć razy czy sto razy powinno pozostawić adres w tym samym stanie.

Można zapisać to skrótowo jako f(f(x)) = f(x). W przypadku API nie chodzi jednak o matematyczną identyczność całej odpowiedzi, lecz o zamierzony efekt po stronie serwera. Drugi request może zwrócić inny kod, na przykład 404 zamiast 204 po powtórnym usunięciu zasobu, a operacja nadal pozostaje idempotentna.

RFC 9110 definiuje idempotentność na poziomie zamierzonego efektu metody HTTP. To ważne rozróżnienie, ponieważ serwer może zapisywać logi, zwiększać licznik monitoringu albo aktualizować znacznik czasu. Takie techniczne skutki uboczne nie muszą przekreślać idempotentności, ale nie powinny zmieniać danych biznesowych przy każdym ponowieniu.

Które metody HTTP są idempotentne

Idempotentność wynika przede wszystkim z semantyki metody, ale ostatecznie zależy również od implementacji endpointu. Można użyć metody PUT zgodnie ze standardem, a mimo to napisać kod, który przy każdym wywołaniu dopisuje nowy rekord audytowy traktowany jako operacja biznesowa. Dlatego sama nazwa metody nie zastępuje analizy skutków.

Metoda Idempotentność Typowe zastosowanie Praktyczna uwaga
GET Tak Odczyt zasobu Nie powinien zmieniać danych biznesowych.
HEAD Tak Sprawdzenie nagłówków i dostępności Działa podobnie do GET, ale bez treści odpowiedzi.
PUT Tak Utworzenie lub pełna podmiana zasobu Ten sam request ustawia ten sam stan.
DELETE Tak Usunięcie zasobu Ponowne usunięcie nie usuwa „jeszcze bardziej”.
POST Nie z założenia Utworzenie zasobu lub uruchomienie procesu Kolejne wywołanie może utworzyć kolejny obiekt.
PATCH Nie z założenia Częściowa modyfikacja Może być idempotentny, jeśli operacja ustawia wartość zamiast ją zmieniać.

PUT nie jest tym samym co POST

Różnica najlepiej wychodzi przy tworzeniu zamówienia. Żądanie PUT /orders/123 może zapisać zamówienie pod znanym identyfikatorem. Powtórzenie requestu nadal dotyczy zamówienia 123, więc nie powinno utworzyć drugiego obiektu.

W przypadku POST /orders serwer zwykle sam generuje identyfikator. Dwa identyczne żądania mogą więc oznaczać dwa zamówienia, ponieważ każde uruchamia proces tworzenia nowego zasobu. Jeśli klient nie może sam nadać identyfikatora, potrzebny będzie dodatkowy mechanizm ochrony przed duplikatem.

DELETE może zwracać różne odpowiedzi

Pierwsze DELETE /users/42 może zwrócić 204 No Content, a drugie 404 Not Found. Stan zasobu po obu operacjach jest jednak taki sam, czyli użytkownik nie istnieje. To właśnie efekt po stronie serwera, a nie identyczny kod odpowiedzi, decyduje o idempotentności.

PATCH wymaga szczególnej ostrożności

Operacja PATCH ustawiająca status na „opłacone” może być idempotentna. Operacja zwiększająca licznik o jeden już nie, ponieważ każde powtórzenie zmieni wartość ponownie.

PATCH /counters/7
{
  "increment": 1
}

Bezpieczniejszy wariant wygląda tak:

PATCH /orders/123
{
  "status": "paid"
}

W pierwszym przykładzie request opisuje działanie, a w drugim docelowy stan. Przy projektowaniu API zwykle preferuję drugie podejście, gdy operacja może zostać ponowiona przez klienta lub kolejkę.

Dlaczego idempotentność jest ważna przy retry

Biblioteka kliencka, reverse proxy albo aplikacja mobilna może ponowić request po przekroczeniu limitu czasu. Klient nie wie wtedy, czy serwer nie otrzymał żądania, czy wykonał je i nie zdążył odesłać odpowiedzi. To jeden z najbardziej zdradliwych przypadków w systemach rozproszonych.

Załóżmy, że endpoint tworzy płatność. Serwer zapisuje transakcję, wysyła ją do operatora i dopiero potem odpowiada. Jeśli połączenie zerwie się po zapisie, klient może ponowić request. Bez ochrony powstaną dwie płatności, mimo że użytkownik kliknął przycisk tylko raz.

Retry jest rozsądną strategią dla błędów przejściowych, takich jak chwilowa niedostępność usługi, timeout albo odpowiedź 503 Service Unavailable. Nie powinien jednak bezrefleksyjnie ponawiać operacji biznesowych, które nie mają bezpiecznego mechanizmu deduplikacji. Dokumentacja Azure Architecture Center słusznie traktuje idempotentność jako jeden z warunków bezpiecznego stosowania ponowień.

Idempotentność a bezpieczeństwo

Idempotentna metoda nie musi być metodą bezpieczną. GET jest zwykle bezpieczny, bo służy do odczytu, natomiast PUT i DELETE mogą zmienić dane, choć ich wielokrotne wykonanie prowadzi do tego samego stanu.

To rozróżnienie ma znaczenie przy automatycznych mechanizmach klienta. Odczyt można zazwyczaj ponowić bez większego ryzyka. Usunięcie również może być ponawiane, ale aktualizacja lub usunięcie nadal wymagają autoryzacji, kontroli współbieżności i audytu.

Idempotentność nie oznacza wykonania tylko raz

Serwer może przetworzyć ten sam request więcej niż raz, ale powinien osiągnąć ten sam efekt biznesowy. Jeśli wywołanie publikuje komunikat, wysyła e-mail i zapisuje rekord, sama idempotentność głównej tabeli nie wystarczy. Każdy istotny efekt uboczny trzeba objąć deduplikacją albo zaprojektować przez wzorzec outbox.

W praktyce systemy chmurowe często działają zgodnie z zasadą at-least-once delivery, czyli dostarczenie może nastąpić więcej niż raz. Odbiorca komunikatu powinien przechowywać identyfikator przetworzonej wiadomości i odrzucać duplikaty, zamiast zakładać, że infrastruktura zapewni idealne „dokładnie raz”.

Jak zabezpieczyć POST za pomocą klucza idempotencji

Dla operacji, które naturalnie korzystają z POST, stosuję osobny identyfikator pojedynczej próby biznesowej. Klient generuje wartość, na przykład UUID, i wysyła ją w nagłówku Idempotency-Key. Serwer zapisuje ten klucz razem z wynikiem operacji.

POST /payments
Idempotency-Key: 7f2b9c12-7d0e-4ae1-bb4a-2f2c8f4d8a11

{
  "orderId": "ORD-1007",
  "amount": 249.99,
  "currency": "PLN"
}

Przy pierwszym requestcie serwer wykonuje płatność i zapisuje odpowiedź. Przy kolejnym requestcie z tym samym kluczem powinien zwrócić wynik wcześniej zakończonej operacji, a nie uruchamiać płatność ponownie.

Co trzeba przechowywać

Minimalny rekord deduplikacyjny powinien zawierać klucz, identyfikator klienta lub użytkownika, nazwę operacji, skrót requestu, status przetwarzania, kod odpowiedzi i dane odpowiedzi. W wielu systemach przechowuje się również identyfikator utworzonego zasobu.

  • Klucz musi być unikalny przynajmniej w obrębie klienta i operacji.
  • Ten sam klucz z innym payloadem powinien kończyć się błędem, na przykład 409 Conflict.
  • Stan „processing” chroni przed równoległym wykonaniem dwóch identycznych requestów.
  • Retencja kluczy musi odpowiadać czasowi, w którym klient może ponowić operację. Typowy przedział to 24-48 godzin, ale płatności lub procesy asynchroniczne mogą wymagać dłuższego okresu.
  • Ograniczenie unikalności w bazie jest ważniejsze niż sama kontrola w kodzie aplikacji.

Nie usuwam kluczy po kilku minutach tylko dlatego, że request zakończył się sukcesem. Jeśli urządzenie użytkownika pozostanie offline przez kilka godzin, późniejsze ponowienie nadal powinno zostać rozpoznane. Czas przechowywania trzeba dobrać do realnego cyklu operacji, a nie do wygodnego domyślnego TTL.

Przykładowy schemat obsługi w ASP.NET Core

[HttpPost("payments")]
public async Task CreatePayment(
    [FromHeader(Name = "Idempotency-Key")] string key,
    CreatePaymentRequest request)
{
    var existing = await repository.FindAsync(UserId, key);

    if (existing is not null)
    {
        if (!existing.RequestHashMatches(request))
            return Conflict("Ten klucz został użyty z innymi danymi.");

        return StatusCode(existing.StatusCode, existing.Response);
    }

    var operation = await repository.CreatePendingAsync(
        UserId, key, request);

    var result = await paymentService.ProcessAsync(request);

    await repository.CompleteAsync(operation.Id, result);

    return Ok(result);
}

Ten kod pokazuje kierunek, ale nie rozwiązuje sam z siebie wyścigu dwóch równoległych requestów. Potrzebna jest transakcja, blokada albo unikalny indeks na przykład na parze UserId + IdempotencyKey. W systemie produkcyjnym trzeba też obsłużyć przerwanie procesu po utworzeniu rekordu „pending”.

Jak projektować idempotentne endpointy w .NET

Najłatwiej zacząć od opisania operacji jako ustawiania stanu, a nie wykonywania polecenia. Zamiast endpointu „zwiększ saldo o 10” lepiej, jeśli to możliwe, przyjąć żądanie ustawiające konkretny stan albo przypisać operacji unikalny identyfikator.

Używaj stabilnych identyfikatorów zasobów

Endpoint PUT /documents/abc ma naturalną ochronę przed duplikatem, ponieważ identyfikator dokumentu jest znany przed zapisem. Jeśli request zostanie wysłany ponownie, aktualizuje ten sam rekord. To prostsze do kontrolowania niż POST, który za każdym razem może wygenerować nowy klucz.

Dodaj kontrolę współbieżności

Idempotentność odpowiada na pytanie, co się stanie przy powtórzeniu tego samego requestu. Nie rozwiązuje konfliktu dwóch różnych requestów wysłanych niemal jednocześnie. Do tego potrzebny jest optimistic concurrency, czyli sprawdzanie wersji rekordu, numeru wiersza lub nagłówka ETag.

PUT /profiles/42
If-Match: "version-17"

{
  "displayName": "Anna Kowalska",
  "timezone": "Europe/Warsaw"
}

Jeśli ktoś wcześniej zmienił profil, wersja nie będzie się zgadzać i serwer może zwrócić 412 Precondition Failed. Dzięki temu ponowienie starego requestu nie nadpisze po cichu nowszych danych.

Oddziel zapis od efektów ubocznych

Gdy aktualizacja danych uruchamia wysyłkę wiadomości, dobrym rozwiązaniem jest zapisanie zmiany i zdarzenia w jednej transakcji. Worker odczytuje zdarzenie z tabeli outbox, wysyła komunikat i oznacza go jako przetworzony. Jeśli worker uruchomi się ponownie, identyfikator zdarzenia pozwala rozpoznać powtórne przetwarzanie.

Sam mechanizm retry w HttpClientFactory albo bibliotece Polly nie czyni operacji bezpieczną. Retry tylko zwiększa liczbę prób. To model danych, klucz deduplikacyjny i zachowanie endpointu decydują, czy dodatkowa próba będzie neutralna.

Typowe błędy przy wdrażaniu idempotentności

Sprawdzanie duplikatu wyłącznie w pamięci

Dwa requesty mogą trafić do różnych instancji aplikacji, a po restarcie proces straci lokalny słownik kluczy. Pamięć procesu nadaje się do krótkiego cache, ale nie jako źródło prawdy. Deduplikację należy oprzeć na współdzielonej bazie, Redisie z odpowiednią polityką trwałości albo innym trwałym magazynie.

Zwracanie sukcesu bez zapisania wyniku

Jeżeli pierwszy request utworzył płatność, ale serwer nie zapisał odpowiedzi powiązanej z kluczem, kolejna próba nie będzie wiedziała, co zwrócić. Rekord klucza powinien być aktualizowany do stanu końcowego razem z identyfikatorem efektu biznesowego. Warto też rozróżnić stan completed, failed i processing.

Traktowanie każdego błędu tak samo

Timeout można czasem bezpiecznie ponowić, ale błąd walidacji, brak uprawnień albo nieistniejące zamówienie nie stanie się poprawny po kolejnych próbach. Polityka retry powinna uwzględniać kod HTTP, typ wyjątku i idempotentność konkretnej operacji.

Przeczytaj również: Nagłówek H2 w HTML - jak budować dobrą hierarchię?

Przypadkowe skutki uboczne w GET

GET z endpointu /reports/generate, który przy każdym odczycie tworzy plik lub wysyła e-mail, łamie oczekiwania klienta. Sam fakt użycia GET nie ochroni aplikacji. Jeśli operacja uruchamia proces, lepiej użyć POST i jawnie zaprojektować jej obsługę oraz możliwość ponowienia.

Jak sprawdzić idempotentność przed wdrożeniem

Przed publikacją endpointu wykonuję kilka prostych testów. Wysyłam identyczny request dwa lub więcej razy, przerywam połączenie po stronie klienta, uruchamiam równoległe żądania i sprawdzam nie tylko odpowiedź HTTP, ale też liczbę rekordów, komunikatów, płatności oraz innych efektów ubocznych.

  • Powtórzenie sekwencyjne powinno pozostawić dane w tym samym stanie.
  • Powtórzenie po timeoutcie nie może utworzyć drugiego efektu biznesowego.
  • Dwa requesty równoległe powinny zostać zserializowane albo rozpoznane jako duplikat.
  • Zmiana payloadu przy tym samym kluczu powinna zostać odrzucona.
  • Restart aplikacji nie może usuwać informacji potrzebnych do deduplikacji.
  • Retry po błędzie zależności powinien działać zgodnie z ustaloną polityką, bez lawiny kolejnych prób.

W logach warto rejestrować identyfikator korelacyjny, klucz idempotencji, identyfikator zasobu i numer próby. Dzięki temu łatwo odróżnić normalne ponowienie od błędu w kliencie. Nie logowałbym jednak pełnego payloadu, jeśli może zawierać dane osobowe lub informacje płatnicze.

Idempotentne API zaczyna się od modelu operacji

Najważniejsza zasada jest prosta. Powtórzenie requestu nie powinno powtarzać efektu biznesowego, nawet jeśli technicznie serwer otrzyma go kilka razy. GET, PUT i DELETE dają dobry fundament, ale POST i PATCH wymagają dodatkowych decyzji dotyczących identyfikatorów, kluczy, transakcji i skutków ubocznych.

W aplikacjach .NET szczególnie dobrze sprawdza się połączenie stabilnego identyfikatora zasobu, unikalnego indeksu w bazie, kontroli współbieżności oraz outboxa dla komunikacji asynchronicznej. Taki zestaw nie usuwa wszystkich problemów systemów rozproszonych, ale sprawia, że timeout, retry i ponowne dostarczenie wiadomości przestają być źródłem przypadkowych duplikatów.

FAQ - Najczęstsze pytania

Po timeoutcie klient nie wie, czy serwer nie otrzymał żądania, czy zapisał zmianę bez wysłania odpowiedzi. Idempotentność sprawia, że ponowienie nie powtarza efektu biznesowego, na przykład nie tworzy drugiej płatności. Nie oznacza jednak, że serwer wykona request tylko raz ani że zwróci identyczną odpowiedź HTTP.

GET, HEAD, PUT i DELETE są z założenia idempotentne, ale PUT i DELETE nadal mogą zmieniać dane i wymagają autoryzacji oraz kontroli współbieżności. POST i PATCH nie są automatycznie idempotentne. PATCH może być bezpieczny, gdy ustawia konkretną wartość, na przykład status „paid”, ale nie wtedy, gdy zwiększa licznik.

Klient powinien wysłać unikalny UUID w nagłówku Idempotency-Key, a serwer zapisać go wraz z identyfikatorem klienta, operacją, skrótem requestu, statusem i wynikiem. Ponowienie z tym samym kluczem powinno zwrócić wcześniej zapisany rezultat. Użycie tego samego klucza z innym payloadem należy odrzucić, na przykład kodem 409 Conflict.

Ochrona przed równoległymi requestami wymaga transakcji, blokady albo unikalnego indeksu, na przykład dla pary UserId i IdempotencyKey. Przy efektach asynchronicznych warto zastosować wzorzec outbox oraz identyfikatory przetworzonych wiadomości. Kontrola wersji rekordu lub nagłówek ETag chronią dodatkowo przed nadpisaniem nowszych danych przez stary request.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

deduplikacja
współbieżność
outbox
idempotentność
ponowienia
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz