Какой код статуса HTTP нужно вернуть, если несколько действий заканчиваются разными состояниями?

72

Я создаю API, где пользователь может попросить сервер выполнить несколько действий в одном HTTP-запросе. Результат возвращается в виде массива JSON с одной записью на действие.

Каждое из этих действий может быть неудачным или успешным независимо друг от друга. Например, первое действие может быть выполнено успешно, входные данные для второго действия могут быть плохо отформатированы и не могут быть проверены, а третье действие может вызвать непредвиденную ошибку.

Если бы был один запрос на действие, я бы вернул коды состояния 200, 422 и 500 соответственно. Но теперь, когда есть только один запрос, какой код статуса я должен вернуть?

Некоторые варианты:

  • Всегда возвращайте 200, и дайте более подробную информацию в теле.
  • Может быть, следовать приведенному выше правилу, только если в запросе более одного действия?
  • Может быть, вернуть 200, если все запросы выполнены успешно, в противном случае 500 (или какой-то другой код)?
  • Просто используйте один запрос на действие и принимайте дополнительные издержки.
  • Что-то совершенно другое?
Андерс
источник
3
Ваш вопрос заставил меня задуматься о другом: programmers.stackexchange.com/questions/309147/…
AilurusFulgens
7
Немного связано также: programmers.stackexchange.com/questions/305250/… (см. Принятый ответ о разделении между кодами состояния HTTP и кодами приложения)
4
Какое преимущество вы получаете, группируя эти запросы вместе? Речь идет о бизнес-логике, как транзакция над несколькими ресурсами, или о производительности? Или что-то другое?
Люк Франкен
5
Хорошо, в этом случае я бы настоятельно рекомендовал улучшить эту производительность в других областях. Попробуйте такие вещи, как оптимистичный пользовательский интерфейс, пакетные запросы, кэширование и т. Д., Прежде чем внедрять эту сложность в свой бизнес-уровень. У вас есть четкое представление о том, где вы теряете больше всего времени?
Люк Франкен
4
... не надейтесь, что люди будут правильно смотреть на эти статусы. Большинство программ проверяют только наиболее распространенные из них и терпят неудачу или плохо себя ведут, если получают неожиданный код состояния. (Я помню, что на DefCon была также презентация о защите вашего сайта от сканеров путем отправки произвольных статусов выхода, которые браузер игнорирует и просто показывает, почему сканеры иногда принимают за ошибки и, таким образом, прекращают сканирование этой части вашего сайта).
Бакуриу

Ответы:

21

Короткий прямой ответ

Поскольку запрос говорит о выполнении списка задач (задачи - это ресурс, о котором мы здесь говорим), то если группа задач была перенесена на выполнение (то есть независимо от результата выполнения), то это было бы разумно что статус ответа будет 200 OK. В противном случае, если возникла проблема, которая помешала бы выполнению группы задач, например, не удалось проверить объекты задачи , или какая-либо необходимая служба, например, недоступна, то статус ответа должен обозначать эту ошибку. В прошлом, когда начинается выполнение задач, учитывая, что задачи для выполнения перечислены в теле запроса, я бы ожидал, что результаты выполнения будут перечислены в теле ответа.


Длинный философский ответ

Вы столкнулись с этой дилеммой, потому что вы отклоняетесь от того, для чего был разработан HTTP. Вы не взаимодействуете с ним для управления ресурсами, скорее вы используете его как средство удаленного вызова метода (что не очень странно, но плохо работает без заранее заданной схемы).

С учетом вышесказанного и без смелости превратить этот ответ в подробное руководство, ниже приводится схема URI, которая соответствует подходу управления ресурсами:

  • /tasks
    • GET перечисляет все задачи, разбитые на страницы
    • POST добавляет одну задачу
  • /tasks/task/[id]
    • GET отвечает объектом состояния одной задачи
    • DELETE отменяет / удаляет задачу
  • /tasks/groups
    • GET перечисляет все группы задач, разбитые на страницы
    • POST добавляет группу задач
  • /tasks/groups/group/[id]
    • GET отвечает с состоянием целевой группы
    • DELETE отменяет / удаляет группу задач

Эта структура говорит о ресурсах, а не о том, что с ними делать. То, что делается с ресурсами, является заботой другой службы.

Еще один важный момент: желательно, чтобы блок обработчика HTTP-запросов не блокировался слишком долго. Как и в случае с пользовательским интерфейсом, интерфейс HTTP должен быть отзывчивым - в масштабе времени, который на несколько порядков медленнее (потому что этот уровень имеет дело с IO).

Переход к разработке HTTP-интерфейса, который строго управляет ресурсами, вероятно, столь же труден, как и перемещение кнопки из потока пользовательского интерфейса при нажатии кнопки. Требуется, чтобы HTTP-сервер связывался с другими службами для выполнения задач, а не их выполнения в обработчике запросов. Это не поверхностная реализация, это изменение направления.


Некоторые примеры того, как будет использоваться такая схема URI

Выполнение одной задачи и отслеживание прогресса:

  • POST /tasks с задачей выполнить
    • GET /tasks/task/[id]пока объект ответа не будет completedиметь положительного значения при отображении текущего состояния / прогресса

Выполнение одной задачи и ожидание ее завершения:

  • POST /tasks с задачей выполнить
    • GET /tasks/task/[id]?awaitCompletion=trueпока не completedимеет положительного значения (вероятно, имеет тайм-аут, поэтому это должно быть зациклено)

Выполнение группы задач и отслеживание прогресса:

  • POST /tasks/groups с группой задач для выполнения
    • GET /tasks/groups/group/[groupId]пока completedсвойство объекта ответа не будет иметь значение, показывая статус отдельной задачи (например, 3 задачи выполнены из 5)

Запрос выполнения для группы задач и ожидание его завершения:

  • POST /tasks/groups с группой задач для выполнения
    • GET /tasks/groups/group/[groupId]?awaitCompletion=true пока не ответит с результатом, который обозначает завершение (вероятно, имеет тайм-аут, поэтому должен быть зациклен)
Бен Баркай
источник
Я думаю, что разговор о том, что имеет смысл семантически, является правильным способом приблизиться к этому Спасибо!
Андерс
2
Я собирался предложить этот ответ, если его там еще не было. Это невозможно сделать несколько запросов в одном запросе HTTP. С другой стороны, вполне возможно сделать один HTTP-запрос, который говорит: «выполните следующие действия и дайте мне знать, каковы результаты». И это то, что здесь происходит.
Мартин Кочански
Я приму этот ответ, даже если он далек от большинства голосов. Хотя другие ответы тоже хороши, я думаю, что это единственный из соображений семантики HTTP.
Андерс
87

Мой голос состоял бы в том, чтобы разделить эти задачи на отдельные запросы. Однако, если слишком много поездок туда и обратно, я столкнулся с HTTP-кодом ответа 207 - Multi-Status

Скопируйте / вставьте из этой ссылки:

Ответ о нескольких состояниях передает информацию о нескольких ресурсах в ситуациях, когда может потребоваться несколько кодов состояния. Тело ответа Multi-Status по умолчанию представляет собой текстовый / xml или application / xml HTTP-объект с корневым элементом 'multistatus'. Другие элементы содержат коды состояния серий 200, 300, 400 и 500, сгенерированные во время вызова метода. Коды состояния серии 100 НЕ ДОЛЖНЫ записываться в элементе XML «ответ».

Хотя в качестве общего кода состояния ответа используется «207», получателю необходимо просмотреть содержимое тела ответа о множественном статусе для получения дополнительной информации об успехе или неудаче выполнения метода. Ответ МОЖЕТ быть использован в случае успеха, частичного успеха, а также в ситуациях неудачи.

neilsimp1
источник
22
207кажется, что ОП хочет, но я действительно хочу подчеркнуть, что, вероятно, плохая идея иметь такой подход с несколькими запросами в одном. Если проблема заключается в производительности, то вы должны разрабатывать горизонтальные масштабируемые системы в облачном стиле (в этом превосходны системы на основе HTTP)
восстановите Monica
44
@DavidGrinberg Я не мог не согласиться больше. Если отдельные действия дешевы, то накладные расходы на обработку запроса могут быть намного более дорогостоящими, чем само действие. Ваше предложение может привести к сценариям, когда обновление нескольких строк в базе данных выполняется с использованием отдельной транзакции для каждой строки, поскольку каждая строка отправляется как отдельный запрос. Это не только ужасно неэффективно, но и означает, что невозможно будет атомарно обновить несколько строк, если это необходимо. Горизонтальное масштабирование важно, но оно не является заменой для эффективных проектов.
Касперд
4
Хорошо сказано и указывает на типичную проблему реализации REST API, которую делают люди, неосведомленные о реалиях бизнес-потребностей, таких как производительность и / или атомарность. Вот почему, например, спецификация OData REST имеет пакетный формат для нескольких операций в одном вызове - в этом есть реальная необходимость.
TomTom
8
@TomTom, ОП не хочет атомарности. Это было бы гораздо проще спроектировать, поскольку существует только один статус атомарной операции. Кроме того, спецификация HTTP допускает пакетные операции для повышения производительности через мультиплексирование HTTP / 2 (естественно, поддержка HTTP / 2 - это другой вопрос, но спецификация позволяет это).
Пол Дрейпер
2
@ Давид Работая над некоторыми проблемами HPC в прошлом, по моему опыту, стоимость отправки одного байта почти такая же, как и отправка тысячи (разные носители передачи, конечно, имеют разные накладные расходы, но редко бывают лучше, чем эта). Так что, если производительность является проблемой, я не вижу, как отправка нескольких запросов не будет иметь больших накладных расходов. Теперь, если бы вы могли мультиплексировать несколько запросов по одному и тому же соединению, эта проблема исчезла бы, но, насколько я понимаю, это только вариант с HTTP / 2, и его поддержка довольно ограничена. Или я что-то упустил?
Во
24

Несмотря на то, что мульти-статус является опцией, я бы вернул 200 (все хорошо), если все запросы были выполнены успешно, и ошибку (500 или, может быть, 207) в противном случае.

Стандартный случай обычно должен быть 200 - все работает. И клиенты должны только проверить это. И только в случае ошибки вы можете вернуть 500 (или 207). Я думаю, что 207 - правильный выбор в случае, по крайней мере, одной ошибки, но если вы видите весь пакет как одну транзакцию, вы также можете отправить 500. - Клиент захочет интерпретировать сообщение об ошибке в любом случае.

Почему не всегда отправлять 207? - Потому что стандартные случаи должны быть простыми и стандартными. Хотя исключительные случаи могут быть исключительными. Клиент должен только прочитать тело ответа и принять дальнейшие сложные решения, если этого требует исключительная ситуация.

Falco
источник
6
Я не совсем согласен. Если подзапрос 1 и 3 завершился успешно, вы получаете объединенный ресурс и все равно должны проверить объединенный ответ. Вам просто нужно рассмотреть еще один случай. Если ответ = 200 или ответ 1 = 200, то запрос 1 выполнен успешно. Если ответ = 200 или ответ 2 = 200, тогда запрос 2 будет успешным и т. Д., А не просто проверять подответ.
gnasher729
1
@ gnasher729 это действительно зависит от приложения. Я представляю управляемое пользователем действие, которое просто перейдет к следующему шагу с (все в порядке), когда все запросы будут выполнены успешно. - Если что-то пошло не так (глобальное состояние <= 200), вам нужно отобразить подробные ошибки и изменить рабочий процесс, и вам потребуется только одна проверка для каждого подзапроса, поскольку вы находитесь в функции «handleMixedState», а не в функции «handleAllOk» ,
Falco
Это действительно зависит от того, что это значит. Например, у меня есть конечная точка, которая контролирует торговые стратегии. Вы можете «запустить» список идентификаторов за один прогон. Возврат 200 означает, что операция (обработать их) прошла успешно, но не все могут начать успешно. Что, кстати, даже не видно в немедленном результате (который будет запускаться), так как запуск может занять несколько секунд. Семантика в многооперационных вызовах зависит от сценария.
TomTom
Я бы также, скорее всего, отправил бы 500, если бы была общая проблема (например, не работает база данных), чтобы сервер даже не пробовал отдельные запросы, а мог просто вернуть общую ошибку. - Поскольку для пользователя есть 3 разных результата 1. все в порядке, 2. общие проблемы, ничего не работает, 3. некоторые запросы не выполнены. -> Что обычно приводит к совершенно другому программному потоку.
Фалько
1
Итак, один из подходов: 207 = индивидуальный статус для каждого запроса. Все остальное: возвращенный статус применяется к каждому запросу. Имеет смысл для 200, 401, ≥ 500.
gnasher729
13

Один из вариантов - всегда возвращать код состояния 200, а затем возвращать конкретные ошибки в теле документа JSON. Именно так разработаны некоторые API (они всегда возвращают код состояния 200 и отправляют сообщение об ошибке в теле). Для получения дополнительной информации о различных подходах см. Http://archive.oreilly.com/pub/post/restful_error_handling.html.

BigONotation
источник
2
В этом случае мне нравится идея использовать, 200чтобы указать, что все хорошо, запрос получен и был действителен , а затем использовать JSON, чтобы предоставить подробности о том, что произошло в этом запросе (то есть, результат транзакций).
Риккнаги
4

Я думаю, что neilsimp1 верен, но я бы порекомендовал изменить структуру отправляемых данных таким образом, чтобы вы могли отправить их 206 - Acceptedи обработать позже. Возможно с обратными вызовами.

Проблема с попыткой отправить несколько действий в одном запросе заключается именно в том, что каждое действие должно иметь свой собственный «статус»

Глядя на импорт CSV (я не знаю, что такое OP, но это простая версия). ПОСТАВЬТЕ CSV и верните 206. Затем позже CSV можно будет импортировать, и вы можете получить статус импорта с помощью GET (200) для URL, который отображается для каждой строки ошибок.

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

Этот же шаблон может быть применен ко многим групповым операциям

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

Код, который обрабатывает POST, должен только проверить, что формат данных операций является допустимым. Затем в более позднее время операции могут быть выполнены. Например, на заднем плане, так что вы можете легче масштабировать. Затем вы можете проверить статус операций, когда захотите. Вы можете использовать опросы или обратные вызовы, или потоки или что-то еще, чтобы удовлетворить потребность знать, когда набор операций завершен.

coteyr
источник
2

Здесь уже много хороших ответов, но отсутствует один аспект:

Какой контракт ожидают ваши клиенты?

Коды возврата HTTP должны указывать, по крайней мере, различие успеха / неудачи и, таким образом, играть роль «исключений для бедняков». Тогда 200 означает «контракт полностью выполнен», а 4xx или 5xx означают невыполнение.

Наивно, я бы ожидал, что контракт вашего запроса с несколькими действиями будет «выполнять все мои задачи», и если одно из них не будет выполнено, то запрос не будет (полностью) успешным. Как правило, как клиент я понимаю 200 как «все в порядке», а коды из семейства 400 и 500 заставляют меня задуматься о последствиях (частичного) сбоя. Таким образом, используйте 200 для «всех выполненных задач» и 500 плюс описательный ответ в случае частичного сбоя.

Другой гипотетический контракт может быть «попытаться выполнить все действия». Тогда это полностью соответствует контракту, если (некоторые из) действий не удаются. Таким образом, вы всегда будете возвращать 200 плюс документ с результатами, где вы найдете информацию об успехе / неудаче для отдельных задач.

Итак, каким контрактом вы хотите следовать? Оба действительны, но первый (200 только в том случае, если все было сделано) для меня более интуитивен и лучше соответствует типичным шаблонам программного обеспечения. И для (надеюсь) большинства случаев, когда служба выполняла все задачи, клиенту легко обнаружить этот случай.

Последний важный аспект: как вы сообщаете своим клиентам о своем контрактном решении? Например, в Java я бы использовал имена методов, такие как "doAll ()" или "tryToDoAll ()". В HTTP вы можете соответствующим образом называть URL-адреса конечных точек, надеясь, что ваши клиентские разработчики увидят, прочитают и поймут названия (я бы не стал на это ставить). Еще одна причина, чтобы выбрать контракт наименее неожиданным.

Ральф Клеберхофф
источник
0

Ответ:

Просто используйте один запрос на действие и принимайте дополнительные издержки.

Код состояния описывает состояние одной операции. Следовательно, имеет смысл иметь одну операцию на запрос.

Несколько независимых операций нарушают принцип, на котором основаны модель запроса-ответа и коды состояния. Вы боретесь с природой.

HTTP / 1.1 и HTTP / 2 значительно снизили количество HTTP-запросов. Я полагаю, что очень мало ситуаций, когда рекомендуется группировать независимые запросы.


Это сказало,

(1) Вы можете сделать несколько модификаций с помощью запроса PATCH ( RFC 5789 ). Однако для этого необходимо, чтобы изменения не были независимыми; они применяются атомарно (все или ничего).

(2) Другие указали на код 207 Multi-Status. Однако это определено только для WebDAV ( RFC 4918 ), расширения HTTP.

Код состояния 207 (Multi-Status) предоставляет статус для нескольких независимых операций (см. Раздел 13 для получения дополнительной информации).

...

Ответ о нескольких состояниях передает информацию о нескольких ресурсах в ситуациях, когда может потребоваться несколько кодов состояния. Корневой [XML] элемент 'multistatus' содержит ноль или более элементов 'response' в любом порядке, каждый из которых содержит информацию об отдельном ресурсе.

207 XML-ответ WebDAV будет таким же странным, как утка в не-WebDAV API. Не делай этого.

Пол Дрэйпер
источник
1
По сути, вы утверждаете, что у @Anders есть проблема XY . Возможно, вы правы, но, к сожалению, это означает, что вы на самом деле не ответили на заданный им вопрос (какой код состояния использовать для многозадачного запроса).
Азуарон
2
@Azuaron, какой ремень лучше всего подходит для избиения детей? Я думаю, что «N / A» является допустимым ответом. Кроме того, Андрес включил несколько запросов в свой список идей. Я искренне поддержал этот вариант.
Пол Дрейпер,
Я как-то упустил, что он это перечислил. В таком случае, я утверждаю, что это глупый вопрос, Ваша Честь!
Азуарон
1
@Azuaron Я абсолютно думаю, что это правильный ответ. Если я все делаю неправильно, я хочу, чтобы кто-то так сказал, и не давал мне инструкций о том, как лучше всего сойти с обрыва.
Андерс
1
Ничто не запрещает отправлять JSON в ответе 207, если заголовок Content-Type установлен правильно и совпадает с запросом клиента (заголовок Accept).
дольмен
0

Если вам действительно нужно иметь несколько действий в одном запросе, почему бы не обернуть все действия в транзакции в бэкэнд? Таким образом, они либо все преуспевают, либо все проваливаются

Как клиент, использующий API, я могу иметь дело с полным успехом или неудачей при вызове API. С частичным успехом трудно иметь дело, так как мне пришлось бы обрабатывать все возможные результирующие состояния.

декан
источник
2
Я предполагаю, что если бы запрос был атомарным, он бы не разместил этот вопрос.
Энди
@ Энди Может быть, но ты не можешь предположить, что он обдумал все последствия такого дизайна.
Дин
Запрос не должен быть атомарным - например, если # 2 терпит неудачу, изменения, сделанные # 1, должны все еще сохраняться. Таким образом, упаковка всего в одну транзакцию не вариант.
Андерс