Pull to refresh

Comments 195

Странная все же у автора манера. Он говорит — делай так, а не вот так. Ни объяснения, ни доводов. Ничего.
Здесь представлены best practices, собранные на основе моего опыта и обсуждения с друзьями, которые работали над приложениями веб-служб REST.
А по моему объяснено)

Это не объяснение, это "верьте мне". Объяснение — это "мы пробовали сделать вот так, и получилось вот такое" или "мы не делали по этой практике, и получилось вот такое".

Ну с REST тут, такое. Никто не мешает и в единственном числе и с глаголами, работать будет, но большинство все таки склоняется что это не по понятиям) Поэтому объяснения только два, так нравится и все так делают. От манеры автора тут вряд ли что зависело.

Ни "мне так нравится", ни "все так делают" — не критерий best practice.

Ну если вы найдете аналогичную статью, где есть объективные объяснения таких моментов по REST, поделитесь ссылкой. Прочту с интересом.

https://habrahabr.ru/company/mailru/blog/345184/


Все эти сурьёзные принципы — как споры между сторонниками Big Endian и Little Endian, философы часами переубеждают друг друга, но всё это «правильное делание» имеет очень мало отношения к реальным проблемам.
Зарегистрировался, но скачать или посмотреть этот документ все равно не дают. А посмотреть его все еще хочется.
Looks like you don't have permission to access this project or the page does not exist.

Не-а, но вообще спасибо за упоминание. Найду через кого скачать.

8) забыли про 500х коды. И вообще про коды тут пункт ниочём. список HTTP статусов можно и на википедии посмотреть.
Мой бестпрактис касательно кодов: В самой первой версии апи сделайте поддержку 4х статусов


  • 200 — ок
  • 400 — неправильный запрос
  • 404 — не найдено результатов
  • 500 — внутренняя ошибка сервера
    Этого достаточно на первое время, а если у вас есть бюджет вы всегда сможете расширить этот список. Главное — поддержать хотя бы их.

5) пагинация через link — имхо это даже бэд практис в некоторых случаях. Если мы предоставляем limit/offset (skip/size и т.п.) — то этого более чем достаточно для пагинации, и создание линков усложняет код, не привнося никакого дополнительного функционала (ведь пагинация и так есть). А вот отказываться от limit/offset в пользу линков — плохая идея. мы заведомо ограничиваем возможности клиента, не привнося ничего взамен.


7) А что за путаница у него в методах и их индепендности? и счего бы вдруг GET был индепендным? запросив одну и ту же книгу с разницей в несколько секунд, у неё может измениться статус или рейтинг. Но это уже мелочь

Если уж делать совсем-совсем REST, то пагинацию надо делать через Range :-)
7) А что за путаница у него в методах и их индепендности?

Идемпотентности, а не индепендности. А путаница вызвана тем, что в статье дано некорректное определение:


Идемпотентность: когда вы получаете один и тот же ответ, сколько раз вы вызываете один и тот же ресурс, он известен как идемпотентный.

Корректное можно найти в википедии, и оно совсем про другое:


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

Между выделенными жирным определениями есть принципиальная разница. При повторе запроса ответы могут отличаться. Один пример: упомянутое в википедии изменение (не из-за выполнения самого GET) запрашиваемого GET-ом ресурса между запросами. Другой пример: выполнение DELETE объекта с неким id — первый запрос может вернуть 204 No Content а последующие 404 Not Found, но при этом многократное удаление не отличается от однократного в том смысле, что в обоих случаях результат одинаковый — объект с этим id более не существует.


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

ого. большое спасибо! Ваш комментарий был для меня на порядок полезнее статьи (:
Идемпотентность: когда вы получаете один и тот же ответ, сколько раз вы вызываете один и тот же ресурс

Идемпотентная операция в информатике — действие, многократное повторение которого эквивалентно однократному.

Четное слово, не понимаю разницу между этими двумя определениями в контексте REST, можете пожалуйста объяснить?

[EDIT]
Перечитал ваш комментарий, вопрос снимается.
404 — не найдено результатов


Не найдено результатов это какой-то другой код. Либо, к примеру, просто пустой массив.
404 это именно про ненайденный ресурс.
Насчет кодов. А что уважаемые господа думают о следующей схеме:
Например для GET — если сам запрос прошел удачно, но оибку в бизнес логике, возврещается HTTP код 200 и документ с полем ошибка, описанием ошибки и кодом ошибки (код ошибки не HTTP, а самой системы).
Иначе получается путаница транспортного уровня (HTTP) и уровня бизнес логики.
Из русской вики:
Коды 5xx выделены под случаи неудачного выполнения операции по вине сервера.
Ошибка бизнесс-логики — это ошибка сервера. То что вы её аккуратно отлавливаете — не значит что она не происходила.

Или 422, например если валидация не прошла.

Нет, это 4хх ошибки типа bad request, invalid argument, not found

… и клиенту нужно каждый раз парсить содержимое поле "ошибка". Спасибо, нет.

Лучше проверять Errors!=null, чем заворачивать условный HttpClient в трайкэтчи и проверять все варианты ошибок, расписывать на каждую свое действие и так же парсить поле «ошибка»плюс принудительно отключать эксепшены на неуспешные ответы и каждый раз проверять IsSuccess. Вы просто с одной стороны баррикады рассматриваете, видимо. Со стороны клиента во много раз удобнее получать всегда один и тот же ответ и просто проверять в нем одно поле.
чем заворачивать условный HttpClient в трайкэтчи

А зачем это делать, если не секрет?


Со стороны клиента во много раз удобнее получать всегда один и тот же ответ и просто проверять в нем одно поле.

Знаете, я регулярно пишу и SOAP и REST-клиенты. Так что нет, лично мне намного удобнее, когда бросят эксепшн при неудачном логине, чем если я забуду проверить, что логин удачный, и получу ошибку уже потом, при запросе.

А зачем это делать, если не секрет?

Не секрет — "проверять все варианты ошибок"


лично мне намного удобнее, когда бросят эксепшн при неудачном логине

Только если вам нет нужды объяснять user'у перед экраном, что же, собственно, произошло нехорошего на той стороне интернета, из-за чего самый важный и срочный запрос в его жизни не проходит. Причем в максимально удобной и понятной для него форме, чтобы он самостоятельно вырулил из сложившейся ситуации и по-возможности минимально вынес мозг call-центру. А так — да, для разговоров "server-to-server" достаточно в лог скинуть сообщение и стектрейс.

Не секрет — "проверять все варианты ошибок"

Так для этого try..catch не нужен, достаточно обработать код возврата.


Только если вам нет нужды объяснять user'у перед экраном [...] А так — да, для разговоров "server-to-server" достаточно в лог скинуть сообщение и стектрейс.

Ну так я и пишу преимущественно unattended-клиенты, и планирую сервера для таких. Другое дело, что использование HTTP-статусов для ошибок позволяет мне выбирать из этих двух способов.

Так для этого try..catch не нужен, достаточно обработать код возврата.
© lair
… и клиенту нужно каждый раз парсить содержимое поле "ошибка". Спасибо, нет.
© lair

Надеюсь, что "код возврата" это не "содержимое поля 'ошибка'" ;)

Правильно надеетесь. Я про HTTP Status. В том же .net у вас есть три варианта:


HttpResponseMessage response;

// 1
response.EnsureSuccessStatusCode();

//2 
if (response.IsSuccessStatusCode()) 

//3
switch (response.StatusCode)

Выбор между ними зависит от стратегии клиента.

Т.е., "код возврата" — это HTTP Status, в котором не предусмотрены ошибки бизнес-логики. Я правильно понимаю, что вы, как клиент, предпочитаете в случае ошибки на уровне бизнес-логики (например, "клиент с таким ИНН уже есть") получить ответ от сервера со статусом 200?

Нет, неправильно. Я предпочитаю получить от сервера релевантный HTTP Status (в зависимости от того, что именно пошло не так), а дополнительную информацию — в теле ответа. Соответственно, ошибка "вы шлете нам невалидный ИНН" будет выражаться либо в 400, либо в 422 (в зависимости от нашего занудства), а уже в теле будет написано "дублирующийся ИНН", или INN: "duplicate", или в меру нашего извращения.

и клиенту нужно каждый раз парсить содержимое поле "ошибка". Спасибо, нет.
© liar

а уже в теле будет написано "дублирующийся ИНН", или INN: "duplicate", или в меру нашего извращения.
© liar

улыбнуло.

… а что вас "улыбнуло"-то? В этой схеме для того, чтобы понять, что запрос невалиден, не надо парсить тело ответа.

А тело ответа — оно зачем в таком случае? Либо его тоже нужно парсить, и тогда ваше "Спасибо, нет" неуместно, либо его парсить не нужно и тогда, согласно старику Оккаму, неуместно само тело. Вот это и улыбнуло.

А тело ответа — оно зачем в таком случае?

Для тех ситуаций, когда нам нужна дополнительная информация об ошибке (а не только сам факт, что она произошла).

но оибку в бизнес логике, возврещается HTTP код 200 и документ с полем ошибка, описанием ошибки и кодом ошибки (код ошибки не HTTP, а самой системы).

Вот за то коллега evgenyk и говорил. Если ошибка бизнес-логики, значит пользователь что-то сделал не так, как предусматривалось разработчиками, и ему нужна дополнительная информация, чтобы он сам мог разрулить ситуацию (если она разруливаемая), которая через состояния HTTP-статуса не передается. Очень редко, когда пользователь остается доволен, видя на экране надпись "Ошибка 500. Обратитесь в службу поддержки".


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

Если ошибка бизнес-логики, значит пользователь что-то сделал не так,

Совершенно не обязательно. Собственно, у вас вообще может не быть человеческого пользователя.


ему нужна дополнительная информация, чтобы он сам мог разрулить ситуацию (если она разруливаемая), которая через состояния HTTP-статуса не передается

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

С точки зрения backend-разработчика человеческого пользователя нет всегда. Но это ограниченная точка зрения.


С точки зрения forntend-разработчика (в частности, разработчика SPA) человеческий пользователь есть слишком часто, чтобы его игнорировать. Разделение ошибок на 500-е (т.е., любые ошибки, не предусмотренные логикой операции, которые произошли в момент выполнения зпроса на сервере) и на ошибки бизнес-логики, зависящие от конкретной запрошенной операции (200-е с необходимостью анализа содержимого поля "ошибка") позволяют запускать на клиенте сценарии решения ошибочной с точки зрения бизнес-логики ситуации с использованием полученных с сервера данных.


Другими словами, 500-я ошибка — это всегда тупиковая ошибка. Ее обработка и на неинтерактивном клиенте и на интерактивном одинакова — логируем / показываем пользователю сообщение, что все плохо. Код ошибки в 200-м ответе сигнализирует, что выполнение запрошенной операции на сервере пошло по одному из предусмотренных разработчиками сценариев, не обеспечивающих успешного завершения в силу каких-то условий. В этом случае пользователь (интерактивный) или программа (неинтерактивный) может выполнить некоторые дополнительные действия, после чего повторить операцию.


То есть, если сценарий выполнения операции предусматривает возможность дублирования ИНН и имеются рекомендации для пользователя, что нужно сделать в данном случае — это ошибка бизнес-логики (код 200). Если на сервере при добавлении записи в БД произошла ошибка из-за того, что в таблице на поле ИНН повешен ключ уникальности, а записывались данные с повторяющимся значением ИНН, которая перехватилась и обернулась в типовое сообщение об ошибке на уровне REST framework'а, потому что разраб сервиса даже не предполагал такого варианта — это 500-я ошибка.


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


Есть мнение, что ошибки валидации входных данных не являются ошибками, как таковыми — это предусмотренный сценарий взаимодействия. Эту точку зрения можно распростанить и на ошибки бизнес-логики, предусмотренные разработчиками сервисов. Все остальное — настоящие ошибки, 500-й код, окончательный тупик.

С точки зрения backend-разработчика человеческого пользователя нет всегда. Но это ограниченная точка зрения.

Это, очевидно, неправда — это я вам говорю как backend-разработчик.


Есть мнение, что ошибки валидации входных данных не являются ошибками, как таковыми — это предусмотренный сценарий взаимодействия.

Они являются ошибками, но не являются исключениями. Я выше уже приводил пример: если во входящем сообщении нет необходимого поля — это какая ошибка (или исключение)?


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

Зато теперь все промежуточные системы потеряли возможность логировать "ошибки бизнес-логики", потому что они не знают, как их выделить из успешных ответов.


Знаете, у меня вот есть маленький проект, который интегрировался с тремя системами. Все они возвращали сообщения об ошибках с соответствующими кодами HTTP (4xx и 5xx). Когда мне понадобилась диагностика, я поставил стандартный модуль, который делает автотрассировку HTTP-вызовов, и сразу увидел статистику успешных и неуспешных вызовов (а для неуспешных — еще и тела ответов). А потом я добавил туда интеграцию со Slack, который возвращает ошибки в теле ответа с кодом 200. Мне пришлось пойти и дописать специальную обработку такого тела в диагностическую мидлварь (и теперь я вынужден, униформности ради, парсить тело дважды, тоже мило).

А универсального решения, устраивающего всех, нет. Если работаешь в разнородной среде то ради униформности чего только не приходится делать. Это вы ещё на фронт не залазили.

Если работаешь в разнородной среде то ради униформности чего только не приходится делать.

Так вот, один из критериев, упомянутых Филдингом — это униформный интерфейс. Возврат 2xx в случае, если произошла ошибка — нарушение униформности.

возврат 4xx в случае отправки клиентом корректного запроса, который сервер не может обработать из-за каких-то внутренних бизнес-правил точно нарушение униформности.

Во-первых, как вы определяете "корректный" запрос?
Во-вторых:


The 422 (Unprocessable Entity) status code means the server understands the content type of the request entity (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was unable to process the contained instructions. For example, this error condition may occur if an XML request body contains well-formed (i.e., syntactically correct), but semantically erroneous, XML instructions.
Корректный — соответствующий ожидаемой схеме, json schema, xml dtd, xml schema, просто какая-то чистая функция от http-запроса. Максимум из проверки состояния (читай обращения к базе) сервера — проверка прав, и то подвопросом в случае с jwt или аналогов с правами в запросе.

Иными словами, вы не признаете приведенное выше определение 422 кода?

именно, что признаю. 422 для велл-формед xml, но не соответствующего dtd/schema, 400 для недесериализуемого json, 422 для десериализуемого, но не соответствующего схеме. 200 со телом типа {status: «domain-error», message: «tax identifier already exists»}
422 для велл-формед xml, но не соответствующего dtd/schema,

Противоречит моей цитате. Несоответствие схеме — это не семантическая ошибка. Там не зря написано "unable to process the contained instructions". Я вам более того скажу, хороший современный модел-биндер вход, не соответствующий схеме, сразу завернет обратно с 400 просто чтобы избежать атаки.

Вот, для меня HTTP ошибка — это исключение уровня контроллера в MVC. Контроллер не смог преобразовать http запрос в вызов метод(ов) модели.

Ну то есть то, будет возвращено 4хх или 2хх, зависит исключительно от того, насколько умной вы сможете сделать валидацию до контроллера?

Скорее, настолько насколько решу сделать, где проведу грань между http и доменом, грань между тонким и толстым контроллером.

… а в контроллере-то ничего и не будет, валидация происходит раньше, в биндере.

Плюс к экзепшенам, правильно разделенных по типам. Они позволяют строить действительно структурированный код. Если ошибка при вызове метода API была на уровне транспорта (а в случае REST API это еще надо разобраться, ха-ха) — бросаем исключение технического типа и не обрабатываем его на уровне бизнес-логики.

Если у вас клиент кидает исключение, при ответе сервера статусом 500, то смените клиент или свое представление о работе http протокола
Я фулстек разработчик, с даже бОльшим опытом во фронте. так что я рассматриваю с двух сторон баррикады.

на клиенте нам важно знать получилось ли выполнить операцию или не получилось. получилось — только для статусов 200
всё остальное (и не важно, пропала сеть, неправльный урл, ошибка сервера, ошибка БЛ, да хоть апокалипсис) — это не получилось, тобишь ошибка.

Вы же предлагаете примешать некоторые ошибки к неошибкам что бы вместо одной проверки удалось/неудалось делать их кучу даже в том коде который должен выполняться когда «удалось».

вы заведомо пытаетесь усложнить код. усложнить состояние приложение. ввести ситуации когда у нас есть не только fail/success а ещё и «какбы success, но немножко fail»
всё остальное (и не важно, пропала сеть, неправльный урл, ошибка сервера, ошибка БЛ, да хоть апокалипсис) — это не получилось, тобишь ошибка.

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

так это отлично 400х 500х кодами решается


например на условный
POST: /users/123/im {text: 'hello'}
500 {ok: false, message: 'SMTP relay fail: code 1234', trace: ['/path/to','/bla/bla','/lib/smtp',...]} — ошибка кода. когда возник необработанные ексепшн


или 500: {ok: false, message: 'user is not activated yet'} — ошибка бизнесс-логики, когда произошла нештатная ситуация (отправлено сообщение пользователю, но он ещё не активирован)


или 404 {ok: false, message: 'user not found'} — ошибка запроса. пользователя с таким ID нету.


на клиенте все эти ошибки можно обработать адекватно. а можно и не обрабатывать но знать хотя бы что сообщение не доставлено.


а если мы на 2 и 3 случай будем отдавать 200, то клиент будет думать — а, всё ок сообщение доставлено. он даже не узнает об ошибке, если мы отдельно не пропишем обработку псевдо-успешных запросов

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

можно в тело ответа вставлять поле errors и при это отдавать правильный статус


а можно отдавать ошибку, но "как будто не ошибку" и на клиенте обрабатывать не только успешные и провалившиеся запросы к серверу, а ещё и "немножко успешные, но чуть-чуть провалившиеся"


Кстати нет, на беке проще отдавать всегда 200 и не париться. HTTP коды вводят для улучшения удобства работы с api

Нет, клиент проверяет поле Errors в ответе и если оно не пустое, значит произошла ошибка. Но главное, что он теперь точно знает, что ошибка это не системная и не транспортная. Получается, что неуспешный ответ может прийти только во время отладки (под контролем разработчика) или если всё сломалось. По моему опыту это значительно удобнее.

To есть то что клиент теперь не может разбираться в разных категориях ошибок — это по-вашему хорошо? «Удобнее» — возможно (хотя тоже спорно)
Если код < 400 значит ОК, если нет — то ошибка — по-моему еще более простой в реализации на клиенте подход.

Еще такой момент, объясните пожалуйста что вы понимаете под «системная» ошибка, а то создается впечатление что вы с ветряными мельницами боретесь.

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

На бэке послать 200 и кастомную ошибку обычно наоборот проще чем нормально настроить RESТ.

У вас похоже сложилось ложное впечатление что вам предлагают использовать статус коды ВМЕСТО поля errors. Это не так, их надо использовать СОВМЕСТНО.
системная ошибка — не ожидаемая реакция системы, например, запрос апи по не существующему ендпоинту или отправка неверного шэйпа тела. То есть где-то рассогласования кода или клиента, или ошибка сети, или место на диске кончилось. Такие ошибки обычно обрабатываются глобально в клиенте с сообщениями пользователю типа " обратитесь в саппорт или повторители позже ". А, например, непройденная валидация в модели на сервере — ошибка пользователя, их обычно обрабатывают локально, например подсвечивая поле формы красным.
Я попросил dmitry_dvm объяснить этот термин потому-что он разделяет понятия «транспортная» и «системная» ошибка.

В вашем же примере — в чем разница между «отправка неверного шэйпа тела» и «непройденная валидация в модели — ошибка пользователя»?
В вашем же примере — в чем разница между «отправка неверного шэйпа тела» и «непройденная валидация в модели — ошибка пользователя»?

Вот как бы да. Вот мы смотрим на тело {first_name: "John"} — как нам понять, в нем last_name нет потому, что его не ввели, или потому, что клиентское приложение его не отправило?

Не ввели — пустая строка или null

Во-первых, дефолтный сериализатор в .net из коробки это не различает: на запись вам надо будет выставить флаг, чтобы он так делал, а на чтение вам придется руками разбирать JObject, чтобы понять, было там значение, или нет. Что хуже, во-вторых, вам придется заставить сделать то же самое каждого вашего клиента — и они не скажут вам за это спасибо. В-третьих, вы только что на пустом месте увеличили накладные расходы: теперь я должен всегда передавать все свойства, даже если заполнено только одно, а представьте, что дело происходит с мобильника.


(и это еще не считая тех, гм, интересных разработчиков, которые решили, что property: null — это "запишите в свойство null`, а нет свойства — это "не трогайте свойство")

Не знаю про клиенты .net, но как по мне он должен формировать запрос в соответствии с объявленным сервером API. Если там заявлено, что last-name required & (string|null), а клиент не предоставил, то это 400/422 до, как говорят юристы, рассмотрения дела по существу. Это ошибка клиента, несоблюдение им протокола, аналог попытки вызова метода с неверной сигнатурой, а не недопустимое в данной ситуации действие пользователя. А если не объявлено обязательным, то это уже не ошибка клиента и места 4xx ошибкам нет, ответ должен быть 2xx с результатом или линком на него, а результат вполне может быть типа {status: «error», message: «last-name required in create user command» }.
Не знаю про клиенты .net, но как по мне он должен формировать запрос в соответствии с объявленным сервером API.

С этим никто не спорит. Но объявленное вами API сложно в реализации.


А если не объявлено обязательным, то это уже не ошибка клиента и места 4xx ошибкам нет, ответ должен быть 2xx

… а где, бишь, это написано, и как с этим соотносится код 422?


Более того, речь как раз о том, что оно объявлено обязательным и непустым, и это значит, что сервер не может различить ситуацию "клиент не передал" от ситуации "пользователь не ввел" — и, как следствие, всегда должен возвращать 400/422.

Как по мне, то проще. Ошибка http, полученная с сервера, для клиента в общем случае — невозобновляемая исключительная ситуация, прерывающая текущий сценарий.
Как по мне, то проще.

Речь, напоминаю, не об ошибках, а о выставленных вами требованих к JSON.

Формирование json сложная операция?

С различными значениями для Deserialize("{prop: null}").Prop и Deserialize("{}").Prop — не такая простая. Формировать просто, обратно читать сложно. В .net, под дефолтным конвертером.

Если разделять, то транспортная — отсутствие сети, прежде всего.

Отправка неверного шэйпа — например, несоответствие тела json schema — это 400 или 422. Валидация модели — с успешно пройденной валидацией на схему несоответствие каким-то бизнес-правилам, глобальным или для конкретного кейса, требующего, как правило, анализ не только тела запроса, но и текущего стейта приложения. Чёткой грани в общем случае нет, но по дефолту в моих реализациях 400 или 422 это невозможность собрать в контроллере объект типа Command или Query, или просто определить параметры для вызова метода модели. Это в архитектуре, где у контроллера основная функция служить тупым адаптером к слою приложения, преобразовывать http-сообщения.
Отправка неверного шэйпа — например, несоответствие тела json schema — это 400 или 422.

Из вашего же примера:
...«поле ОКПО обязательно для типа клиента „юрлицо“» должны приходить с 200 кодом.


На каком уровне будет эта проверка? У вас будет возможность «собрать в контроллере объект типа Command или Query» и пустить его в модель обрабатывать бизнес логику, или он сразу отвалится на уровне валидации, сказав что без нужного поля в теле запроса он не пройдет?
Это зависит от APIs и http-ендпоинта, и домена, решение времени проектирования APIs. Как правило, валидация в ендпоинте не зависит от состояния домена, с одной стороны, с другой, часто ограничена инструментами и стандартами типа OpenAPI и JSON Schema. Общее правило наверное такое для 4xx — выдавать их в ситуациях когда программный клиент (его разработчик) может проверить валидность запроса на своей стороне, но по каким-то то причинам или не проверял, или проверил, но неправильно. То, что он может проверить, если и будет отдавать ошибку http, то 5xx
Общее правило наверное такое для 4xx — выдавать их в ситуациях когда программный клиент (его разработчик) может проверить валидность запроса на своей стороне, но по каким-то то причинам или не проверял, или проверил, но неправильно.

Снова возвращаясь к вашему примеру: «поле ОКПО обязательно для типа клиента „юрлицо“» — по сути это же тот случай когда клиент не проверил на своей стороне что отправляет. И получается приходим к проблеме выбора какие ошибки отдавать для каждого поля — http или свои.
Опять же, как вы сказали, валидация скорее всего будет обрабатываться стандартными инструментами и отсекать «неправильные» запросы до начала их «полноценной» обработки.
В результате получим апи в котором часть эндпоинтов возвращает ошибки с 200 кодом, а часть с кодами хттп, и правила по которым эти ошибки различаются могут быть совершенно не очевидны тем, кто будет использовать или поддерживать это апи.

Еще можете пожалуйста объяснить почему на клиенте может быть полезно различать между 4хх и 200 ошибками в том подходе, который вы предлагаете.
4xx ошибки в этом подходе свидетельствуют о несогласованности кода клиента и сервера, клиент отправил запрос, который контроллер сервера технически не готов передать в модель для обработки, если говорить в терминах MVC при тонком контроллере. Это ошибки, сигнализирующие о том, что код клиента и сервера рассинхронизированы, они не предполагают реакции от пользователя кроме обращения в поддержку, «чистки кеша» и т. п.
Тут вариант, когда у клиента нет полной информации о том, каким должен быть запрос, например обязательность ОКПО для юрлица выставлена в конфиге/админке, не участвующих в документировании апи.

По-вашему получается, что 200-е нельзя разделить между собой, а 500-е — можно.


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


А сама проблема лежит глубже — REST смешивает транспортный и прикладной уровень, и подход "200 на все" эту проблему решает (правда, делая REST не нужным вообще).

А сама проблема лежит глубже — REST смешивает транспортный и прикладной уровень, и подход "200 на все" эту проблему решает (правда, делая REST не нужным вообще).

а к чему это тогда? используйте RPC или что вам ближе. Топик всё-же про REST. и выбирая его как протокол взаимодействия всё же стоит придерживаться рекомендаций этого стандарта.


А так да, вас никто не держит за руки. Кто-то пилит псевдо-REST через одни POST запросы, передавая {type: GET|POST|UPDATE|etc} в теле. Кто-то отдаёт 200 на всё, даже ошибки. Кто-то делает 1 ендпоинт а все параметры переносит в query аля GET: /api/users/:id/books/ -> /api?model=users&id=:id&field=books. Каждый строчит как он хочет


Но когда я начинаю работать над уже существующим проектом, я молюсь чтобы авторы легаси использовали стандарты а не своё "видение". Ибо "видение" очень часто в итоге превращается в боль

REST — архитектура, как и RPC. HTTP — протокол прикладного уровня очень сильно по спецификации близкий к принципам REST. Но это не мешает использовать его лишь как транспортный, в рамках любой архитектуры. В этом случае ошибки HTTP должны свидетельствовать лишь об ошибках транспортного уровня.
Вы пишите апи для себя или для клиента? Пробовали хоть раз на клиенте работать с тем, что сами предлагаете? Этот «стандарт» апи просто неудобен в использовании.
пробовал. и перенял этот подход, когда начал писать сам REST-сервера. ибо это удобно
4xx — накосячил клиент
5xx — накосячил сервер

Ни разу не rocket science
Где накосячил пользователь?
Смысл заключается в фиксированных классах ошибок — чтобы клиент мог правильно обработать любую, в т.ч. новую, ошибку.

Можете привести пример где ваш подход делает «чтобы клиент мог правильно обработать любую, в т.ч. новую, ошибку», а стандартный REST подход — нет.

Сначала объясните, пожалуйста, что Вы понимаете под стандартным REST-подходом. Чувствую, имеет место недопонимание. На всякий случай напоминаю, я отвечал на коммент, где предлагалось делать так:


500 {ok: false, message: 'SMTP relay fail: code 1234', trace: ['/path/to','/bla/bla','/lib/smtp',...]} — ошибка кода. когда возник необработанные ексепшн

500: {ok: false, message: 'user is not activated yet'} — ошибка бизнесс-логики
При использовании http в качестве транспорта 4xx и 5xx коды должны приходить в идеале только в случае ошибок работы клиента или сервера, ошибки бизнес-логики типа «договор закрыт», " клиент с таким ИНН уже есть", «поле ОКПО обязательно для типа клиента „юрлицо“ обязательно» должны приходить с 200 кодом.
ошибки бизнес-логики типа «договор закрыт», " клиент с таким ИНН уже есть", «поле ОКПО обязательно для типа клиента „юрлицо“ обязательно» должны приходить с 200 кодом

С каким кодом должна приходить ошибка «клиента с таким ИНН нет»?
Это ошибка бизнес логики, или клиента?

Если у клиента нет каких-то прав на выполнение запроса — это 403 или 200? Бизнес логика или транспорт?
Как по мне, то 200, в теле где-то.
С правами, пожалуй, 403, если это не что-то очень локальное типа нет права оставлять поле пустым.
То есть уже начинаются «детали» — и в определенных случаях нужно таки различать аутентификация, например — это бизнес логика, или транспорт и тому подобные моменты. В итоге получается не «на все ответ 200» — а все-таки разночтения.
Возможно для вашего случая — REST вообще не лучшее решение, и действительно удобнее использовать свою вариацию, но тогда для вас будут закрыты многие инструменты и подходы которые «окружают» эту методологию.

Эти "детали", скорее, архитектурные решения. Разночтения часто начинаются при отсутствии чёткого разделения на слои, модули и т. п. Грубо, если в терминах MVC у нас анемичные модели и ТУК, то там действительно сложно разобраться бывает куда лучше отнести ошибку.

Лучше проверять Errors!=null, чем заворачивать условный HttpClient в трайкэтчи и проверять все варианты ошибок, расписывать на каждую свое действие и так же парсить поле «ошибка»плюс принудительно отключать эксепшены на неуспешные ответы и каждый раз проверять IsSuccess

Вы о чем вообще? Проверить что статус код < 400, a потом уже точно так же обрабатывать поле error — религия не позволяет?

А с вашим подходом недолго скатиться в идиотизм в стиле `200 OK User not found`
А с вашим подходом недолго скатиться в идиотизм в стиле 200 OK User not found

Что, заметим, лично мной виденное поведение у как минимум одного API: ты, значит, шлешь запрос на логин, а тебе в ответ 200 OK, в котором {code: "Invalid login/password"}. Сколько раз я все проклинал при отладке.

А я проклинаю ваши коды в заголовках.
И что?

Использование разных методов для запросов.
Это усложняет.

Но такие как Вы видимо этого не осознают.
Вам лишь бы код клепать.

Читай документацию по API и не будешь проклинать.
Использование разных методов для запросов. Это усложняет.

Да, усложняет. Но взамен мы получаем более читаемые запросы (и более простую работу на промежуточных узлах). Каждый выбирает для себя.


Читай документацию по API и не будешь проклинать.

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

Так некоторые делают. И не обязательно мелкие конторы.

Основная проблема — ошибок в http мало, в любом случае свои структуры придумывать для более детальной информации. Но все же лучше совмещать оба подхода: использовать примерно 10 кодов ошибок, и в теле дополнительно больше информации давать.

Если же про различие ошибок бизнес-логики и транспортных, то выделите разные коды. Например, 422 — ошибки валидации (бизнес-логики), а 502 — ошибки бекенда, 504 — транспортные.
У меня в голове такая логика. HTTP — транспорт, ошибки должны быть транспортного уровня. Если запрос обработан правильно, то ИМХО на транспортном уровне должен быть код 200.
А тело ответа может быть таким (пвсеводокод):
<error_code>5238<error_code/>Красных надувных шариков нет на складе.

Этот взгляд вполне себе имеет право на жизнь. Просто в REST так не принято.

Я делаю всегда ответ 200 и ошибки в поле ошибок. Потому что раньше я писал мобильные приложения и знаю как неудобен подход с кодами. Если отдать в ответ 404, то как на клиенте понять в магазине нет соли или нет магазина?
Когда я отдаю всегда 200 это значит, что все остальные ответы относятся к сетевым или фатальным проблемам и их можно фильтровать одной пачкой, типа повторите позже. Грубо говоря все остальные коды не могут появиться в жизни, если не отвалилась сеть или не легли какие-то из сервисов.
И у меня всегда одинаковый ответ состоящий из
Data<T> и Errors[]
, чтобы в типизированных языках на клиенте не пришлось под каждый ответ пилить отдельную модель.
Если отдать в ответ 404, то как на клиенте понять в магазине нет соли или нет магазина?

А не надо в ответ на запрос "есть ли соль" отдавать 404, если ее нету — если, конечно, у вас правильно сформулирован запрос.


Но вообще, конечно, просто надо сначала прийти на корень магазина, и если там нет 404, то магазин есть. А соли — нет.


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

… и вы, конечно, не учитываете тот факт, что запросы, на которые пришли ответы из 400-ой группы, так просто повторять нельзя?

надо сначала прийти на корень магазина, и если там нет 404, то магазин есть

Может, задеплоили не ту версию магазина. В котором эндпойнт "отсыпьте-соли" отсутствует.
И вот мне в ответ на запрос "отсыпьте-соли/2кг" возвращается 404. Если это значит "соль не завезли" — ну бог с ним, бывает. А если "эндпойнт не существует" — то надо бить в набат админу и слать ошибки в логи.


Чтобы по-разному обработать ошибку транспорта и отсутствие данных в базе (ошибка приложения), придется использовать код 200 для ошибки приложения.

если ендпоинт отсутствует — нефиг отдавать по нему 404
А какой еще код можно отдать на запрос по несуществующему эндпоинту?

Это я уже запутался. Вы правы, 404 тут как раз и должен возвращаться. Можете 418 попробовать (;


А на практике у меня обычно
404 {ok: false, message: 'not found'} для отсутствия ендпоинта
и 404 {ok: false, result: null} для отсутствия соли


это меня натолкнуло на мысль что стоит подумать над отсутствием соли. вероятно я измению этот ответ в ближайшем будущем.


к слову, для правильного запроса, по которому должен возвращаться массив. например корзина. в случае отсутствия элементов у меня
200 {ok: true, result: []}

В вашем случае 404й ответ надо проверять дважды. А с моим подходом — единожды.

согласен. я думаю что мой подход стоит пересмотреть.


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

Может, задеплоили не ту версию магазина. В котором эндпойнт "отсыпьте-соли" отсутствует.

А вы по приходу на корень магазина не проверяете доступные эндпойнты? Ну так горе вам.


И вот мне в ответ на запрос "отсыпьте-соли/2кг" возвращается 404. Если это значит "соль не завезли" — ну бог с ним, бывает.

Так у вас две семантических ошибки. "2 кг" — это не идентификатор ресурса, ему нечего делать в пути; поэтому и отсутствие соли — это не "404 не найден".


Чтобы по-разному обработать ошибку транспорта и отсутствие данных в базе (ошибка приложения), придется использовать код 200 для ошибки приложения.

Что мешает использовать код 500 для ошибки приложения?

А вы по приходу на корень магазина не проверяете доступные эндпойнты

Расскажите, как вы это делаете? Я не слышал о таком подходе.


"2 кг" — это не идентификатор ресурса

Так и знал, что пример с солью меня подведет.
Ну, замените на GET /project/123 — что изменится?
Аналогично, две ситуации:


  • эндпойнт /project/ не создан в коде приложения (транспортная проблема — очень серьезно)
  • проект 123 отсутствует в базе (ошибка приложения).

Что мешает использовать код 500 для ошибки приложения?

Например, семантичность :).
Даже если забыть про семантику — у нас остаются все те же 2 ситуации:


  • База упала, ну или там out of memory — ошибка 500 на транспортном уровне, надо сообщать админу
  • Проект 123 не найден — обычно ничего делать не надо.

А зачем вы вообще пытаетесь на клиенте определить критичность ошибки для сервера?


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


Проблемы сервера, потенциальные — вроде запросов к несуществующим ресурсам, или реальные — вроде out of memory или невозможности подключиться к БД, гораздо удобнее определять на стороне сервера, и там же реализовать их мониторинг и рассылку алертов. Клиент о таких вещах думать не должен от слова совсем.

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

Проблемы с соблюдением протокола клиенту интересны только в одном, достаточно редком кейсе: когда клиент свой, он всего один, и общается со сторонним сервером, который нередко без предупреждения меняет API. В остальных случаях клиенту это не нужно (если сервер свой, то проблемы с протоколом удобнее ловить на стороне сервера, если клиентов много то получать от каждого отчёт что сервер не то вернул очень неудобно, если API сервера стабильно то реализовывать в клиенте дополнительный контроль API избыточная паранойя).

4xx ошибка — сигнал для клиента, что протокол он не соблюдает. Сторонний клиент для публичного api может, например, инициировать отправку сообщения о такой ошибке своему вендору.
Расскажите, как вы это делаете? Я не слышал о таком подходе.

По ссылкам. Пришли на /, запросили ссылки, по типу нашли отвечающую за выдачу соли, перешли туда, запросили доступные действия. HATEOAS, одна из частей имплементации REST.


Ну, замените на GET /project/123 — что изменится?

Да ничего. Надо было сначала идти на project, а потом на 123, если вам так важно знать, где проблема.


Аналогично, две ситуации: [...] очень серьезно [...] ошибка приложения

Вот вопрос только, для кого эти две ситуации реально отличаются. То бишь, с чьей стороны мы смотрим на API.


Например, семантичность

Семантически все верно: "The 500 (Internal Server Error) status code indicates that the server encountered an unexpected condition that prevented it from fulfilling the request."


База упала, ну или там out of memory — ошибка 500 на транспортном уровне, надо сообщать админу

Не надо сообщать админу, админ сам все мониторит. А вы получили свой 503 — и ждите. И, заметим, никакого 500.

Да, если использовать 500 вместо 404, проблема решается.


Про семантику кода 500 не согласен, но спорить не буду.

Да, если использовать 500 вместо 404, проблема решается.

Не надо использовать 500 вместо 404, надо использовать тот код, который наилучшим образом подходит к ситуации.

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

Не соглашусь по двум пунктам:


  1. 400 — BadRequest, указывает на то что сервер не смог понять запрос. Используется когда в эндпоинт нужен строго определенный входной параметр, а вместо него присылают что-то другое. Если сервер смог понять что ему прислали и это по структуре совпадает с тем что должно быть, то 400 быть не должно. На ошибки валидации есть код 422 — Unprocessable Entity, который как раз и означает что переданная сущность не может быть обработана (из-за ошибок)
  2. Ошибки валидации должны быть всегда, а не только во время отладки. Например, после выпуска приложения вы обновили API и ранее необязательное поле сделали обязательным (не лучший пример, но возможен). Нормальное приложение сможет это обработать и показать пользователю ошибку валидации, которую нашел сервер. Если сервер не будет присылать ошибки, то пользователь не узнает что именно произошло не так и может не выполнить свою задачу
1. Это придумал не я. BadRequest — стандартный дотнетовский (работаю с ним) ответ на неудачную валидацию модели.

2. Так в моем случае как раз приходят ошибки которые можно напрямую отдавать юзеру, потому что они осмысленные. При изменившейся валидации я верну 200, но поле Errors будет содержать новые требования, который юзер сразу увидит. А если возвращать 400, то это уже эксепшн на клиенте, который неизвестно как предвосхитить.
При изменившейся валидации я верну 200, но поле Errors будет содержать новые требования, который юзер сразу увидит.

Ну то есть вы все-таки пишете логику обработку ошибок на клиенте, даже если их не может быть.


А если возвращать 400, то это уже эксепшн на клиенте, который неизвестно как предвосхитить.

Эмм, это проблема вашего клиента, выкиньте его и возьмите нормальный. Стандартный дотнетовский HttpClient так себя не ведет.

Ну то есть вы все-таки пишете логику обработку ошибок на клиенте, даже если их не может быть.

Я предлагаю на клиенте засунуть запрос в трайкэтч и в кэтче предлагать юзеру трайэгэйн, потому что проблема точно не на клиенте. А если ответ успешен и Errors!=null то показывать его содержимое.

дотнетовский HttpClient так себя не ведет

Как это не ведет? При .EnsureSuccess… Он именно так себя и ведет. Да и на остальных, типа рестшарпа, надо вручную отключать выброс эксепшнов на неуспешных ответах.
Хорошо. Я вижу в вашем подходе на клиенте мешанину из транспортных и программных ошибок, у которых надо сортировать не только коды, но и описания. Какие плюсы у вашего подхода, кроме псевдостандартности?
Удобнее отладка в консоли браузера — сразу видно, где ошибка.
Как это не ведет? При .EnsureSuccess… Он именно так себя и ведет.

А кто вас заставляет делать EnsureSuccessStatusCode?


Какие плюсы у вашего подхода, кроме псевдостандартности?

То, что я знаю, что если сервер вернул мне 200 — тело можно парсить, и там то, что я запрашивал. И если меня интересует happy path, я могу просто проверять на ответы 200-ой группы, и считать все остальное фейлами, не тратя ресурсы на парсинг ответа. И еще то, что системы мониторинга тоже не надо парсить тело, чтобы определять статус происходящего.

Вашей системе серверного мониторинга интересны случаи, когда пользователь не заполнил обязательное поле?

Моей системе серверного мониторинга интересны случаи, когда запрос не был успешно выполнен. Почему — дело пятнадцатое.

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

Еще раз, меня волнует число неуспешных запросов, потому что они определяют успешность интеграции. Если это число (или доля, не суть) внезапно выросла, это повод пойти и постучать по голове тем, кто делает интеграцию. А уж люди у них это вводят или машина криво шлет — пусть сами разбираются.

Логичнее, по-моему, когда мониторингом интеграции занимается разработчики интеграции, нет?

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

(пропустил почему-то)


Я предлагаю на клиенте засунуть запрос в трайкэтч и в кэтче предлагать юзеру трайэгэйн, потому что проблема точно не на клиенте.

Откуда вы знаете, что она не на клиенте? Какой смысл делать еще одну попытку, если сервер вам сказал "not authorized" или, того веселее, "invalid media type"? А разделять между "мы не достучались до сервера" и "сервер сказал, что мы слишком много запросов делаем" тоже не надо?


А если ответ успешен и Errors!=null то показывать его содержимое.

… а там, значит, написано "У вас нет прав на эту операцию". И что пользователю дальше делать?

А кто вас заставляет делать EnsureSuccessStatusCode?

Никто не заставляет. Так просто удобнее отсеять сразу все транспортные проблемы.

invalid media type

Оно не может вылезти во время эксплуатации. А если вылезло, то все настолько плохо, что все равно надо дебажить.

not authorized

Это в любом случае должно обрабатываться отдельно, особенно учитывая всякие акцес и рефреш токены и что, например в OpenID Connect эта ошибка описывается вообще в хэдерах, а не в теле ответа.

Откуда вы знаете, что она не на клиенте?

Потому что все ошибки такого плана должны отлавливаться при разработке клиента и в эксплуатации не возникать.

мы слишком много запросов делаем

Это вообще идеальный пример правильности моего подхрда. Такую ошибку как раз надо юзеру показывать. Даже гугл в брауезере не стесняется это делать. А как вы ее через обработку кодов обработаете?

считать все остальное фейлами, не тратя ресурсы на парсинг ответа.

В том-то и проблема, что не только тратя, но тратя в 2 раза больше, чем можно тратить, используя подход REST over HTTP.

Я же не просто так это пишу, а по собственному опыту использования множества разных апи.
Никто не заставляет. Так просто удобнее отсеять сразу все транспортные проблемы.

Если у вас случились транспортные проблемы, вы не получите ответа вообще (кроме случаев с прокси и их специфическими ошибками).


Оно не может вылезти во время эксплуатации. А если вылезло, то все настолько плохо, что все равно надо дебажить.

Ну то есть ретраить-то бесполезно.


Это в любом случае должно обрабатываться отдельно

То есть тоже ретраить бесполезно.


Потому что все ошибки такого плана должны отлавливаться при разработке клиента и в эксплуатации не возникать.

В реальности так не бывает, и между сервером и клиентом постоянно случаются расхождения.


Такую ошибку [мы слишком много запросов делаем] как раз надо юзеру показывать.

То есть опять нельзя ретраить. Не многовато ли получается исключений из вашего правила "Я предлагаю на клиенте засунуть запрос в трайкэтч и в кэтче предлагать юзеру трайэгэйн".


А как вы ее через обработку кодов обработаете?

Ну так тривиально же: ловим 429, дальше блокируем все запросы до истечения Retry-After, если он есть.


В том-то и проблема, что не только тратя, но тратя в 2 раза больше, чем можно тратить, используя подход REST over HTTP.

… и где я их трачу в два раза больше, покажите, пожалуйста?


Я же не просто так это пишу, а по собственному опыту использования множества разных апи.

Likewise.

В реальности так не бывает, и между сервером и клиентом постоянно случаются расхождения.

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

Завидую вам. В моей реальности клиент расходится с сервером даже когда их пишет одна команда (потому что есть k версий серверов и n версий клиентов); а как только за клиент и сервер начинают отвечать разные команды (что уж говорить про разные компании?) — там даже в рамках одной версии есть недопонимания.


Иными словами, если клиенты никогда не расходятся с API, откуда все эти некорректные запросы в моих логах?

Так зачем писать логику разбора и обхождения этой ошибки на клиенте, если ее никогда не будет при моем подходе?

Чтобы в ситуации, когда на сервере валидацию поменяют, а на клиенте — нет, вам было понятно, что происходит. А если вам на эту ситуацию положить, то вы говорите "все незнакомые статус — в эксепшн", и больше ничего не делаете.

Если отдать в ответ 404, то как на клиенте понять в магазине нет соли или нет магазина

Если у вас нет магазина — то у вас должен быть зафэйленный деплой бэкенда с кучей логов и алертами. А клиент о том что магазин не задеплоился знать вообще не должен.

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

POST потом тоже будете «повторять позже»?
POST потом тоже будете «повторять позже»?

Именно! Потому что раз произошла транспортная ошибка, то я данные вообще не получил и можно их отправить еще раз. В этом и есть главный плюс разделения ошибок.
Потому что раз произошла транспортная ошибка, то я данные вообще не получил и можно их отправить еще раз.

Или нельзя, если вы получили 400, 405, 413, 415 и еще некоторые. Потому что ошибки группы 4xx — это не "транспортные ошибки".

Просто надо различать HTTP REST API и REST API over HTTP. В первом случае мы связаны по рукам и ногам спецификациями HTTP, мы решаем описать доменную модель в терминах HTTP, в частности мы должны проецировать ошибки домена на ошибки HTTP. Мы можем вводить дополнительные коды в теле ответа, но ошибки должны возвращаться со статусом 400 или 500 как минимум.

Если же мы делаем REST API over HTTP, то просто используем HTTP как транспортный протокол (в целом это скорее прикладной протокол) и руководствуемся исключительно практическими соображениями, в пределе дав клиентам один урл типа /api с методом POST и возвращая 200 в случае если запрос вообще дошёл до приложения и принят им для обработки, начат анализ тела запроса.

На практике чаще всего смешивают оба подхода, или вообще говорят о REST HTTP API, на деле не соблюдая принципов REST вообще.

Это всё абсолютно верно, только я не понимаю, в чём вообще смысл использовать REST API over HTTP. Как по мне, в момент принятия решения использовать HTTP исключительно в качестве транспорта REST теряет большую часть своей привлекательности и выбор какого-нибудь RPC протокола over HTTP становится более разумным.

В REST архитектуру заложены определённые принципы, облегчающие создание распределённых систем. Есть и другие, но эти тоже работают.

Безусловно, только сколько пользы остаётся от этих принципов после того, как мы потеряли возможность использовать громадное количество существующих инструментов, поддерживающих эти принципы? Пока REST использует соответствующие методы HTTP все эти инструменты могут перехватывать запросы, модифицировать их, кешировать, автоматически повторять идемпотентные… но как только мы сделали "over HTTP" и для всего теперь используем POST — все эти инструменты стали неприменимы.


Теперь для использования заложенных в REST принципов нам нужно писать собственные инструменты. А если всё-равно писать собственное, то не проще ли это делать уже не ограничивая себя REST-ом, если только он не подходит действительно идеально для текущего проекта (и я лично таких проектов не встречал — всегда что-нибудь да "выпирает" из "чистого RESTful")?

Скорее всего именно поэтому на практике комбинируют оба подхода: используют идентификаторы в виде урл, для RUD используют GET, PUT, DELETE и т. п. Но при конфликте домена и семантики HTTP предпочтение отдают домену.

В таком случае запрос OPTIONS вернёт клиенту 200 со всеми вытекающими.

ODATA — лучшее решение для REST API.

Во-первых, стандарт.
Во-вторых, не надо писать документацию.
В-третьих, легкая и быстрая интеграция: скормил метадату «клиенту» и тот знает что от куда брать и как с чем связано.

Такое ощущение, что при придумывании REST интерфейса основное время уходит на проверку следования рекомендациям и, при смене авторитета, изменениям в реализации.
Может быть, RPC подход не так уж и плох? Или SOAP.

И RPC, и SOAP действительно не так уж плохи. Но если вы думаете, что там нет проблемы продумывания интерфейса, то вы глубоко ошибаетесь. Один только спор "как в соапе возвращать ошибки" чего стоит.

304 Not Modified — используйте этот код состояния, когда заголовки HTTP-кеширования находятся в работе

когда имеете дело с HTTP-кешированием (или когда работаете с HTTP-кеширвоанием)


до этого момента в тексте не замечал, что это перевод, а после — сразу видно, как будто немного лень стало переводить

В пункте 8:
200 OK — это ответ на успешные GET, PUT, PATCH или DELETE.
Хотя до этого, в списке методов PATCH вообще не упоминался.
Касательно предмета разговора, считаю использование методов кроме GET и POST избыточным, по ответам использую в основном 200, 400, 401, 404, 413, 422, 500.
В академическом смысле REST направлен на управление ресурсом, а под ресурсом, как правило, пониматься документ не имеющий бизнес-логики, которая создаст сайд-эффекты.

Базовый пример: изменить статус заказа на «Отгружен»
REST: говорим серверу изменить статус заказа с любого на 2
PUT /order/1 HTTP/1.0
{
    "status": 2,
    "seller_id": 101,
    "client_name": "Mr. Holmes",
    "client_address": "Baker st, 221b"
    // ...
}

Реальный мир: перевод заказа в какой-то статус имеет действия бизнес-логики (управление остатком товара на складе, уведомления клиенту и менеджеру, отправка данных в API транспортной компании и тд), поэтому в своих проектах задача «перевести заказ в статус Отгружено» решаю POST запросом с объектом:
POST /order/ship HTTP/1.0
{
    "order_id": 1
}


При реализации по REST сервер должен сам сделать diff текущего состояния ресурса с измененным, решить что там за операция бизнес-логики и выполнить ее. Мне такой подход не нравится, поэтому на бизес-операции делаю отдельные точки входа, из соображений 1 запрос = 1 бизнес-действие. Так удобнее поддерживать, тестировать и разрабатывать.
А если так?
PUT /order/1/status HTTP/1.0
{ "value": 2 }

Не очевидно. API делается для внешних разработчиков, поэтому преимущества семантического программирования тут показывают себя во всей красе. После прочтения какого из методов проще понять что происходит при его вызове: /order/1/status/2 или /order/1/ship? Мне больше нравится второй вариант

2 имелось в виду в теле запроса. Обычное валидное applcation/json значение
Изменить статус заказа академически скорее будет PUT /order/1/status 2 или PATCH /order/1 status=2 или POST /order/1/ship.
Не встречал ранее практики управления свойствами ресурса через /resourse/id/property. Для меня ресурс уже является атомарным и дальнейшее его деление никогда не пробовал. Надо более подробно изучить что это дает и какие проблемы имеет, хотя бы в академическом смасле
концепция так называемых подресурсов

В классических подходах современных MVC фреймворках роутинг строится по принципу /{Controller}/{Method}. Реализовать /order/ship проще чем /order/1/ship. Предложенный вариант тоже хорош, на первый взгляд мне нравится, но реализация требует дополнительных усилий по конфигурации проекта, а мне этого делать обычно лень. Кастомный конфиг роутинга может стать запутанным и поддержка будет сложнее.

Мне почему-то всегда казалось что /{Controller}/{Method} — это не принцип построения роутинга, а всего-то маршрут по умолчанию…

А кто-нибудь может объяснить откуда ажиотаж (вроде уже немного спал) вокруг GraphQL? Никаких преимуществ против нормально документированного REST-like я не вижу

GraphQL это следующий шаг в REST API. Со своими плюсами и минусами.
Плюсы связаны в основном с развитием технологий — непример в GraphQL можно интегрировать real-time за счёт вебсокетов, и это всё ещё находится в рамках GraphQL (в отличие от рест, где сокеты — это что-то отдельное).
Самодокументирования, типизация, строгая объектная модель — это всё про GraphQL.


Минусы в основном сосредоточены в проблемах реализации адекватной ACL модели, и необходимостью продумывать ограничения тяжести запросов на предмет DDoS сервера. Впрочем, в REST эти проблемы тоже есть.


Основное "из жизни" преимущество GraphQL — в половине случаев не надо менять бэк, если немного изменилась конфигурация необходимых фронту параметров.

Итоговая производительность у graphql-клиента будет выше за счёт объединения запросов

Основное преимущество, имхо, возможность по умолчанию делать запросы, в ответе на которые будут скомбинированы разные типы «ресурсов». В REST технически вы можете сделать ендпоинт, который позволит неограниченно разворачивать в глубину разные типы связанных ресурсов типа пользователей и их групп, но это будет нарушением идеологии в общем случае — ресурсы должны содержать ссылки на другие ресурсы, а не включать их представление в себя. Грубо, при подготовке ответов в REST сервер не должен делать join с таблицами, содержащими данные разных типов ресурсов, а в GraphQL он обязан их делать по запросу пользователя. «Джойны» в нём чуть ли не главная фича из коробки, их специально нужно ограничивать, если не хочешь дать возможность клиенту разворачивать неограниченно всю модель. А в HTTP REST нужно вводить дополнения, нарушающие его идеологию, если хочешь, например, вытащить одним запросом пользователя, его группы, их пользователей их из группы.
Что я скажу.

Существует неправильное представление о том, что все данные, доступные через сеть, считаются REST, но это не так.

Нет, это существует мейнстримовое представление, что только REST API по этим гребаным лучшим практикам — REST, а все остальное непойми что.

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

Мне вот нравится ВК API.
Оно что, не REST?
Еще как REST.

Что мне это напоминает?
Да любую мейнстримовую истерию.
Да хоть на счет того же Agile.
Это хомякам кто-то оплачивает эти статьи?
Или они сами выбрали течь в чужих сомнительных парадигмах?
Мне вот нравится ВК API. Оно что, не REST? Еще как REST.

По какому формальному определению?

То есть вы утверждаете, ВК API (какой конкретно, кстати?) выполняет пять из шести ограничений по Филдингу?

1. ВК API может как отвечать всем требованиям, так и не отвечать.
Я не ВК.
Я смотрю на сам «протокол».

2. Я считаю да.
Да и некоторые ограничие какие-то тупорылые и непонятные.
Вы считаете нет, что он нарушает?
Какой API? Да тот, который в документации освещен по умолчанию.
ВК API может как отвечать всем требованиям, так и не отвечать.

Если он не отвечает требованиям этого определения, то он не REST в рамках этого определения.


Да и некоторые ограничие какие-то тупорылые и непонятные.

А это не важно, они все равно часть определения. Если они вам не нравятся — значит, это определение вам не подходит. Ок, дайте другое.


Какой API? Да тот, который в документации освещен по умолчанию.

Ссылку дайте, пожалуйста, на конкретное описание. Я сходу нашел кучу разных, непонятно, о чем именно речь идет.

Мы видимо пошли чуток не туда.

Меня интересует, как делать запросы и получать ответы.

Да, документация по API немного запутанная. Не сразу поймешь куда попадешь.
Вот это vk.com/dev/methods
Меня интересует, как делать запросы и получать ответы.

Оно и видно. Вас не интересует, REST ли это, вас интересует, как делать запросы и ответы. Ну и прекрасно. Просто оставьте REST в покое и пользуйтесь теми API, которые вам удобны.


Вот это vk.com/dev/methods

Выглядит как типичный RPC.

Ну да, это не может не быть RPC.
Потому что RPC — это просто констатация межпроцессного взаимодействия.
Это не конкретный протокол.

А REST — это RPC по HTTP.

Многие под REST и RPC почему-то понимают конкретный протокол.
Но это заблуждение.
Потому что RPC — это просто констатация межпроцессного взаимодействия.

Конечно, нет. RPC — это remote procedure call, вполне конкретный стиль межпроцессного взаимодействия. Скажем, если вы в SOA кидаетесь документными событиями, у вас будет межсистемное взаимодействие, но не будет RPC.


А REST — это RPC по HTTP.

Просто нет. Во-первых, вы не найдете определения, где это было бы сказано, во-вторых, вот: https://www.quora.com/What-is-the-difference-between-REST-and-RPC

1. Почитай что такое RPC на википедии.
Это не только HTTP.

2. Иногда просто нужно думать головой и уметь складывать цельную картину.

3. Из статьи о REST на вики:
В сети Интернет вызов удалённой процедуры может представлять собой обычный HTTP-запрос (обычно «GET» или «POST»; такой запрос называют «REST-запрос»), а необходимые данные передаются в качестве параметров запроса[2][3].


вызов удалённой процедуры — ссылка на RPC.

4. Да потому что интернеты захватили упоротыши.
Вон еще один статью накалякал с бредом.
Это как форсед мем.

5. По ссылке — это все ерунда.

Если мой ответ не пробъет Вашу броню, то можете не отвечать мне.
Почитай что такое RPC на википедии. Это не только HTTP.

Я где-то утверждал, что RPC — это только HTTP? Вроде нет.


Из статьи о REST на вики:

Если подниметесь буквально на строчку выше вашей цитаты, там написано буквально: "REST является альтернативой RPC" (выделение мое)


Да потому что интернеты захватили упоротыши. [...] По ссылке — это все ерунда.

Конечно, один вы знаете, как правильно определяется REST. Было бы здорово, конечно, увидеть-таки ссылку на признанное формальное определение.

UFO just landed and posted this here

Везде где могу — использую или реализую JsonRPC 2.0 (http://www.jsonrpc.org/specification). Больше нет неоднозначностей с объектами, нет ограничений в методах, нет необходимости определять — ошибка на уровне протокола хттп или бизнес-логики. Да и вообще без разницы, отправка идет по чистому хттп или через веб-сокеты. А еще нет смешения свойств объекта ответа с информацией о результате выполнения. Все просто и прозрачно. А такое в энтерпрайз не берут.

Я у себя на энтерпрайзе не использую REST. В последнее время — GraphQL, до этого — JSON over HTTP, в RPC-стиле.

Мне искренне непонятно как вообще на REST можно построить API для типичных страниц приложения — где надо показать 10 наиболее релевантных зайчиков, статистику по белочкам, детали по первому зайчику, и вот это всё. Если делать в стиле REST — это надо будет на каждой странице в 20 эндпоинтов ходить.

Да, есть случаи когда REST неплохо подойдет. Скажем, как API какого-нибудь популярного приложения типа Trello или Gmail-а. Там это API нужно, в основном, чтобы быстро разобраться, зацепиться, и сделать какую-нибудь простую штуку типа «письмо пришло — создали таску».

При этому, можно легко убедиться, что тот же Gmail, имея публичное REST API, в веб-приложении пользуется совсем другими внутренними API, которые ни разу не REST. И так — делают все нормальные люди. Потому что REST не подходит для построения API для веб- или мобильного приложения. GraphQL — подходит, JSON RPC — подходит, а REST — нет.

Яростная пропаганда REST, как лучшего похода для API — это очень вредная штука. Я лично видел как из-за этого факапят проекты в человеко-годы.
Какие-то уж совсем очевидные вещи. Можно было бы затронуть хотя б опции для фильтрации и сортировки ресурсов, а тут может быть много вариантов.
— как в django rest framework:
site.com/books?author=George Orwell&year__lte=1945&ordering=price
— исползьуя POST(некоторые и так делают):
POST site.com/filters/books
BODY: {
«filter»: ...,
«ordering»: ...,
}
— используя urlencode (лично мне этот метод нравится, т.к. не нужно запоминать извращённую djangoвскую нотацию и можно кешировать, также нет конфликтов полей книги и служебных слов(ordering), но минус — читабельность)
site.com/books?filter=author%3DGeorge%20Orwell%26year%3C%3D1945&order_by=price
— создавать фильтр POST'ом, записывая в базу и делать дальнейший запрос как-то так:
site.com/filters/1234
ИМХО, самый извращённый вариант, зачем хранить ненужный мусор в БД?
И тыщи других вариантов.
1. Конечные точки в URL – имя существительное, не глагол

Можно ли сделать из этого вывод, что нужно использовать
https://my-site.com/subscribtions/{subscribe_id}?unsubscribe
вместо
https://my-site.com/subscribtions/{subscribe_id}/unsubscribe
?

Совсем по уму, уж если у вас есть множество subscriptions с отдельными элементами, достаточно делать DELETE на этом элементе.

Я имею ввиду не удалить ресурс, а пропатчить, сохранив семантическую ценность запроса.

Даже если не обсуждать, надо ли здесь удалять или патчить (потому что это весьма спорный вопрос), ваша фраза уже содержит ответ: просто вызовите PATCH.

Если патчить (просто изменить следующий GET), то PATCH, как замечено ниже. Если вызывать сложные обработки, то POST, а конкретную обработку указать в теле.
Или в параметре тоже можно в целом

Недавно с коллегами обсуждали такой кейс, будет интересно выслушать мнения :)
Есть сущность «купон», в базе есть как идентификатор (первичный ключ), так и уникальный код купона. Так как пользователь знает только этот код, он может оперировать только им. Соответственно сейчас наши эндпоинты выглядят как GET whatever/api/coupons/code/ для получения инфы по купону. На сколько это легально, учитывая, что код купона может содержать экранируемые символы (пробел, например)?

Тоже как-то раз обсуждали такое. Если заморачиваться, то можно оформить это как связь один-к-одному. coupon-code будет как отдельная сущность. whatever/api/coupon-code/abcd/coupon, по аналогии с whatever/api/product/1/picture.
При правильном URL-кодировании проблем быть не должно, но кажется в nginx бывают какие-то проблемы с экранированным слэшем.

В целом, что у вас есть в базе уровня HTTP REST API не касается. Какой-то суррогатный идентификатор (например serial/auto_increment int) на уровне СУБД вовсе не должен обязательно выставляться в веб. Какой-то гарантированно уникальный, условно естественный код (например вторичный ключ в базе), вполне годится для использования в uri как уникальная часть идентификатора ресурсов определенного типа во всех ситуациях на уровне HTTP REST API, в виде, напрмиер whatever/api/coupons/{code}, то есть о вашем первичном ключе вашим клиентам вообще знать незачем, у них есть способ однозначно идентифицировать каждый купон.

2. Множественное число

Старый спор, из серии какое число выбрать для названия таблицы базы данных, и по-моему единственное число в итоге имеет больше плюсов
Мультимедийный способ управления версиями:

Жесть, не надо такого делать никогда. Как вы в респонсе урлом объясните клиенту номер версии? А ваш фреймворк такое кстати поймёт когда будет определяться во что конвертировать объект в xml или json?

PUT: этот метод является идемпотентным. Вот почему лучше использовать этот метод вместо POST для обновления ресурсов. Избегайте использования POST для обновления ресурсов.

пост по спецификации всегда создаёт подресурс (субординат) по урлу, так что семантически он не для обновления и сюрприз тут кроется в том, что браузер именно так и думает и при кэшировании эта разница всплывёт неприятным образом для тех кто использует post для обновления а не создания подресурсов

401 Unauthorized — Если не указаны или недействительны данные аутентификации. Также полезно активировать всплывающее окно auth, если приложение используется из браузера

когда-то скопипастил с хабра, кажется с блога Яндекса.
статус 401 Unauthorized обязан сопровождаться заголовком WWW-Authenticate и, таким образом, применим только тогда, когда клиент аутентифицируется посредством HTTP-аутентификации; во всех остальных случаях следует использовать 403 Forbidden;

В общем передавайте привет «Krishna Srinivasan» из солнечной Индии :) (и прочитайте ту статья в блоге Яндекса она полнее и точнее)
пост по спецификации всегда создаёт подресурс (субординат) по урлу

Да откуда вообще вы берёте эту информацию? В спецификации я вижу другое:


The POST method requests that the target resource process the
representation enclosed in the request according to the resource's
own specific semantics

Так что POST можно использовать для чего угодно.


браузер именно так и думает и при кэшировании эта разница всплывёт неприятным образом для тех кто использует post для обновления а не создания подресурсов

Устаревшая версия ресурса может "всплыть" при любом способе обновления, единственный способ этого избежать — отключить кеширование вовсе. PUT инвалидирует лишь тот кеш, через который прошёл, и лишь для одного ресурса. Это редко когда бывает достаточно.

Sign up to leave a comment.

Articles