GET, POST, PUT?
Różnica między metodami HTTP jest w dużej mierze znaczeniowa. Każda z nich niesie ze sobą pewne intencje. Zatem jeśli wszędzie piszą i mówią (w tym specyfikacja HTTP), że metoda GET jest bezpieczną metodą pobrania informacji, to ja to przyjmuję do wiadomości i będę się tego trzymał tworząc kod mojego API. W końcu od tego jest specyfikacja. Ale czy to oznacza, że rodzice albo jakieś ograniczenia technologiczne zabronią mi usunąć wiersz z bazy danych, gdy dotrze do mnie request GET? No nie. Generalnie wszystko możesz! :> Czy tak będzie ok czy nie, to inna para kaloszy. Bo to zależy. Sam musisz odpowiedzieć sobie na to pytanie. Jeśli tworzysz API z którego będziesz korzystał Ty i Twoja dziewczyna, to kto by się martwił specyfikacją i o to jakie metody HTTP są używane. Mógłbym na przykład obsłużyć usuwanie zasobu poprzez uderzenie GETem do poniższego URLa:
http://1.1.1.1/api/products?id=1&action=1
Do tego kodzik:
[HttpGet] [ActionName("products")] public IHttpActionResult Handle(int id, string action) { if("1".Equals(action)) { myService.deleteFromEverywhereHehe(id); } (...) }
Done. Mógłbym nawet to zrobić bez tego parametru action. Mów mi zwariowany programista. Niby GET, a ja nie zwracam danych tylko usuwam (i może nie tylko). Piorun nie uderzył, nie zająłem też się żywym ogniem. Mógłbym zwrócić jakieś dane i dodatkowo jeszcze w jakiś inny sposób zmodyfikować stan biznesowy mojego systemu. No nie jestem read-only, ale cóż. Zakładam koronę i klikam jak chcę.
Wytrawnego playera takie API jednak naraża jedynie na smutek. Po jednej bowiem stronie jest klient, który chce jakieś dane z mojego API, a z drugiej strony jestem ja, który tworzy to API. Nie znam tego klienta, nie komunikowaliśmy się werbalnie. On widzi GETa, myśli że dostanie dane, a tu figa z makiem. Nie dostaje danych i co więcej zaczyna po pewnym czasie zauważać, że te jego niby bezpieczne requesty psują mu życie.
Mogę stworzyć też takie uproszczone API
[HttpGet] [ActionName("products")] public IHttpActionResult GetAllProducts() { List<Product> products = productsService.getAll(); (...) }
Lepiej. Teraz moja implementacja jest zgodna z przeznaczeniem ujętym w specyfikacji.
Każda z metod HTTP wyraża pewną intencję i staram się tego trzymać.
GET & HEAD & idempotentność & bezpieczeństwo
Klient, który wykonuje GET spodziewa się (bo tak jest napisane w specyfikacji HTTP), że otrzyma w zwrotce dane (dokładnie to chodzi nam tu o entity zwracane w responsie). Za każdym razem, choćbym uderzał 150 razy do API po produkt o id 1 to wiem, że otrzymam w odpowiedzi dane dotyczące produktu o identyfikatorze 1 i moje żądanie nie powinno mieć skutków innych niż owe pobranie danych. Użytkownik nie żąda i nie oczekuje, aby kod obsługujący jego żądanie GET zmieniał stan systemu w jakikolwiek sposób. Metoda GET (i HEAD) według specyfikacji jest bowiem bezpieczna. Użytkownik wykonujący metodę GET bądź HEAD, kierując się wiedzą ze specyfikacji HTTP czuje ciepło na serduszku, bo wie, że nic mu nie grozi. Tzn., wykonane przez niego akcje nie powinny mieć szkodliwych efektów ubocznych. Pobranie danych i nic więcej. Metody GET oraz HEAD są również idempotentne. Z minutę pisałem to słowo. Tak naprawdę to skopiowałem z internetu, bo nie umiałem napisać. Oznacza to, że ile razy byśmy nie wywołali GETem naszego:
http://1.1.1.1/api/products
to stan naszego systemu będzie taki sam jak dla pojedynczego requestu.
Właściwość jaką jest idempotentność zawierają także metody PUT, DELETE, OPTIONS i TRACE.
Metoda HEAD jest identyczna jak GET tyle, że nie zwraca danych. A żeby być dokładnym i trzymać się terminów opisanych w poprzedniej części, to nie zwraca message-body. Nagłówki zwrócone w responsie metody HEAD będą takie same jak te, które byś dostał wykonując GET do zasobu.
Po co HEAD? Krótkie wyjaśnienie:
This method can be used for obtaining metainformation about the entity implied by the request without transferring the entity-body itself. This method is often used for testing hypertext links for validity, accessibility, and recent modification.
Brzmi całkiem klarownie.
POST vs PUT
Najpierw POST. RFC7231 i 2616 (kapkę deprecated i lepiej zajrzeć na httpbis), podaje kilka przykładów na użycie POSTa. O czym poniżej.
Kiedy POST (hehe)
Użyjemy POSTa gdy, ogólnie mówiąc, chcemy przetworzyć dane wedle logiki zawartej na serwerze. Mark Nottingham pisze o metodzie POST jako funkcji o odpowiedzialności: “process this”. Po prostu. Może to się skończyć utworzeniem zasobu, ale nie musi. Może będzie to zwykłe przeprocesowanie danych / przekazanie ich gdzieś dalej.
I mniej więcej to samo mówi RFC:
The POST method requests that the origin server accept the representation enclosed in the request as data to be processed by the target resource. – httpbis
….i podaje też takie przykłady użycia.
– Posting a message to a bulletin board, newsgroup, mailing list, or similar group of articles;
– Providing a block of data, such as the result of submitting a form, to a data-handling process; – Extending a database through an append operation.
a) Tworzymy zasób
Stwórzmy nową wizytę u doktora od oczu:
POST /api/appointments/ <nagłówki> { <dane wizyty> }
Uderzyliśmy do “kolekcji” / do zasobu appointments (zasób [resource] czyli identyfikowalny poprzez URI byt).
Powinienem oczekiwać w responsie informacji, że moja wizyta została utworzona (kod 201) i jest dostępna pod jakimś URI. Może /api/appointments/123, a może serwer przetworzy żądanie i umieści wizytę w /api/visits/v-abc-123. Jako klient nie mam wiedzy na ten temat. Informacja ta mogłaby być dostępna dla mnie w nagłówku Location odpowiedzi serwera. Jest to zachowanie, które powinno (SHOULD według RFC) zostać zapewnione przez dostawcę API.
A dążę do tego, że według specyfikacji intencją utworzenia endpointu dostępnego za pomocą metody POST powinno być przetworzenie danych bez żadnej “kontroli” / sugestii / wsparcia ze strony klienta (np.: klient nie sugeruje identyfikatora bądź nazwy zasobu pod którym potem chce mieć do niego dostęp). To serwer decyduje jak przetworzy wysłany request do zasobu. Czyli klient uderzając do /api/appointments/ nie powinien oczekiwać, że wizyta zostanie utworzona akurat w /api/appointments/{id} (czyli w URI requestu) lub co więcej żeby miała jakiś konkretny identyfikator. I tak też mówi RFC:
A service that is intended to select a proper URI on behalf of the client, after receiving a state-changing request, SHOULD be implemented using the POST method rather than PUT.
O tym, że korzystając z metody POST to serwer decyduje o wszystkim co się dzieje wokół zasobów, pisze także Mark Nottingham:
POST keeps the server in control of the URI space, while PUT effectively gives it to the client.
I to jest jedna z różnic między POST a PUT, do czego jeszcze wrócę.
b) Przetwarzamy żądanie bez tworzenie zasobu
Ok. A co jeśli chcemy wysłać do aplikacji typu “mailer” dane, aby wysłał za nas maila do Mamy. No wyślemy mu adres mailowy Mamy, swój adres i treść. Mailer przetworzy nasz request wysłany za pomocą metody POST – wyśle maila. Nie powinien nam zwracać kodu 201 (Created), bo serwer nie stworzył żadnego zasobu, który potem możemy sobie pobrać GETem po id. Ale może nam zwrócić za to kod 202 (Accepted) – przyjąłem zlecenie do realizacji. W tym przypadku nie zmieniłem stanu systemu. Czy POST musi zmieniać stan systemu? Chyba nie. Dziwne to by było wymaganie. Nie znalazłem nigdzie takiej informacji.
Serwer przyjął żądanie POST, przetworzył je i finito.
Kiedy PUT
RFC zakłada, że klient używający metody PUT wie do jakiego konkretnego zasobu chce wysłać wiadomość. Kod implementujący obsługę żądania PUT przyjmie request klienta zawierający informację o konkretnym zasobie, następnie sprawdzi czy zasób taki istnieje. Jeśli nie, to utworzy go. Jeśli zaś istnieje, to dokona aktualizacji, a dokładnie mówiąc podmiany tego co mamy utrwalone, na to, co niesie ze sobą w treści request.
Mówi też o tym fragment RFC:
Proper interpretation of a PUT request presumes that the user agent knows what target resource is desired.
A poniżej krótki przykład tego zagadnienia.
Chcąc umieścić wpis na blogu o identyfikatorze (tytule) “Nauka-Http” mógłbym uderzyć do zasobu /api/posts/Nauka-Http. Przekazuję zatem do serwera tytuł / identyfikator mojego zasobu jakim jest wpis. Wysyłając więc żądanie PUT, użytkownik ma pewną kontrolę nad tym jak zostanie przetworzony request. A dokładnie ma pewien wpływ na to, jak będzie wyglądał zasób, do którego się odwołał. Możemy wrócić więc w tym miejscu do cytatu Marka N.:
POST keeps the server in control of the URI space, while PUT effectively gives it to the client.
A jeśli wyślemy żądanie i nie dostaniemy odpowiedzi, ze względu np.: na problemy sieciowe, to możemy taki request PUT spokojnie powtórzyć raz, dwa czy 100 razy. Stan naszego systemu nie zmieni się. O przeznaczeniu metody PUT tak mówi RFC:
(…) the target resource in a PUT request is intended to take the enclosed representation as a new or replacement value.
Zaznaczam jednak – to są tylko (albo aż) reguły, których programista będzie się trzymał lub nie.
Podsumowanie
Żadna z tych metod nie jest sama z siebie idempotentna czy bezpieczna. Zależy to od kodu, który piszesz. Tworzymy oprogramowanie dla kogoś. Chcemy, aby nasze API było swego rodzaju sposobem komunikacji między nami. Żeby było czytelne i w swojej formie niosło instrukcję użytkowania. W miarę możliwości. Więc musimy, tzn., powinniśmy trzymać się jakiegoś standardu, specyfikacji i korzystać ze wspólnego źródła wiedzy.
Czyli co. Wszędzie piszą updaty PUTem, a nowe zasoby POSTem. Serwer przyjmujący request wysłany metodą PUT może utworzyć zasób jeśli nie istnieje. Jeśli dusza zapragnie to POSTem możesz aktualizować zasób. Nie widzę nigdzie, aby ktoś pisał, że trafisz z tego powodu do kąta. Mark N. pisze, że obie metody mogą być używane zarówno do aktualizacji jak i do tworzenia zasobu. Ważny jest z pewnością kontekst use case’a wokół którego budujesz API. Może właśnie klikasz kodzik, który spowoduje, że uzyskasz idempotentność metody POST? 🙂
Jedną z dobrych reguł na wybranie odpowiedniej metody będzie przemyślenie intencji poszczególnych metod oraz specyfiki procesu, który zakodowaliśmy w naszych kontrolerach i serwisach / whatever. Co chcemy powiedzieć użytkownikowi tworząc nasze API.
Mark Nottingham ujął to jeszcze tak: użyjesz PUT jeśli chcesz dokonać operacji utworzenia/aktualizacji “tu”. “Tu”, tzn., wskazując dokładne miejsce. Chcę utworzyć / dokonać podmiany w konkretnym zasobie: /api/articles/Nauka-Http. Natomiast POSTa użyjemy, aby przetworzyć dane według widzimisi serwera. Dokładny cytat poniżej:
The difference is that PUT says “I want this to go here, and nowhere else;” i.e., there’s a good chance that when you GET the same place later, you’ll get the same thing back (unless someone else has changed it, or another change had side effects on it, etc.). POST says “Here’s this; process it as you will (probably as you described to me earlier), and the results can end up anywhere.”
I jeszcze to, w zasadzie to samo:
Think about the difference between POST and PUT; the former is a “process this” function that might result in the representation being sent becoming available pretty much anywhere, while the latter very specifically says “put this here.”
I wzmianka z RFC:
The fundamental difference between the POST and PUT methods is highlighted by the different intent for the enclosed representation. The target resource in a POST request is intended to handle the enclosed representation according to the resource’s own semantics, whereas the enclosed representation in a PUT request is defined as replacing the state of the target resource. Hence, the intent of PUT is idempotent and visible to intermediaries, even though the exact effect is only known by the origin server.
Dobrze byłoby wspomnieć jeszcze o PATCH, ale o tym i o cache’owaniu następnym razem.