Jaki kod stanu HTTP należy zwrócić, jeśli wiele akcji zakończy się różnymi statusami?

72

Buduję interfejs API, w którym użytkownik może poprosić serwer o wykonanie wielu akcji w jednym żądaniu HTTP. Wynik jest zwracany jako tablica JSON, z jednym wpisem na akcję.

Każda z tych akcji może zakończyć się niepowodzeniem lub odnieść sukces niezależnie od siebie. Na przykład pierwsza akcja może się powieść, dane wejściowe do drugiej akcji mogą być źle sformatowane i nie mogą się sprawdzić, a trzecia akcja może spowodować nieoczekiwany błąd.

Gdyby było jedno żądanie na akcję, zwróciłbym odpowiednio kody stanu 200, 422 i 500. Ale teraz, gdy jest tylko jedno żądanie, jaki kod stanu powinienem zwrócić?

Niektóre opcje:

  • Zawsze zwracaj 200 i podaj bardziej szczegółowe informacje w ciele.
  • Może postępujesz zgodnie z powyższą zasadą tylko wtedy, gdy w żądaniu jest więcej niż jedna akcja?
  • Może zwróci 200, jeśli wszystkie żądania zakończą się powodzeniem, w przeciwnym razie 500 (lub inny kod)?
  • Wystarczy użyć jednego żądania na akcję i zaakceptować dodatkowe koszty ogólne.
  • Coś zupełnie innego?
Anders
źródło
3
Twoje pytanie sprawiło, że pomyślałem o innym: programmers.stackexchange.com/questions/309147/…
AilurusFulgens
7
Nieznacznie powiązane: programmers.stackexchange.com/questions/305250/… (patrz zaakceptowana odpowiedź na temat oddzielenia kodów stanu HTTP od kodów aplikacji)
4
Jaką korzyść osiągniesz, grupując te żądania? Czy chodzi o logikę biznesową, na przykład transakcję obejmującą wiele zasobów, czy może chodzi o wydajność? Albo coś innego?
Luc Franken,
5
Ok, w takim przypadku zdecydowanie zalecam poprawę tej wydajności w innych obszarach. Wypróbuj optymistyczne interfejsy użytkownika, grupowanie żądań, buforowanie itp. Przed wdrożeniem tej złożoności w warstwie biznesowej. Czy masz jasny wgląd w to, gdzie tracisz najwięcej czasu?
Luc Franken,
4
... nie bądź zbytnio przekonany, że ludzie poprawnie spojrzą na te statusy. Większość programów sprawdza tylko te najczęstsze i nie działa lub działa nieprawidłowo, jeśli otrzyma nieoczekiwany kod stanu. (Pamiętam, że na DefCon odbyła się również prezentacja na temat ochrony twojej witryny przed robotami poprzez wysyłanie losowych stanów wyjścia, które przeglądarka ignoruje i po prostu pokazuje, dlaczego roboty robią czasem błędy, a tym samym przestają indeksować tę część witryny).
Bakuriu

Odpowiedzi:

21

Krótka, bezpośrednia odpowiedź

Ponieważ żądanie mówi o wykonaniu listy zadań (zadania są zasobem, o którym tu mówimy), to jeśli grupa zadań została przeniesiona do wykonania (to znaczy niezależnie od wyniku wykonania), byłoby rozsądne będzie status odpowiedzi 200 OK. W przeciwnym razie, jeśli wystąpi problem, który uniemożliwiłby wykonanie grupy zadań, taki jak niepowodzenie sprawdzania poprawności obiektów zadania lub niektóre wymagane usługi nie będą dostępne, na przykład stan odpowiedzi powinien oznaczać ten błąd. W przeszłości, gdy rozpoczyna się wykonywanie zadań, ponieważ zadania do wykonania są wymienione w treści żądania, oczekiwałbym, że wyniki wykonania zostaną wyświetlone w treści odpowiedzi.


Długa, filozoficzna odpowiedź

Ten dylemat występuje, ponieważ odwracasz się od tego, do czego został zaprojektowany HTTP. Nie współdziałasz z nim w celu zarządzania zasobami, a raczej używasz go jako narzędzia do zdalnego wywoływania metod (co nie jest bardzo dziwne, ale działa słabo bez wcześniej założonego schematu).

Biorąc powyższe pod uwagę, i bez odwagi, aby zamienić tę odpowiedź w długo opiniotwórczy przewodnik, poniżej przedstawiono schemat URI zgodny z podejściem do zarządzania zasobami:

  • /tasks
    • GET wyświetla wszystkie zadania, paginowane
    • POST dodaje jedno zadanie
  • /tasks/task/[id]
    • GET odpowiada obiektem stanu pojedynczego zadania
    • DELETE anuluje / usuwa zadanie
  • /tasks/groups
    • GET wyświetla wszystkie grupy zadań, paginowane
    • POST dodaje grupę zadań
  • /tasks/groups/group/[id]
    • GET odpowiada stanem grupy zadań
    • DELETE anuluje / usuwa grupę zadań

Ta struktura mówi o zasobach, a nie o tym, co z nimi zrobić. To, co dzieje się z zasobami, dotyczy innej usługi.

Inną ważną kwestią do zrobienia jest to, że wskazane jest, aby nie blokować zbyt długo w module obsługi żądań HTTP. Interfejs HTTP, podobnie jak interfejs użytkownika, powinien być responsywny - w skali czasowej wolniejszej o kilka rzędów wielkości (ponieważ ta warstwa dotyczy IO).

Przejście w kierunku zaprojektowania interfejsu HTTP, który ściśle zarządza zasobami, jest prawdopodobnie tak trudne, jak oderwanie pracy od wątku interfejsu użytkownika po kliknięciu przycisku. Wymaga to, aby serwer HTTP komunikował się z innymi usługami w celu wykonania zadań zamiast wykonywania ich w module obsługi żądań. To nie jest płytkie wdrożenie, to zmiana kierunku.


Kilka przykładów zastosowania takiego schematu URI

Wykonywanie pojedynczego zadania i śledzenie postępu:

  • POST /tasks z zadaniem do wykonania
    • GET /tasks/task/[id]do momentu, gdy obiekt odpowiedzi będzie completedmiał wartość dodatnią, jednocześnie pokazując aktualny status / postęp

Wykonanie pojedynczego zadania i oczekiwanie na jego zakończenie:

  • POST /tasks z zadaniem do wykonania
    • GET /tasks/task/[id]?awaitCompletion=truedopóki completedma wartość dodatnią (prawdopodobnie ma limit czasu, dlatego należy go zapętlić)

Wykonywanie grupy zadań i śledzenie postępu:

  • POST /tasks/groups z grupą zadań do wykonania
    • GET /tasks/groups/group/[groupId]do momentu, gdy completedwłaściwość obiektu odpowiedzi będzie miała wartość, pokazującą status pojedynczego zadania (na przykład 3 zadania ukończone z 5)

Żądanie wykonania grupy zadań i oczekiwanie na jej zakończenie:

  • POST /tasks/groups z grupą zadań do wykonania
    • GET /tasks/groups/group/[groupId]?awaitCompletion=true dopóki nie odpowie wynikiem oznaczającym zakończenie (prawdopodobnie ma limit czasu, dlatego należy go zapętlić)
Ben Barkay
źródło
Myślę, że mówienie o tym, co ma sens semantyczny, jest właściwym sposobem podejścia do tego. Dzięki!
Anders
2
Chciałem zaproponować tę odpowiedź, jeśli jeszcze jej tam nie było. Jest to niemożliwe , aby wiele żądań w jednym żądaniu HTTP. Z drugiej strony jest całkowicie możliwe utworzenie pojedynczego żądania HTTP, które mówi „wykonaj następujące czynności i daj mi znać, jakie są wyniki”. I oto co się tutaj dzieje.
Martin Kochański
Przyjmę tę odpowiedź, nawet jeśli ma ona daleką liczbę głosów. Chociaż inne odpowiedzi też są dobre, myślę, że to jedyna przyczyna semantyki HTTP.
Anders
87

Głosowałbym za podzieleniem tych zadań na osobne wnioski. Jeśli jednak problemem jest zbyt wiele podróży w obie strony, natknąłem się na kod odpowiedzi HTTP 207 - Multi-Status

Skopiuj / wklej z tego linku:

Odpowiedź na wiele statusów przekazuje informacje o wielu zasobach w sytuacjach, w których może być odpowiednie wiele kodów statusu. Domyślna treść odpowiedzi na wiele statusów to encja HTTP tekst / xml lub application / xml z elementem głównym „multistatus”. Kolejne elementy zawierają kody stanu serii 200, 300, 400 i 500 wygenerowane podczas wywołania metody. Kody statusu serii 100 NIE POWINNY być zapisywane w elemencie XML „odpowiedzi”.

Chociaż „ogólny kod odpowiedzi” jest używany jako „207”, odbiorca musi zapoznać się z treścią wieloczęściowego organu odpowiedzi w celu uzyskania dalszych informacji na temat powodzenia lub niepowodzenia wykonania metody. Odpowiedź MOŻE być wykorzystana w sukcesie, częściowym sukcesie, a także w sytuacjach awaryjnych.

neilsimp1
źródło
22
207wydaje się być tym, czego chce OP, ale naprawdę chcę podkreślić, że to prawdopodobnie zły pomysł, aby zastosować takie podejście z wieloma żądaniami w jednym. Jeśli chodzi o wydajność, powinieneś zająć się architekturą dla systemów skalowalnych poziomo w stylu chmurowym (co jest czymś, w czym systemy HTTP są świetne)
David Grinberg
44
@DavidGrinberg Nie mogłem się więcej nie zgodzić. Jeśli poszczególne akcje są tanie, narzut związany z obsługą żądania może być znacznie bardziej kosztowny niż sama akcja. Twoja sugestia może prowadzić do scenariuszy, w których aktualizacja wielu wierszy w bazie danych odbywa się przy użyciu osobnej transakcji na wiersz, ponieważ każdy wiersz jest wysyłany jako osobne żądanie. Jest to nie tylko okropnie nieefektywne, ale oznacza również, że nie będzie możliwa atomowa aktualizacja wielu wierszy, jeśli będzie to konieczne. Skalowanie w poziomie jest ważne, ale nie zastępuje wydajnych projektów.
kasperd
4
Dobrze powiedziane i wskazujące na typowy problem implementacji interfejsu API REST przez osoby nieświadome realiów potrzeb biznesowych, takich jak wydajność i / lub atomowość. Dlatego na przykład specyfikacja REST OData ma format wsadowy dla wielu operacji w jednym wywołaniu - jest to naprawdę potrzebne.
TomTom
8
@TomTom, OP nie chce atomowości. Byłoby to znacznie łatwiejsze do zaprojektowania, ponieważ istnieje tylko jeden status operacji atomowej. Ponadto specyfikacja HTTP pozwala na operacje wsadowe w celu zwiększenia wydajności poprzez multipleksowanie HTTP / 2 (oczywiście obsługa HTTP / 2 to inna sprawa, ale specyfikacja na to pozwala).
Paul Draper,
2
@David Pracowałem w przeszłości nad niektórymi problemami HPC, z mojego doświadczenia wynika, że ​​koszt wysłania jednego bajtu jest prawie taki sam jak wysłanie tysiąca (różne nośniki transferu mają z pewnością różne koszty ogólne, ale rzadko są lepsze niż to). Jeśli więc chodzi o wydajność, nie widzę, jak wysyłanie wielu żądań nie miałoby dużego obciążenia. Teraz, jeśli możesz multipleksować wiele żądań przez to samo połączenie, problem zniknie, ale jak rozumiem, jest to tylko opcja z HTTP / 2, a obsługa tego jest raczej ograniczona. A może coś mi brakuje?
Voo
24

Chociaż opcja wielu statusów jest opcją, zwróciłbym 200 (Wszystko jest dobrze), jeśli wszystkie żądania się powiodły, aw przeciwnym razie błąd (500, a może 207).

Standardowy przypadek powinien zwykle wynosić 200 - wszystko działa. Klienci powinni tylko to sprawdzić. I tylko jeśli wystąpił przypadek błędu, możesz zwrócić 500 (lub 207). Myślę, że 207 jest prawidłowym wyborem w przypadku co najmniej jednego błędu, ale jeśli widzisz cały pakiet jako jedną transakcję, możesz również wysłać 500. - Klient będzie chciał zinterpretować komunikat o błędzie w jakikolwiek sposób.

Dlaczego nie zawsze wysłać 207? - Ponieważ standardowe skrzynki powinny być łatwe i standardowe. Chociaż wyjątkowe przypadki mogą być wyjątkowe. Klient powinien czytać treść odpowiedzi i podejmować dalsze złożone decyzje, jeśli uzasadnia to wyjątkowa sytuacja.

Falco
źródło
6
Nie do końca się zgadzam. Jeśli żądanie 1 i 3 się powiedzie, otrzymasz połączony zasób i i tak musisz sprawdzić połączoną odpowiedź. Masz jeszcze jeden przypadek do rozważenia. Jeśli odpowiedź = 200 lub odpowiedź podrzędna 1 = 200, żądanie 1 powiodło się. Jeśli odpowiedź = 200 lub subresponse 2 = 200, to żądanie 2 powiedzie się i tak dalej, zamiast po prostu testować sub-odpowiedź.
gnasher729,
1
@ gnasher729 to naprawdę zależy od aplikacji. Wyobrażam sobie akcję kierowaną przez użytkownika, która po prostu przejdzie do następnego kroku z (wszystko ok), gdy wszystkie żądania zakończą się powodzeniem. - Jeśli coś poszło nie tak (stan globalny <= 200), musisz wyświetlić szczegółowe błędy i zmienić przepływ pracy i potrzebujesz tylko jednego sprawdzenia dla każdego zapytania, ponieważ jesteś w funkcji „handleMixedState”, a nie w funkcji „handleAllOk” .
Falco
To naprawdę zależy od tego, co to znaczy. Na przykład mam punkt końcowy, który kontroluje strategie handlowe. Możesz „uruchomić” listę identyfikatorów za jednym razem. Zwrot 200 oznacza, że ​​operacja (przetworzenie ich) zakończyła się powodzeniem - ale nie wszystkie mogą rozpocząć się pomyślnie. Które, przy okazji, nie można nawet zobaczyć w bezpośrednim wyniku (który rozpocznie się), ponieważ uruchomienie może potrwać kilka sekund. Semantyka w wywołaniach wielu operacji zależy od scenariusza.
TomTom
Najprawdopodobniej wysłałbym również 500, jeśli wystąpił ogólny problem (np. Wyłączenie bazy danych), więc serwer nawet nie próbuje pojedynczych żądań, ale może po prostu zwrócić ogólny błąd. - Ponieważ istnieją 3 różne wyniki dla użytkownika: 1. wszystko OK, 2. ogólny Problem, nic nie działa, 3. Niektóre żądania nie powiodły się. -> Co zwykle prowadzi do zupełnie innego przebiegu programu.
Falco
1
Ok, więc jednym podejściem byłoby: 207 = indywidualny status dla każdego żądania. Wszystko inne: Zwrócony status dotyczy każdego żądania. Ma sens dla 200, 401, ≥ 500.
gnasher729
13

Jedną z opcji byłoby zawsze zwrócenie kodu stanu 200, a następnie zwrócenie określonych błędów w treści dokumentu JSON. Tak właśnie zaprojektowano niektóre interfejsy API (zawsze zwracają kod stanu 200 i wysyłają błąd w treści). Aby uzyskać więcej informacji na temat różnych podejść, zobacz http://archive.oreilly.com/pub/post/restful_error_handling.html

BigONotation
źródło
2
W tym przypadku podoba mi się pomysł użycia, 200aby wskazać, że wszystko jest w porządku, żądanie zostało odebrane i było prawidłowe , a następnie użyć JSON, aby podać szczegóły na temat tego, co wydarzyło się w tym żądaniu (tj. Wyniku transakcji).
rickcnagy
4

Myślę, że neilsimp1 jest poprawny, ale zaleciłbym przeprojektowanie przesyłanych danych w taki sposób, aby można było wysłać 206 - Acceptedi przetworzyć dane później. Być może z oddzwanianiem.

Problem z próbą wysłania wielu akcji w jednym żądaniu polega na tym, że każda akcja powinna mieć swój własny „status”

Patrząc na import CSV (nie wiem tak naprawdę o czym jest OP, ale jest to prosta wersja). POST CSV i odzyskaj 206. Następnie później CSV można zaimportować, a status importu można uzyskać za pomocą GET (200) w odniesieniu do adresu URL, który pokazuje błędy według wierszy.

POST /imports/ -> 206
GET  /imports/1 -> 200
GET  /imports/1/errors -> 200 -> Has a list of errors

Ten sam wzór można zastosować do wielu operacji wsadowych

POST /operations/ -> 206
GET  /operations/1 -> 200
GET  /operations/1/errors -> 200 - > Has a list of errors.

Kod obsługujący test POST musi jedynie zweryfikować, czy format danych operacji jest prawidłowy. W późniejszym czasie operacje mogą zostać wykonane. Na przykład w tle pracownika, dzięki czemu można łatwiej skalować. Następnie możesz sprawdzić status operacji, kiedy tylko chcesz. Możesz użyć odpytywania lub oddzwaniania, strumieni lub czegokolwiek, aby zaspokoić potrzebę wiedzieć, kiedy zestaw operacji zostanie zakończony.

Coteyr
źródło
2

Jest tu już wiele dobrych odpowiedzi, ale brakuje jednego aspektu:

Jakiej umowy oczekują Twoi klienci?

Kody zwrotne HTTP powinny wskazywać przynajmniej rozróżnienie sukces / porażka, a zatem odgrywać rolę „wyjątków biedaka”. Wtedy 200 oznacza „umowa całkowicie spełniona”, a 4xx lub 5xx wskazują na niedotrzymanie umowy.

Naiwnie oczekiwałbym, że kontrakt z twoją prośbą o wiele działań będzie „wykonywał wszystkie moje zadania”, a jeśli jedno z nich się nie powiedzie, wtedy prośba nie była (całkowicie) pomyślna. Zazwyczaj jako klient rozumiem 200 jako „wszystko w porządku”, a kody z rodziny 400 i 500 zmuszają mnie do myślenia o konsekwencjach (częściowej) awarii. Tak więc użyj 200 dla „wszystkich wykonanych zadań” i 500 plus opisowa odpowiedź w przypadku częściowej awarii.

Inną hipotetyczną umową może być „spróbuj wykonać wszystkie czynności”. To jest całkowicie zgodne z umową, jeśli (niektóre) działania zawiodą. Zawsze zwracasz 200 plus dokument wyników, w którym znajdziesz informacje o sukcesie / porażce dla poszczególnych zadań.

Więc jaki kontrakt chcesz przestrzegać? Oba są ważne, ale pierwszy (200 tylko w przypadku, gdy wszystko zostało zrobione) jest dla mnie bardziej intuicyjny i lepiej zgodny z typowymi wzorcami oprogramowania. A w (miejmy nadzieję) większości przypadków, w których usługa wykonała wszystkie zadania, klient może to łatwo wykryć.

Ostatni ważny aspekt: ​​w jaki sposób przekazujesz klientom decyzję dotyczącą umowy? Np. W Javie użyłbym nazw metod takich jak „doAll ()” lub „tryToDoAll ()”. W HTTP można odpowiednio nazwać adresy URL punktów końcowych, mając nadzieję, że programiści klienta zobaczą, przeczytają i zrozumieją nazewnictwo (nie postawiłbym na to). Jeszcze jeden powód, aby wybrać umowę najmniej zaskoczenia.

Ralf Kleberhoff
źródło
0

Odpowiedź:

Wystarczy użyć jednego żądania na akcję i zaakceptować dodatkowe koszty ogólne.

Kod stanu opisuje status jednej operacji. Dlatego sensowne jest posiadanie jednej operacji na żądanie.

Wiele niezależnych operacji łamie zasadę, na której oparty jest model żądanie-odpowiedź i kody statusu. Walczysz z naturą.

HTTP / 1.1 i HTTP / 2 znacznie zmniejszyły obciążenie żądaniami HTTP. Szacuję, że jest bardzo niewiele sytuacji, w których zalecane jest grupowanie niezależnych żądań.


To mówi,

(1) Możesz dokonać wielu modyfikacji za pomocą żądania PATCH ( RFC 5789 ). Wymaga to jednak, aby zmiany nie były niezależne; są stosowane atomowo (wszystko lub nic).

(2) Inni zwrócili uwagę na kod Multi-Status 207. Jest to jednak zdefiniowane tylko dla WebDAV ( RFC 4918 ), rozszerzenia HTTP.

Kod stanu 207 (Multi-Status) zapewnia status wielu niezależnych operacji (więcej informacji znajduje się w rozdziale 13).

...

Odpowiedź na wiele statusów przekazuje informacje o wielu zasobach w sytuacjach, w których może być odpowiednie wiele kodów statusu. Element główny [XML] „wielostanowiskowy” zawiera zero lub więcej elementów „odpowiedzi” w dowolnej kolejności, z których każdy zawiera informacje o pojedynczym zasobie.

Odpowiedź XML WebDAV 207 byłaby tak dziwna jak kaczka w interfejsie API innym niż WebDAV. Nie rób tego

Paul Draper
źródło
1
Zasadniczo twierdzisz, że @Anders ma problem XY . Być może masz rację, ale niestety oznacza to, że tak naprawdę nie odpowiedziałeś na zadane przez niego pytanie (jakiego kodu stanu użyć do żądania wielu działań).
Azuaron,
2
@Azuaron, jaki pas najlepiej nadaje się do bicia dzieci? Myślę, że „nie dotyczy” jest dopuszczalną odpowiedzią. Poza tym Andres zamieścił wiele wniosków na swojej liście pomysłów. Z całego serca poparłem tę opcję.
Paul Draper,
Jakoś tęskniłem za tym, że to wymienił. W takim razie twierdzę, że to głupie pytanie, Wysoki Sądzie!
Azuaron,
1
@Azuaron Myślę, że to jest poprawna odpowiedź. Jeśli robię to wszystko źle, chcę, żeby ktoś to powiedział i nie dał mi instrukcji, jak najlepiej zjechać z klifu.
Anders
1
Nic nie zabrania wysyłania JSON w odpowiedzi 207, pod warunkiem, że nagłówek Content-Type jest odpowiednio ustawiony i odpowiada żądaniom klienta (nagłówek Accept).
dolmen
0

Jeśli naprawdę potrzebujesz wielu akcji w jednym żądaniu, dlaczego nie zawinąć wszystkich akcji w transakcji w backend? W ten sposób albo wszyscy odnoszą sukces, albo wszyscy zawodzą.

Jako klient korzystający z interfejsu API mogę sobie poradzić z całkowitym powodzeniem lub niepowodzeniem wywołania interfejsu API. Trudno poradzić sobie z częściowym sukcesem, ponieważ musiałbym poradzić sobie ze wszystkimi możliwymi stanami wynikowymi.

Dziekan
źródło
2
Zakładam, że gdyby prośba była atomowa, nie zadałby tego pytania.
Andy
@Andy Może, ale nie możesz założyć, że wziął pod uwagę wszystkie implikacje takiego projektu.
Dziekan
Żądanie nie powinno być atomowe - np. Jeśli błąd 2 nie powiedzie się, zmiany wprowadzone przez numer 1 powinny nadal obowiązywać. Zatem zawijanie wszystkiego w jedną transakcję nie jest opcją.
Anders