Jak interfejs API REST powinien obsługiwać żądania PUT zasobów częściowo modyfikowalnych?

46

Załóżmy, że interfejs API REST w odpowiedzi na GETżądanie HTTP zwraca dodatkowe dane w podobiektie owner:

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'Programmer'
  }
}

Oczywiście nie chcemy, aby ktokolwiek mógł się PUTcofnąć

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'CEO'
  }
}

i odnieść sukces. Rzeczywiście, w tym przypadku prawdopodobnie nie zamierzamy nawet wdrożyć sposobu, aby nawet potencjalnie odnieść sukces.

Ale to pytanie nie dotyczy tylko podobiektów: co ogólnie należy zrobić z danymi, których nie można modyfikować w żądaniu PUT?

Czy powinien być wymagany brak w żądaniu PUT?

Czy należy to po cichu wyrzucić?

Czy należy to sprawdzić, a jeśli różni się od starej wartości tego atrybutu, zwróć kod błędu HTTP w odpowiedzi?

A może powinniśmy używać łatek JFC RFC 6902 zamiast wysyłać cały JSON?

Robin Green
źródło
2
Wszystko to działałoby. Myślę, że to zależy od twoich wymagań.
Robert Harvey
Powiedziałbym, że zasada najmniejszego zaskoczenia wskazywałaby na brak jej w żądaniu PUT. Jeśli nie jest to możliwe, sprawdź, czy jest inaczej i wróć z kodem błędu. Ciche odrzucanie jest najgorsze (wysyłanie przez użytkowników oczekuje, że zmieni się, a ty powiesz im „200 OK”).
Maciej Piechotka
2
@MaciejPiechotka problem polega na tym, że nie możesz użyć tego samego modelu na opakowaniu jak na wkładce lub dostać itp. Wolałbym, aby ten sam model został użyty i istnieją proste reguły autoryzacji w terenie, więc jeśli wprowadzą wartość dla pole, którego nie powinni zmieniać, zwracają 403 Zabronione, a jeśli później zezwolenie zostanie skonfigurowane, aby na to pozwolić, otrzymają 401 Nieautoryzowane, jeśli nie są upoważnione
Jimmy Hoffa
@JimmyHoffa: Przez model rozumiesz format danych (ponieważ może być możliwe ponowne użycie modelu w frameworku MVC Rest w zależności od wyboru, jeśli taki jest używany - OP nie wspomniał o żadnym)? Wybrałbym wykrywalność, gdybym nie był ograniczony przez strukturę, a wczesny błąd jest nieco bardziej wykrywalny / łatwiejszy do wdrożenia niż sprawdzanie zmian (ok - nie powinienem dotykać pola XYZ). W każdym razie odrzucenie jest najgorsze.
Maciej Piechotka,

Odpowiedzi:

46

Nie ma żadnej reguły, ani w specyfikacji W3C, ani w nieoficjalnych regułach REST, która mówi, że PUTnależy użyć tego samego schematu / modelu, który odpowiada GET.

To miłe, jeśli są podobne , ale nie jest niczym niezwykłym, PUTże robią rzeczy nieco inaczej. Na przykład widziałem wiele interfejsów API, które zawierają pewien rodzaj identyfikatora w treści zwróconej przez GETdla wygody. Ale z PUT, ten identyfikator jest określany wyłącznie przez URI i nie ma znaczenia w treści. Każdy identyfikator znaleziony w ciele zostanie po cichu zignorowany.

REST i ogólnie sieć są silnie powiązane z zasadą solidności : „Bądź konserwatywny w tym, co robisz [wysyłasz], bądź liberalny w tym, co akceptujesz”. Jeśli zgadzasz się z tym filozoficznie, rozwiązanie jest oczywiste: zignoruj ​​wszelkie nieprawidłowe dane w PUTżądaniach. Dotyczy to zarówno niezmiennych danych, jak w twoim przykładzie, jak i faktycznych bzdur, np. Nieznanych pól.

PATCHjest potencjalnie kolejną opcją, ale nie należy jej wdrażać, PATCHchyba że faktycznie będzie się wspierać częściowe aktualizacje. PATCHoznacza jedynie aktualizację określonych atrybutów, które zawarłem w treści ; to jednak nie znaczy wymienić całą jednostkę jednak wykluczyć pewne konkretne pola . To, o czym tak naprawdę mówisz, nie jest tak naprawdę częściową aktualizacją, jest to pełna aktualizacja, idempotentna i wszystko, tylko część zasobów jest tylko do odczytu.

Dobrą rzeczą do zrobienia, jeśli wybierzesz tę opcję, byłoby odesłanie 200 (OK) z faktycznie zaktualizowanym bytem w odpowiedzi, aby klienci mogli wyraźnie zobaczyć, że pola tylko do odczytu nie zostały zaktualizowane.

Z pewnością są ludzie, którzy myślą w drugą stronę - że próba aktualizacji części zasobu tylko do odczytu powinna być błędem. Jest to uzasadnione, przede wszystkim dlatego, że zdecydowanie zwróciłbyś błąd, gdyby cały zasób był tylko do odczytu, a użytkownik próbował go zaktualizować. Zdecydowanie jest to sprzeczne z zasadą niezawodności, ale możesz uznać, że jest to bardziej „samo dokumentujące” dla użytkowników Twojego API.

Istnieją w tym celu dwie konwencje, które odpowiadają twoim oryginalnym pomysłom, ale rozwinę je. Pierwszym z nich jest zabronienie pojawiania się w treści pól tylko do odczytu i zwrócenie HTTP 400 (Błędne żądanie), jeśli tak się stanie. Interfejsy API tego rodzaju powinny również zwracać HTTP 400, jeśli istnieją inne nierozpoznane / nieużywalne pola. Drugim jest wymaganie, aby pola tylko do odczytu były identyczne z bieżącą zawartością, i zwrócenie wartości 409 (konflikt), jeśli wartości nie są zgodne.

Naprawdę nie lubię sprawdzania równości z 409, ponieważ niezmiennie wymaga to od klienta wykonania GETw celu odzyskania bieżących danych przed wykonaniem PUT. To po prostu nie jest miłe i prawdopodobnie doprowadzi gdzieś do słabej wydajności. Ja też naprawdę nie lubią 403 (zabronione) za to, ponieważ oznacza to, że cały zasób jest zabezpieczony, a nie tylko jego część. Więc moim zdaniem, jeśli absolutnie musisz zweryfikować zamiast postępować zgodnie z zasadą niezawodności, zweryfikuj wszystkie swoje prośby i zwróć 400 za wszystkie, które mają dodatkowe lub niepisywalne pola.

Upewnij się, że twój 400/409 / cokolwiek zawiera informacje o tym, jaki jest konkretny problem i jak go naprawić.

Oba te podejścia są poprawne, ale wolę pierwsze z nich zgodnie z zasadą solidności. Jeśli kiedykolwiek doświadczyłeś pracy z dużym interfejsem API REST, docenisz wartość kompatybilności wstecznej. Jeśli kiedykolwiek zdecydujesz się usunąć istniejące pole lub ustawić je jako tylko do odczytu, jest to wstecznie kompatybilna zmiana, jeśli serwer po prostu zignoruje te pola, a starzy klienci będą nadal działać. Jeśli jednak dokonasz ścisłego sprawdzania poprawności treści, nie będzie ona już zgodna z poprzednimi wersjami, a starzy klienci przestaną działać. Ten pierwszy oznacza ogólnie mniej pracy zarówno dla osoby obsługującej interfejs API, jak i jego klientów.

Aaronaught
źródło
1
Dobra odpowiedź i pozytywnie oceniany. Nie jestem jednak pewien, czy się z tym zgadzam: „Jeśli kiedykolwiek zdecydujesz się usunąć istniejące pole lub ustawić je jako tylko do odczytu, jest to wstecznie kompatybilna zmiana, jeśli serwer po prostu ignoruje te pola, a starzy klienci nadal będą działać. „ Gdyby klient polegał na tym usuniętym / nowym polu tylko do odczytu, czy nie miałoby to nadal wpływu na ogólne zachowanie aplikacji? W przypadku usuwania pól uważam, że prawdopodobnie lepiej jest jawnie wygenerować błąd zamiast ignorować dane; w przeciwnym razie klient nie ma pojęcia, że ​​jego poprzednio działająca aktualizacja kończy się niepowodzeniem.
rinogo
Ta odpowiedź jest zła. z 2 powodów z RFC2616: 1. (sekcja 9.1.2) PUT musi być niezależny. Jeśli umieścisz wiele razy, da to ten sam wynik, co postawienie tylko raz. 2. Dostanie się do zasobu powinno zwrócić obiekt, jeśli nie zgłoszono żadnych innych żądań zmiany zasobu
brunoais,
1
Co się stanie, jeśli wykonasz kontrolę równości tylko wtedy, gdy niezmienna wartość została wysłana w żądaniu. Myślę, że daje to najlepsze z dwóch światów; nie zmuszasz klientów do GET i nadal powiadamiasz ich, że coś jest nie tak, jeśli wysłali niepoprawną wartość dla niezmiennego.
Ahmad Abdelghany
Dzięki, dogłębne porównanie, które zrobiłeś w ostatnich akapitach pochodzących z doświadczenia jest dokładnie tym, czego szukałem.
zjazd
9

Idem potencji

Po RFC PUT musiałby dostarczyć pełny obiekt do zasobu. Głównym tego powodem jest to, że PUT powinien być idempotentny. Oznacza to, że żądanie, które się powtarza, powinno dać taki sam wynik na serwerze.

Jeśli zezwolisz na częściowe aktualizacje, nie będzie to już możliwe. Jeśli masz dwóch klientów. Klient A i B mogą ewoluować następujący scenariusz:

Klient A pobiera obraz z obrazów zasobów. Zawiera opis obrazu, który jest nadal aktualny. Klient B umieszcza nowy obraz i odpowiednio aktualizuje opis. Obraz się zmienił. Klient A widzi, że nie musi zmieniać opisu, ponieważ jest tak, jak chce i umieszcza tylko obraz.

Doprowadzi to do niespójności, do obrazu dołączone są niewłaściwe metadane!

Jeszcze bardziej irytujące jest to, że każdy pośrednik może powtórzyć żądanie. W przypadku, gdy zdecyduje jakoś PUT nie powiodło się.

Znaczenie PUT nie może zostać zmienione (chociaż można go niewłaściwie użyć).

Inne opcje

Na szczęście istnieje inna opcja, to PATCH. PATCH to metoda, która pozwala częściowo zaktualizować strukturę. Możesz po prostu wysłać częściową strukturę. W przypadku prostych aplikacji jest to w porządku. Nie ma gwarancji, że ta metoda będzie silna. Klient powinien wysłać zapytanie w następującej formie:

PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 20
{fielda: 1, fieldc: 2}

A serwer może odpowiedzieć 204 (bez zawartości), aby oznaczyć sukces. W przypadku błędu nie można zaktualizować części struktury. Metoda PATCH jest atomowa.

Wadą tej metody jest to, że nie wszystkie przeglądarki obsługują to, ale jest to najbardziej naturalna opcja w usłudze REST.

Przykładowe żądanie łatki: http://tools.ietf.org/html/rfc5789#section-2.1

Łatanie Jsona

Opcja json wydaje się być dość kompleksowa i interesująca. Wdrożenie może być jednak trudne dla stron trzecich. Musisz zdecydować, czy Twoja baza użytkowników może to obsłużyć.

Jest to również nieco skomplikowane, ponieważ musisz zbudować mały interpreter, który konwertuje polecenia na częściową strukturę, której będziesz używać do aktualizacji swojego modelu. Ten interpreter powinien również sprawdzić, czy podane polecenia mają sens. Niektóre polecenia się znoszą. (napisz fielda, usuń fielda). Myślę, że chcesz zgłosić to klientowi, aby ograniczyć czas debugowania po jego stronie.

Ale jeśli masz czas, jest to naprawdę eleganckie rozwiązanie. Nadal powinieneś sprawdzić poprawność pól. Możesz połączyć to z metodą PATCH, aby pozostać w modelu REST. Ale myślę, że POST byłby do przyjęcia tutaj.

Idzie źle

Jeśli zdecydujesz się na opcję PUT, co jest nieco ryzykowne. W takim razie nie powinieneś przynajmniej odrzucać błędu. Użytkownik ma określone oczekiwania (dane zostaną zaktualizowane), a jeśli je przerwiesz, nie zapewnisz programistom dobrego czasu.

Możesz wybrać wycofanie: 409 Konflikt lub 403 Zakazane. To zależy, jak spojrzysz na proces aktualizacji. Jeśli zobaczysz to jako zestaw reguł (systemowo-centrycznych), konflikt będzie ładniejszy. Coś w rodzaju tych pól nie można aktualizować. (W sprzeczności z zasadami). Jeśli widzisz to jako problem z autoryzacją (zorientowany na użytkownika), powinieneś powrócić zabronione. Z: nie masz uprawnień do zmiany tych pól.

Nadal powinieneś zmusić użytkowników do wysłania wszystkich pól, które można modyfikować.

Rozsądną opcją do tego jest ustawienie podrzędnego zasobu, który oferuje tylko modyfikowalne dane.

Osobista opinia

Osobiście wybrałbym (jeśli nie musisz pracować z przeglądarkami) prosty model PATCH, a następnie rozszerzyłem go o procesor łatek JSON. Można to zrobić, rozróżniając typy mime: Typ mime łatki json:

application / json-patch

I json: application / json-patch

ułatwia wdrożenie go w dwóch fazach.

Edgar Klerks
źródło
3
Twój przykład idempotencji nie ma sensu. Albo zmienisz opis, albo nie. Tak czy inaczej, za każdym razem uzyskasz ten sam wynik.
Robert Harvey
1
Masz rację, myślę, że czas iść spać. Nie mogę tego edytować. To bardziej przykład racjonalnego przesłania wszystkich danych w żądaniu PUT. Dzięki za wskaźnik.
Edgar Klerks,
Wiem, że to było 3 lata temu ... ale czy wiesz, gdzie w RFC mogę znaleźć więcej informacji na temat „PUT musiałby dostarczyć pełny obiekt do zasobu”. Widziałem to wspomniane gdzie indziej, ale chciałbym zobaczyć, jak to jest zdefiniowane w specyfikacji.
CSharper
Myślę, że to znalazłem? tools.ietf.org/html/rfc5789#page-3
CSharper