![](https://habrastorage.org/webt/za/yt/hf/zaythfh3df4c0ypahyi08iqrbrq.jpeg)
Давно я хотел написать эту статью. Все думал — с какой стороны зайти правильнее? Но, вдруг, недавно, на Хабре появилась подобная статья, которая вызвала бурю в стакане. Больше всего меня удивил тот факт, что статью начали вбивать в минуса, хотя она даже не декларировала что-то, а скорее поднимала вопрос об использовании кодов ответа web-сервера в REST. Дебаты разгорелись жаркие. А апофеозом стало то, что статья ушла в черновики… килобайты комментариев, мнений и т.д. просто исчезли. Многие стали кармо-жертвами, считай, ни за что :)
В общем, именно судьба той статьи побудила меня написать эту. И я очень надеюсь, что она будет полезна и прояснит многое.
Предупреждаю, все ниже написанное является реальным опытом, а не когнитивной эквилибристикой. И так, погнали.
HTTP
Первым делом нужно очень четко разделить слои. Слой транспорта — http. Ну и собственно REST. Это фундаментально важная вещь в принятии всего и “себя” в нем. Давайте сначала поговорим только о http.
Я использовал термин “слой транспорта”. И я не оговорился. Все дело в том, что сам http реализует функции транспортировки запросов к серверу и контента к клиенту независимо от tcp/ip. Да, он базируется на tcp/ip. И вроде, нужно считать именно его транспортным. Но, нет. И вот почему — сокет-соединения не являются прямыми, т.е. это не соединение клиент-сервер. Как http запрос, так и http ответ могут пройти длинный путь через уйму сервисов. Могут быть агрегированы или напротив декомпозированы. Могут кэшироваться, могут модифицироваться.
Т.е. у http запроса как и http ответа есть свой маршрут. И он не зависит ни от конечного бэка, ни от конечного фронта. Прошу на это обратить особое внимание.
Маршруты http не являются статическими. Они могут быть очень сложными. Например, если в инфраструктуру встроен балансировщик, полученные запросы он может отправить на любую из нод бэка. При этом, сам бэк может реализовывать собственную стратегию работы с запросами. Часть из них пойдет на микросервисы напрямую, часть будет обработана самим web-сервером, часть дополнена и передана кому-то еще, а часть выдана из кэша и т.п. Так работает Интернет. Ничего нового.
И тут важно понять — зачем нам коды ответов? Все дело в том, что вся вышеописанная модель принимает решения на их базе. Т.е. это коды, позволяющие принимать инфраструктурные и транспортные решения в ходе маршрутизации http.
К примеру, если балансировщик встретится с кодом ответа от бэка 503, при передаче запроса, он может принять это за основание считать, что нода временно недоступна. Отмечу, что в ответе с кодом 503 предусмотрен заголовок Retry-After. Получив из заголовка интервал для повторного опроса, балансировщик оставит ноду в покое на указанный срок и будет работать с доступными. Причем, подобные стратегии реализуются “из коробки” web-серверами.
Небольшой офтопик для глубины понимания — а если нода ответила 500? Что должен сделать балансировщик? Переключать на другую? И многие ответят — конечно, все 5xx основание для отключение ноды. И будут неправы. Код 500 это код неожиданной ошибки. Т.е. той, которая может больше никогда и не повториться. И главное, что переключение на другую ноду может ничего и не изменить. Т.е. мы просто отключаем ноды без малейшей пользы.
В случае с 500 нам на помощь приходит статистика. Локальный WEB-сервер ноды, может переводить саму ноду в статус недоступности при большом количестве ответов 500. В этом случае, балансировщик обратившись на эту ноду, получит ответ 503 и не будет ее трогать. Результат тотже, но теперь, такое решение осмысленно и исключает “ложные” срабатывания.
Но и это еще не все. В такой ситуации мониторинг позволит админам подключиться к ситуации для обслуживания ноды. Т.е. мы получаем не просто реализацию высокодоступного сервиса, с балансировками и т.п., но еще и эффективный процесс поддержки.
И все это позволяют делать коды ответа сервера. Любая архитектура WEB-приложения должна начинаться с проектирования транспортного слоя. Надеюсь, сомнений в этом не осталось.
REST
Задам риторический вопрос — что это такое? И что вы ответили себе на него? Не буду давать ссылки на очевидные пруфы, но скорее всего не совсем то, чем он является по сути :) Это лишь идеология, стиль. Некие соображения на тему — как лучше общаться с бэком. И не просто общаться, а общаться в WEB инфраструктуре. Т.е. на базе http. Со всеми теми “полезными штуками”, о которых я написал выше. Конечные решения по реализации вашего интерфейса остаются всегда за вами.
Вы задумывались почему не придуман отдельный транспорт для REST? Например, для websocket он есть. Да, он тоже начинается с http, но потом, после установки соединения, это вообще отдельная песня. Почему бы не сделать такую же для REST?
Ответ прост — а зачем? Есть прекрасный, уже готовый, выверенный протокол — http. Он хорошо масштабируется. Позволяет реализовывать сложные, высокодоступные сервисы, способные справляться с большой нагрузкой. Все, что нужно — ввести некие концептуальные правила, чтобы разработчики друг друга понимали.
Отсюда следует простой, очевидный вывод — все, что присуще http, присуще и REST. Это неотделимые сущности. Нет отдельного заголовка REST, нет даже намека на то, что REST это REST. Для любого сервера REST запрос ровно такой же, как и любой другой. Т.е. REST это только то, что у нас “в голове”.
Коды ответа http в REST
Давайте поговорим о том, каким же кодом должен отвечать ваш сервер на REST запрос? Лично мне кажется, что из всего выше написанного уже очевиден ответ, что т.к. REST не отличается от любого другого запроса, он должен быть подчинен ровно тем же правилам. Код ответа — неотъемлемая часть REST и он должен быть релевантен сути ответа. Т.е. если не найден объект по запросу, это 404, если клиент обратился с некорректным запросом 400 и т.д. Но, чаще всего, дебаты на сём не заканчиваются. Поэтому, продолжу и я.
Можно ли отвечать на всё кодом 200? А кто вам запретит? Пожалуйста… код 200 такой же код как и другие. Правда, в основе такого подхода лежит очень простой тезис — моя система идеальная, у нее не бывает ошибок. Если вы человек, который может создавать такие системы — этому можно только позавидовать!
Но скорее всего… она не идеальна. И ошибки все же случаются. А бывает, что они случаются по независящим от нас обстоятельствам. И тут типовым решением является создание собственной системы кодирования ошибок. Это плохо? Да, это плохо. Это супер-плохо. Давайте разбираться почему.
И так, принимая код 200 как единственно верный, мы берем на себя обязанности на разработку целого слоя (критического слоя) системы — обработку ошибок. Т.е. труд многих людей по разработке этого слоя отправляется в утиль. И начинается постройка своего “велосипеда”. Но эта мегастройка обречена на провал.
Начнем с кода. Если мы собираемся на все отвечать 200, нам самим придется обрабатывать ошибки. Классическим методом является try конструкции. Каждый сегмент кода мы оборачиваем дополнительным кодом. Обработчиками, которые что-то делают полезное. Например, что-то кладут в лог. Что-то важное. Что позволит локализовать ошибку. А если ошибка возникла не там где ее ожидали? Или если ошибка возникла в обработчике ошибки? Т.е. эта стратегия на уровне кода нерабочая априори. И в конце концов, обрабатывать ваши баги будет интерпретатор или платформа. ОС, наконец. Суть бага в том, что вы его не ждете. Не нужно его прятать, его нужно находить и фиксить. Поэтому, если на какие-то запросы REST ответит ошибкой 500 это нормально. И более того — правильно.
Давайте еще раз вернемся к вопросу — почему это правильно? Потому что:
- Код 500 это инфраструктурный маркер, на основании которого нода на которой возникает проблема может быть отключена;
- Коды 5xx это то, что мониторится и если такой код возникает, любая система мониторинга тут же вас известит об этом. И служба поддержки вовремя сможет подключиться к решению проблемы;
- Вы не пишите дополнительный код. Не тратите на это драгоценное время. Не усложняете архитектуру. Вы не занимаетесь несвойственными вам проблемами — вы пишите прикладной код. То, что от вас хотят. За что платят.
- Трейс который выпадет по ошибке 500 будет куда как полезнее, чем ваши попытки его превзойти.
- Если REST запрос вернет 500 код, фронт уже на моменте обработки ответа будет знать, по какому алгоритму его обрабатывать. Причем, суть дела никак не изменится, вы как ничего толкового не получили и с 200, так и с 500. Но с 500 вы получили профит — осознание того, что это НЕОЖИДАННАЯ ошибка.
- Код 500 придет гарантированно. Независимо от того насколько плохо или хорошо вы написали свой код. Это ваша точка опоры.
Отдельно забью гвоздь во все “тело” кода 200:
7. Даже если вы очень сильно постараетесь избежать иных кодов ответа от сервера кроме как 200 на ваши запросы, вы не сможете это сделать. Вам может ответить на ваш запрос любой сервер посредник, совершенно любым кодом. И вы ДОЛЖНЫ будете такой ответ обработать корректно.
Итого, на логическом уровне борьба за код 200 бессмысленна.
Теперь давайте вернемся к инфраструктурному уровню. Очень часто слышу мнение — код 5xx не прикладного уровня, его нельзя отдавать бэком. Кхм, ну… тут есть противоречие в самом утверждении. Отдавать можно. Но код этот не прикладного уровня. Вот так вернее. Для понимания этого, предлагаю рассмотреть кейс:
Вы реализуете шлюз. У вас несколько ДЦ, на каждом свой канал связи к некоему приватному сервису. Ну, к примеру, к платежке по VPN. И есть канал коммуникации с Интернет. Вы получаете запрос на операцию со шлюзом, но… сервис оказывается недоступен.И так, что вы должны ответить? Кому? Это проблема именно инфраструктурная и, именно, бэк столкнулся с ней. Конечно, нужно смело отвечать 503. Эти действия приведут к тому, что нода будет отключена балансировщиком на какое-то время. При этом, балансировщик при правильной настройке, не разрывая соединение с клиентом, отправит запрос в другую ноду. И… конечный клиент, с великой долей вероятности получил 200. А не кастомное описание ошибки, которая ему ничем не поможет.
Где и какой код использовать
Вопрос непростой. На него нет однозначного ответа. Для каждой системы проектируется транспортный слой и коды в нем могут быть специфичные.
Есть принятые стандарты. Их можно легко найти и, опять же, не буду очевидные пруфы приводить. Но, приведу неочевидный — developer.mozilla.org/ru/docs/Web/HTTP/Status
Почему его? Все дело в том, что обработчики кода могут вести себя по разному, в зависимости от реализации и контекста “понимания кода”. К примеру, в браузерах есть стратегия кеширования, завязанная на коды ответа. А в некоторых сервисах есть свои, кастомные коды. Например, CloudFlare.
Т.е. принятие решений об использовании кодов, нужно базировать на всех элементах входящих в транспортный слой от вашего кода на бэке до кода на клиенте. Только так можно найти верные ответы. Я даже пытаться тут дать всем универсальную пилюлю не буду.
Корни зла
Уже третий проект, в который я прихожу страдает кодом 200 в REST. Именно страдает. Другого слова нет. Если вы внимательно прочли всё до текущего момента, вы уже понимаете, что как только проект начинает расти, у него появляется потребность в развитии инфраструктуры, в ее устойчивости. Код 200 убивает все эти потуги на корню. И первое, что приходится делать — ломать стереотипы.
Корень зла, мне кажется лежит в том, что код 500, это первое, что web-разработчик встречает в своей профессиональной деятельности. Это, можно сказать, детская травма. И все его старания поначалу сводятся к тому, чтобы получить код 200.
Кстати, по какой-то причине, на этом же этапе развивается устойчивое мнение, что только ответы с кодом 200 могут быть снабжены телом. Конечно, это не так и с любым кодом может “приехать” любой ответ. Код это код. Тело это тело.
Далее, с развитием разработчика, у него возникают потребности в управлении багами собственного приложения. Но…, он не умеет пользоваться логами. Не умеет настраивать web-сервер. Он учится. И рождаются те самые «велики». Потому что, они ему доступны и он может их быстро сделать. Далее, на этот «велик» он монтирует новые колеса, усиливает раму и т.д. И этот велик становится его спутником на достаточно длительный промежуток времени, пока… пока у него не появляются реально сложные, многокомпонентные задачи. И тут, как говорится — вход в супермаркет с «великами» и на роликах запрещен.
P.S.: Автор упомянутой статьи восстановил ее из черновиков — habr.com/ru/post/440382, поэтому можно ознакомиться с ней тоже.
P.P.S.: Я постарался изложить все грани необходимости использования релевантных кодов ответа в REST. Я не буду отвечать на комментарии, прошу понять меня правильно. С большим вниманием буду их читать, но добавить мне нечего. Огромное спасибо за то, что вам хватило терпения прочесть статью!
Комментарии (114)
RussDragon
19.02.2019 17:42+1Мысли может в статье-то и правильные (хотя называть HTTP транспортным – сильно), но вот стиль изложения никуда не годится. Больше похоже на большой комментарий, чем на полноценный материал.
rpiontik Автор
19.02.2019 17:46+1Поэтому, стараюсь лишнего не писать:) Не мню себя профи эпистолярного жанра. И вы правы, по сути, это сборник комментов которые я в очередной, и очередной раз вынужден выдавать. Причем, вынужден в прямом смысле. По долгу службы. И будет как минимум «удобно» давать теперь ссылку на статью :)
RussDragon
19.02.2019 17:47+1Вот вам еще статья для «давать ссылку» :)
www.mnot.net/blog/2017/05/11/status_codesdefuz
19.02.2019 18:08+1К сожалению это не всегда помогает. Статья не столько о том «как правильно», а о том, почему эти правила нельзя игнорировать, и убеждение разработчика что он может городить свои правила в собственном царстве скорее всего не верны (потому что помимо приложения разработчика есть еще огромной инфраструктурный зоопарк, полагающийся на эти коды для коректного и/или эфективного функционирования).
VolCh
19.02.2019 19:05+1> Отсюда следует простой, очевидный вывод — все, что присуще http, присуще и REST. Это неотделимые сущности. Нет отдельного заголовка REST, нет даже намека на то, что REST это REST.
rest отделим от http, а http от rest нет. Потому что http построен на базе rest.
Собственно выбор отдавать статусы или нет, использовать все методы HTTP семантически (читай — в соответствии со стандартом) или ограничиться GET/POST, а то и только POST (или только GET) — это выбор между использованием в нашей REST-архитектуре REST-возможностей HTTP, заложенных в нём by design, или просто использование HTTP в качестве тупого транспорта.powerman
19.02.2019 20:09+3К сожалению, автор ведёт себя как воинствующий невежа — видит только часть общей картины, вцепился в неё из всех сил, не слышит когда ему пытаются показать ситуацию с другой точки зрения и продолжает повторять одно и то же (и его P.P.S. в статье "я не буду отвечать на комментарии" ровно из той же оперы: я не хочу ничего слышать, мне достаточно громко настаивать на своём). Я уже устал от попыток донести до него простые вещи в комментариях к прошлой статье.
По большому счёту, после фразы
Т.е. REST это только то, что у нас “в голове”.
дальше можно было не читать — исходя из неверных предпосылок сложно прийти к верному выводу. Потому что REST — это вполне конкретная вещь, при использовании которой по прямому и единственному назначению "CRUD ресурсов" потребности возвращать ошибки в теле ответа 200 просто не возникает.
А вот когда REST-ом начинают называть доморощенный недо-RPC, когда выходят за рамки функциональности "CRUD ресурсов" пытаясь при этом формально сохранить внешний вид API а-ля REST, тогда и возникает описанная проблема: нужно возвращать ошибки, для которых просто нет подходящего статуса HTTP. Кто-то использует для этого 200, кто-то 4xx/5xx, но хорошего решения тут нет, у обоих вариантов свои недостатки. Есть только два адекватных решения:
- перейти на использование полноценного RPC — что довольно затруднительно сделать после открытия публичного доступа к API в стиле REST
- переделать API так, чтобы абсолютно все операции формулировались в терминах "CRUD ресурсов", что позволит использовать чистый REST и избавит от потребности использовать нестандартные для HTTP коды ошибок — но это достаточно непросто, что повышает требования к навыкам архитектора, плюс заметно усложняет код и обычно сильно ухудшает эффективность API (начиная требовать по нескольку запросов для выполнения одной операции)
Но на практике оба решения требуют намного больше усилий, чем быстро/грязно вернуть нестандартные (для REST) ошибки в теле 200 или упихивая их ногами в более-менее близкие по смыслу 4xx/5xx…
VolCh
19.02.2019 20:29+2REST не такая уж конкретная вещь, это лишь принципы. И только если решили, что будем приложение строить на этих принципах, и будем использовать HTTP в нём не просто как транспортный протокол, а как прикладной, во многом на тех же принципах построенный, то тогда и следует стараться возвращать ошибки предусмотренным этим протоколом способом. Просто чтобы уменьшить вероятность неожиданного для разработчиков, пользователей, эксплуататоров и т. п. поведения.
arthuriantech
19.02.2019 23:59Вообще говоря, REST никоим образом не связан с CRUD. Эти вещи могут использоваться как вместе, так и независимо.
maxzh83
19.02.2019 21:47+7Мне другой вопрос не дает покоя. Есть ресурс /user/{id}, который по id отдает пользователя. Что должен вернуть такой запрос, если пользователь с заданным id не найден, 404? Если так, то это вводит в ступор, не понятно это пользователя не существует или ендпоинт неправильно указан. Возвращать 500 тоже сурово как-то.
ilving
19.02.2019 22:11Ответ сильно зависит от логики приложений клиента и сервера.
204 No Content, например — если кривой id это валидная ситуация
Или все-таки 404, если на линки типа /user/{id} можно попасть только вбив руками кривой id
tuxi
19.02.2019 22:49+2Есть такой паттерн в разработке «возвращать всегда список». Не готов обсуждать верный ли он для всех случаев, смысл в нем простой: возвращается всегда список. Типа этого
Найдено 10 пользователей — список из 10 пользователей.
Найден 1 пользователь — список из 1 пользователя.
Не найдено пользователей — список, но пустой.
Это надежно и удобно во многих случаях.
Допустим некий ресурс/api следует этому паттерну. Вот вам и код 200. Ничего не найдено, код 200. Надо смотреть внутрь чтобы понять сколько найдено.
Второй момент: многие api начинаются в полном соответствии REST идеалогии, но спустя некоторое время возникают ситуации, когда их превращают уже в то, что в одном комменте в обсуждениях назвали «недоRPC» и вся архитектура «ломается»…
Ну это реальная жизнь, куда ж от нее скрыться :)
powerman
20.02.2019 00:14+2Если используется REST, в его оригинальном смысле — то 404. REST это операции над ресурсами, ресурс определяется url, данного ресурса-юзера нет — значит единственный корректный ответ это 404.
А если это не REST, а просто внешне похожее на него самопальное API — любой из предыдущих ответов абсолютно корректен.
michael_vostrikov
20.02.2019 07:26Еще пара ситуаций, где непонятно как это правильно сделать в REST.
— Есть пользователи, обращение идет по id, но в некоторых сценариях использования надо достать пользователя по email.
— Есть уникальные длинные хеши, надо достать связанную сущность по хешу, связь 1:1.
— Есть билеты на некоторое мероприятие, есть понятие бизнес-логики «Активировать билет», выполняются некоторые действия, меняется состояние билета.cjbars
20.02.2019 08:19+1- ну например так, создаем ресурс Поиск. И добавляем новую сущность в ресурс поиска (создаем элемент поиска). На человеческом языке это звучит как «создай мне поиск пользователей с вот такими вот данными»
POST /api/users/search { field: 'email', value: 'user@example.com'}
либо просто отдельный ресурс поиск, и искать сущность и поле тогда можно например так
GET /api/search/users?email=user@example.com
- Если я правильно понял задачу то (достать у пользователя {id} сущности по хешу {hash}):
GET /api/users/{id}/entities/{hash}
- можно добавить билетам связнную сущность статус билета и крутить автивации например так:
заодно можно историю активаций смотреть, и деактивировать после активацииPOST /api/tikets/{id}/status { isActive: true, date: now}
michael_vostrikov
20.02.2019 10:16- Ну это тогда получается нелогично, не соответствует модели данных. Email однозначно идентифицирует пользвателя, а в ответе всегда будет массив.
- Нет, у пользователя есть хеш, типа промокод, надо достать срок действия или условия скидки. Возможные хеши генерируются заранее и лежат отдельно, акции на них создаются потом как потребуется. Мы сделали что-то вроде
GET /api/hash/{hash}/discount
, но перед этим долго обсуждали, как это правильно в REST. - Ну как бы да, примерно это обычно и советуют.
То есть получается так.
RPC:
Один эндпойнтPOST /api/tikets/{id}/activate
, с конкретными действиями в обработчике.
Один дополнительный раздел в документации, похожий на другие.
Даже без документации примерно понятно, что подавать на вход и какой результат ожидать.
Изменение статуса отдельно, лог изменений отдельно, API и внутренняя архитектура соответствуют бизнес-логике.
REST:
4 возможных эндпойнта для глаголовGET/POST/PUT/DELETE
.
По каждому свой раздел в документации.
В обработчикеPOST /api/tikets/{id}/status
надо проверять новый статус и вызывать отдельный более специализированный обработчик.
Что будет возвращаться поGET /api/tikets/{id}/status
так сходу и не скажешь — текущий статус, история изменения статусов?
Каким-то образом надо задавать связь между билетом и последней строкой в истории статусов.
Или сделать реализацию как в первом варианте и получить расхождение между API и архитектурой с соответствующими сложностями в поддержке.cjbars
20.02.2019 10:34Ну это тогда получается нелогично, не соответствует модели данных. Email однозначно идентифицирует пользвателя, а в ответе всегда будет массив.
Однозначно идентифицирует пользователя ID — это первичный уникальный ключ. А email в таком случае опциональный идентификатор, и никто не запрещает иметь многим пользователям один email, ну чисто теоретически. Поэтому на выходе и будет массив, все логично, я считаю.
Мы сделали что-то вроде GET /api/hash/{hash}/discount
На мой взгляд это чуть чуть неверно, ибо вы просто достаете сущность hash по его параметру {hash}, пользователя тут нет.
Мне кажется прикольнее было бы так
GET /api/users/{id}/hashes/{hash}/discounts
То есть получается так.
RPC:
Один эндпойнт POST /api/tikets/{id}/activate, с конкретными действиями в обработчике.
…
REST:
4 возможных эндпойнта для глаголов GET/POST/PUT/DELETE.
…
Все так :-) ну а как? Главное не перемешивать оба этих подхода, а то начинаем за REST, а опоминаемся дописывая глаголы к эндпоинту )))michael_vostrikov
20.02.2019 11:01Поэтому на выходе и будет массив, все логично, я считаю.
В ответе вдруг пришло 2 пользователя, кого логинить? Да и просто если в коде возвращать массив там где должен быть объект, то это считается неправильным, почему в API должно быть по-другому.
вы просто достаете сущность hash по его параметру {hash}, пользователя тут нет.
Ну тут пользователь как таковой не нужен. Use case — вычислить скидку при создании заказа, пользователь на этом шаге может быть еще не зарегистрирован.
cjbars
20.02.2019 11:11В ответе вдруг пришло 2 пользователя, кого логинить?
Уточнять. Например я могу быть зарегистрирован как исполнитель и как заказчик, и иметь один email. Спросите меня в качестве кого я хочу войти. А если такого не предусмотрено, то двойных емейлов в базе быть не должно. Еще на этапе регистрации или смены это нужно отсекать.
Да и просто если в коде возвращать массив там где должен быть объект, то это считается неправильным, почему в API должно быть по-другому.
Если мы идем на /search — то ответ подразумевает наличие нескольких элементов.
Если же это логин, то тут должно быть что-то, типа:
POST /api/users/authorizations {field:email, data:'user@example.com'}
хотя если в базе два юзера с одним мылом, вопрос кого логинить остается открытым :-)
- ну например так, создаем ресурс Поиск. И добавляем новую сущность в ресурс поиска (создаем элемент поиска). На человеческом языке это звучит как «создай мне поиск пользователей с вот такими вот данными»
VolCh
20.02.2019 10:481. REST не запрещает иметь несколько URL для одного ресурса, хотя в целом два ключа для одного ресурса странно выглядит. В данном конкретном случае вполне может быть даже единый URL /users/:id, где id анализируется на формат.
2. GET /hash/:hash, а в ответе линк на сущность(и). Можно что-то типа GET /hash/:hash?withDiscount, чтобы лишний запрос не делать.
3. PUT/PATCH /tickets/:id или PUT /tickets/:id/state — если состояние сложный ресурс. POST /tickets/:id/states если нужна история состояний.michael_vostrikov
20.02.2019 11:19PUT/PATCH /tickets/:id
Тут возникает проблема, как определить, что мы хотим сделать. В коде будет куча веток
if (prevFieldValue != currentFieldValue) runSpecialLogic()
, непонятно как показать сообщение "Билет уже активирован". Может пользователь хочет какую-нибудь надпись на активированном билете поменять, а отправляются все поля. Если отправлять только поля, которые надо поменять, то в модели они будут все необязательные. а это тоже не подходит.VolCh
20.02.2019 12:11Причём тут модель к полям запроса? Сначала валидируем запрос (например, если меняется больше одного поля в одном запросе, то ошибку 400 или 422 выдаём), а потом дёргаем уже нужный метод модели, не
prevFieldValue != currentFieldValue
, аfieldName == "state" ticket.setState(newFieldValue)
michael_vostrikov
20.02.2019 14:29Ну а как вы определите, что поле изменилось, не используя
prevFieldValue
?
Если сравнивать
fieldName == "state"
, то при повторном запросе повторятся все действия в системе — отправка email, начисления/списания.VolCh
20.02.2019 14:53В сеттере, вернее там определю можно ли установить новое состояние или оно нарушает бизнес-логику. что-то вроде
if (this.state === 'draft') { if (newState === 'approved') { this.onApproved() }}
А вообще, я о том, что запрос к модели весьма слабое отношение имеет. Задача контроллера преобразовать http-запрос в вызов методов модели.
lair
20.02.2019 14:49Есть билеты на некоторое мероприятие, есть понятие бизнес-логики «Активировать билет», выполняются некоторые действия, меняется состояние билета.
POST /tickets/{ticket-id}/activate
Причем, что занятно, выгоднее эту урлу не записывать ни в какую документацию сервиса, а отдавать как ссылку внутри самого билета (который получается по
GET /tickets/{ticket-id}
).defuz
20.02.2019 16:09Не верно, посколько во-первых активация – идемпотентный запрос, а POST – не идемпотентная операция, а во-вторых «аctivate» — это действие, а не ресурс. Так что:
PUT /tickets/{ticket-id}
{'activated': true}
Или
POST /activations-queue
{'ticket_id': ...}
Или
PUT /tickets/<ticket_id>/activation
REST – это всегда про работу с состоянием ресурсов (получение, изменение, удаление и т.д.), а не про вызов методов.lair
20.02.2019 16:42Не верно, посколько во-первых активация – идемпотентный запрос, а POST – не идемпотентная операция
POST
не обязан быть идемпотентным, но может таким быть. Поэтому ничто не запрещает реализовать такие вещи через него.
Более того, если читать определение идемпотентности в RFC 7231, то эта операция — не идемпотентна, поскольку если клиент не получит (успешного) ответа на первый свой
POST
(например, из-за таймаута), и пошлет второйPOST
, то он получит ответ "уже активировано", из которого нельзя разумным образом понять, кем и когда активирован билет.
REST – это всегда про работу с состоянием ресурсов (получение, изменение, удаление и т.д.), а не про вызов методов.
"Активация" — это частный случай изменения состояния ресурса. Просто выражение ее в виде действия намного понятнее пользователю, нежели в виде свойства.
Но я, в принципе, ничего не имею против
POST /tickets/<ticket_id>/activations
(привет гитхабу).defuz
20.02.2019 17:42Идемпотентна ли операция активации зависит от реализации, которая в свою очередь зависит от требований. Так что может быть как да так и нет.
Согласен, на счет не-идемпотентности POST я погорячился. Хотя я бы все равно рекомендовал стараться все идемпотентные операции реализовывать через PUT, а не идемпотентные через POST, как правило хорошего тона.lair
20.02.2019 18:27а не идемпотентные через POST
… а учитывая, что я считаю эту операцию не-идемпотентной, мое применение
POST
полностью оправдано.
powerman
20.02.2019 17:43Ничто не запрещает использовать POST вообще для всего, но тогда в использовании REST просто не остаётся никакого смысла. А чтобы он был нужно стараться делать как можно больше операций идемпотентными или безопасными, и не использовать для таких операций POST.
Соответственно, не стоит без нужды возвращать ошибку "уже активировано". В большинстве случаев всё, что нужно знать юзеру после отправки запроса на активацию — что состояние ресурса после этого запроса "активирован". Был ли он уже активирован перед отправкой запроса или нет — часто не важно, и в этом случае цена возврата бесполезной дополнительной информации в виде ошибки "уже активирован" ценой потери идемпотентности — слишком высока.
Но если посмотреть немного глубже, то, формально, потери идемпотентности в этом случае вообще-то нет. Идемпотентность гарантирует одинаковый эффект на сервере после одного или более одинаковых запросов. Она не гарантирует одинаковый ответ сервера на все эти запросы (и, по факту, ответ сервера всегда отличается — начиная с того, что в нём есть текущая дата, и заканчивая тем, что между нашими запросами данные на сервере могли измениться кем-то другим — причём это касается не только идемпотентных, но даже и безопасных запросов). Так что мы можем вернуть ошибку "уже активирован" на второй запрос не теряя при этом свойства идемпотентности. НО. В большинстве случаев это просто создаст лишние сложности на клиенте без какой-либо пользы.
lair
20.02.2019 18:29В большинстве случаев всё, что нужно знать юзеру после отправки запроса на активацию — что состояние ресурса после этого запроса "активирован". Был ли он уже активирован перед отправкой запроса или нет — часто не важно
А вот это целиком и полностью зависит от бизнес-задачи, и она у нас с вами явно различается.
Но если посмотреть немного глубже, то, формально, потери идемпотентности в этом случае вообще-то нет. Идемпотентность гарантирует одинаковый эффект на сервере после одного или более одинаковых запросов. Она не гарантирует одинаковый ответ сервера на все эти запросы
А вот это зависит от определения, которым вы пользуетесь. Я не зря дал ссылку на RFC, и там критерием стоит возможность повторной отправки запроса, если ответ не был получен.
powerman
20.02.2019 19:27Я внимательно почитал определение в RFC перед написанием предыдущего комментария, просто на всякий случай. Там ничего не сказано про то, что ответ сервера на первый и второй запрос должен быть идентичен. Там говорится исключительно про "intended effect on the server", что абсолютно корректно.
lair
20.02.2019 23:09Idempotent methods are distinguished because the request can be repeated automatically if a communication failure occurs before the client is able to read the server's response.
powerman
20.02.2019 23:26Да, но из этого не следует, что при повторе запроса сервер должен вернуть такой же ответ, как и при первом запросе. Идемпотентность предъявляет требование к состоянию сервера после повтора — оно должно быть таким же, как после первого запроса — но не к ответу сервера.
Грубо говоря, запрос "установить значение переменной A в 42" — идемпотентный, потому что после одного или нескольких таких запросов эффект на сервере будет одинаковы: значение переменной A станет равно 42, каким бы оно ни было до того. При этом сервер может отвечать абсолютно что угодно, хоть случайным значением, отличающимся при каждом запросе — это не отменит свойство идемпотентности: возможность безопасно повторить запрос если предыдущий по какой-то причине выполнить не удалось. (Для контраста, запрос "увеличить значение A на 1" идемпотентным не является, даже если сервер всегда присылает один и тот же ответ.)
Идемпотентность решает конкретную проблему: если при выполнении запроса произошла ошибка и мы не получили ответ, то в общем случае нам неизвестно, на каком этапе случилась эта ошибка — до того как сервер получил наш запрос, или после этого — т.е. мы не знаем, был ли наш запрос выполнен сервером. В этой ситуации крайне ценно иметь возможность повторить запрос так, чтобы была гарантия того, что результат (на сервере) будет одинаковым, вне зависимости от того, выполнил сервер запрос в первый раз или нет.
Получение при этом идентичного ответа не является критичным — мы всегда можем отдельно запросить у сервера текущее состояние ресурса, можем запросить лог всех изменений ресурса, лог всех отправленных сервером ответов, etc. — всё это возможно реализовать, если оно кому-то нужно.
Более того, как я уже упоминал, получение от сервера идентичных ответов при повторе любого запроса, включая GET — никто никогда не гарантирует, потому что состояние сервера могло измениться между запросами. Чтобы получать гарантированно идентичные ответы при повторе нужно запрашивать конкретную версию версионируемого ресурса — это редко кому нужно.
lair
20.02.2019 23:28Да, но из этого не следует, что при повторе запроса сервер должен вернуть такой же ответ, как и при первом запросе.
Нет, не следует. Но я этого и не говорил, в общем-то.
Я говорил о том, что конкретный сценарий нельзя реализовать идемпотентным с сохранением разумной функциональности клиента.
powerman
20.02.2019 23:34Тогда я Вас не понял. Мы всё ещё говорим про сценарий "активации" некоего ресурса? И в чём проблема повтора идемпотентного запроса "активировать ресурс"? Вне зависимости от полученного ответа ("ресурс был активирован" или "ресурс уже активирован") эффект на сервере идентичен — мы имеем активированный ресурс, не важно, активировал его наш второй запрос, наш первый запрос, или кто-то другой успел активировать его ещё до первого запроса.
lair
20.02.2019 23:35Мы всё ещё говорим про сценарий "активации" некоего ресурса?
Гарантированно одноразовой активации. Клиент должен знать, была его активация успешной или нет (одноразовые купонные коды).
powerman
20.02.2019 23:42В этой ситуации сервер может возвращать "купон был активирован" на первый успешный запрос, "вы уже активировали этот купон" на второй и последующие успешные запрос, либо "купон уже был активирован другим пользователем" на первый и последующие запросы.
Альтернативный подход, который я уже упоминал выше — клиент может сделать отдельный запрос "получить статус купона" после получения ответа "купон уже активирован" и выяснить, кем он был активирован — этим юзером или другим.
Но проще всего просто не создавать случайные сложности на пустом месте, и на первый и последующие запросы возвращать либо "купон активен" либо "купон уже был активирован другим пользователем".
lair
20.02.2019 23:44И тут внезапно выясняется, что пользователи-то не аутентифицированные.
powerman
20.02.2019 23:54В смысле, сервер должен однократно активировать купон анонимному пользователю, но при этом не допускать многократной активации одного купона? На первый взгляд у такой задачи просто нет идемпотентного решения, потому что сервер не может отличить повтор запроса первого юзера, от первого запроса второго юзера.
Но на практике обычно такую задачу всё-таки решают идемпотентно, просто добавив в запрос дополнительным параметром сгенерированный клиентом UUID запроса — но Вы наверняка скажете что это не честно т.к. в некотором смысле аутентифицирует юзера (хотя это не так — это идентифицирует отдельный запрос) нарушая условие задачи, либо поломается при перезапуске клиента перед повтором запроса. :)
lair
20.02.2019 23:58В смысле, сервер должен однократно активировать купон анонимному пользователю, но при этом не допускать многократной активации одного купона?
Да.
На первый взгляд у такой задачи просто нет идемпотентного решения
Я про это и говорю.
либо поломается при перезапуске клиента перед повтором запроса
Это основная проблема.
michael_vostrikov
21.02.2019 06:40Можно привязывать к заказу. У заказа часто несколько шагов, и состояние хранится на сервере, купон является как бы отдельным товаром. Это тоже уникальный идентификатор, но не конкретного запроса, а сессии. Его можно хранить между перезапусками клиента.
arthuriantech
20.02.2019 19:37+1Ничто не запрещает использовать POST вообще для всего, но тогда в использовании REST просто не остаётся никакого смысла.
roy.gbiv.com/untangled/2009/it-is-okay-to-use-post
arthuriantech
20.02.2019 19:41Извините за мой французский, но именование URI не влияет на то, является ли сервис RESTful.
cjbars
19.02.2019 21:54+1Спасибо вам за статью. После срача в прошлой статье меня тоже подрывало написать подобную, вы опередили.
Я своим студентам коды ответов на первой же лекции давал, и очень настоятельно требовал их разумно использовать. Теперь у них в головах недоумение по поводу 200 ОК error:true.
А у более опытных обычно срабатывает внутренний демон, если начинаешь колхозить нестандартное на стандартном — значит где-то косяк в архитектуре.
REST вообще очень легко начать использовать не правильно. Начиная от глаголов в урлах, и get запросов на добавление сущности, заканчивая 200 Ок ( все нормально, падаем! )VolCh
20.02.2019 10:53+1REST вообще про статусы и глаголы ничего не говорит, он говорит про унифицированные интерфейсы. Как унифицировать — ваше дело и ваше ответственность, стандарта REST нет.
RouR
19.02.2019 22:22Ошибки валидации на стороне сервера каким кодом будете отдавать?
ilving
19.02.2019 22:38400 Bad Request (что логичнее, если на мой вкус) \ 501 Not Implemented
Опять-таки, в зависимости от логики приложений. Ну и плюс в body написать что именно не прошло валидациюUksusoFF
19.02.2019 23:00Ну и плюс в body написать что именно не прошло валидацию
Некоторые библиотеки для работы с HTTP (например https://github.com/Alamofire/Alamofire) внезапно не отдают body для всего что отлично 200. Поэтому и начинаются велосипеды.
ilving
19.02.2019 23:10+2«Некоторые не отдают» или «некоторые отдают»?
Если первый вариант — то может стоит поискать другую библиотеку?
Если второй (во что верится слабо) — то стоит поискать другую планету )
Ну и да, велосипеды на крайний случай
VolCh
20.02.2019 10:58Реально? Из описания этого не следует:
By default, Alamofire treats any completed request to be successful, regardless of the content of the response.
defuz
20.02.2019 16:40Давайте вместе почитаем доку:
The HyperText Transfer Protocol (HTTP) 501 Not Implemented server error response code indicates that the server does not support the functionality required to fulfill the request. This is the appropriate response when the server does not recognize the request method and is not capable of supporting it for any resource. The only request methods that servers are required to support (and therefore that must not return this code) are GET and HEAD
501 возвращется если сервер не понимает метод, который вы пытаетесь применить к ресурсу, то есть вместо GET/PUT/POST/… решили вызвать «FOOBAR /path/to/resource», что вообще говоря не противоречит спецификации. На запросы GET и HEAD возвращать 501 вообще не допустипо, поскольку они должны распознаватся любым HTTP сервером.
sentyaev
19.02.2019 23:19+2422 Unprocessable Entity
Отлично подходит для валидации.
Горорит о том, что сервер понял тип запроса (415 Unsupported Media Type в этом случае не вариант) и объект запроса синтаксически корректен (в этом случае 400 Bad Request не вариант), но не смог его выполнить.
bgnx
19.02.2019 22:39А у меня такой вопрос к сторонникам rest-подхода — допустим вы не можете использовать http по причинам слабой производительности и есть необходимость использовать вебсокеты для браузеров (либо tcp для мобильных клиентов) — будете ли вы кодировать в передаваемых сообщениях схему http протокола и дальше в своем привычном режиме организовывать взаимодействие клиента с сервером согласно вашим представлениям rest-а или может вы решите не кодировать в сообщениях всю спецификацию http а ограничить себя каким-то подмножеством или может даже изменить какие-то моменты, или может вообще не будете смотреть на спецификацию http-протокола и закодируете в сообщениях формат общения удобный клиенту и серверу?
powerman
20.02.2019 00:22Почитайте для чего нужен REST. В смысле, какая польза от тех ограничений, которые он накладывает. Вы там обнаружите возможность кеширования на прокси, возможность автоматически повторять идемпотентные/безопасные запросы, etc. Ничто из этого просто не актуально, если не используется HTTP. А, следовательно, нет и смысла притворяться, что у нас тут REST (или HTTP).
VolCh
20.02.2019 11:03Почему не актуально? Вы как разработчик прикладного протокола можете всё это заложить в требования к клиентам, серверам и инфраструктуре. Другое дело, что для соответствия принципам REST вам нужно будет реализовать синхронную модель клиентский запрос-серверный ответ поверх асинхронного дуплексного транспорта.
GrigoryPerepechko
20.02.2019 02:56Долгоживущий сокет на сессию, бинарный rpc протокол (хорошо grpc подойдет). Если яйца из стали — можно libquic подключить к мобильным клиентам, или хотя бы постараться использовать tls1.3 с нулевым rtt на установлении соединения.
VolCh
20.02.2019 11:00Если принято решение использовать REST over WebSockets, то смотреть на спецификацию HTTP в этом случае странновато. Имеет смысл разработать свой прикладной протокол.
Dolbe
21.02.2019 22:04Возможно, товарищ выше имел ввиду, что если реализация Rest уже использует http коды, то будет крайне неудобно переделывать ее под WebSockets.
С другой стороны, такой переход будет не таким болезненным, если изначально у REST были свои коды ответов поверх HTTP 200.
i8008
20.02.2019 01:42+4Я использовал термин “слой транспорта”. И я не оговорился. Все дело в том, что сам http реализует функции транспортировки запросов к серверу и контента к клиенту независимо от tcp/ip
Вот вы правильно начали.
Есть слои (описанные в ISO/OSI) И http согласно стандарту – это уровень приложения. Но в современном фронте http часто используют как транспортный/сессионный уровень. Старый прием, инкапсуляция протоколов. Например, сейчас стек RS-485/MODBUS часто инкапсулируют в TCP/IP ибо так проще, чем тянуть физический RS-485. И при инкапсуляции важно, что вышестоящий уровень не зависит от предыдущего. В нашем случае MODBUS должен работать на своем уровне, а не слать NACK на TCP пакеты протокола, в который он инкапсулирован.
Аналогично с REST. HTTP 401 должна говорить что инициатор HTTP не авторизован и. т.д. Но инициатором, как вы правильно заметил, и может быть другой сервер, а не конечный пользователь. Ну и да, REST – это даже не протокол, это просто договоренность
apapacy
20.02.2019 02:31+5Задумывались ли Вы кода-нибудбна двум я такими вопросами?
1. Почему до сих пор нет стандарта RESTAPI (я не называю это REST) в то время как есть стандарты SOAP, oData, json-api, graphql
2. Почему на 13-м году победного шествия RESTAPI по-прежнему идут горячие споры (не только на форумах но среди реальных разработчиков) какой статус чему соответсвует?lair
20.02.2019 12:18+1У меня есть встречный неудобный вопрос: насколько много пользы вы получаете от "стандарта" SOAP или "стандарта" oData? Например, если вы видите SOAP-endpoint, в каком виде вы будете получать от него ошибки?
apapacy
20.02.2019 15:29Тут нет никакого неудобства. В каждой из приведенных систем вопросы с ошибкой решаются по-своему. Общий подход такой в ответе содержится
{
error,
data
}
Чтобы не усложнять ответ я в наших реалиях допускаю частично использовать статус http (и то только потому что на это уже настроены практически все библиотеки которые обращаются по сети к RESTAPI) то есть 200-е ответы — в ответе содержится data без дополнительного уровня вложенности и какой-нибудь из 400-х содержит те случаи когда возвращается значение другого типа (не хочу называть это ошибкой)lair
20.02.2019 15:30В каждой из приведенных систем вопросы с ошибкой решаются по-своему.
Да я вроде конкретно про SOAP спросил.
apapacy
20.02.2019 15:51Ну так это решается там. В Body окумента содержится элемент Fault в котором содержится ответ другого типа данных а не описанный в общей схеме
<?xml version=«1.0» encoding=«UTF-8»?>
<env:Envelope xmlns:env=«schemas.xmlsoap.org/soap/envelope»>
<env:Body>
<env:Fault>
env:Client
<faultstring xml:lang=«ENU»>Invalid username or password.
</env:Fault>
</env:Body>
</env:Envelope>
см. например docs.oracle.com/cd/E24329_01/web.1211/e24965/faults.htm#WSADV627 с подробным описанием.lair
20.02.2019 15:54Ну так это решается там.
Угу. Рассказать вам, сколько я видел имплементаций, в том числе государственного уровня, многократно проанализированных и одобренных институциями, в которых
soap:Fault
не использовались, а был дополнительный внутренний конверт, самописный, конечно же, в котором был статус обработки?apapacy
20.02.2019 16:10Да знаю что это 99.99% А еще знаю что даже некоторые ТОП коммерческие системы, которые уже 10-ки работают на рынке вместо точного описания схемы документа описывают ровно два поля
<result>OK (or error)</result> <xml>&&;lt;products&&;gt; &&;lt;product&&;gt;Java</product&&;gt; &&;;lt;product&&;gt;JavaScript&&;lt;/product&g&;t; &&;lt;/products&&;gt; </xml>
(Пришлось немного исказить синтаксис т.к. редактор заменяет сущности на текст)
Не знаю это лень или прокрастинация?JustDont
20.02.2019 16:31+2Не знаю это лень или прокрастинация?
Это обычное «да чё я буду с этим транспортным слоем возиться?» (с определенной точки зрения и SOAP будет «транспортным»). Если модель не ложится гладко на схему коммуникаций — тем хуже для схемы коммуникаций!
Именно поэтому стандарты коммуникаций у нас либо простые, как палка, и натягиваемые на что угодно (json-rpc), но одновременно не уточняющие никаких деталей, и внутри всё равно будет местячковый колхоз, либо сложные, навороченные, и всё равно требующие серьезного внутреннего описания, чё ж мы собрались передавать и в каком порядке (oData, graphQL, итд). А в этом внутреннем описании можно конечно же нагородить колхоз.
REST в этом смысле хотя бы честен — все знают, что под REST каждый понимает что-то хотя бы немного своё, и что в итоге будет местячковый колхоз.
lair
20.02.2019 16:44Ну вот видите: стандарт есть, а результат ровно тот же самый, что и в REST, в котором стандарта нет.
"Так зачем платить больше?"
xPomaHx
20.02.2019 08:54-1Для меня аргумент что всё ответы рест должны быть 200, это то что в коде обращения к ресту если там не 200 будет бросаться исключение, а значит будет ветвление кода с помощью try catch, а это антипатен.
Ogra
20.02.2019 09:55+3У вас всегда должна быть обработка ошибок — 4хх/5хх может прилететь в любой момент, сервер может быть тупо недоступен, у клиента может отвалиться сеть, ну и так далее. Да, даже если вы обмениваетесь информацией между двумя серверам в одном датацентре, у вас может пропасть линк между ними, бывали случаи. Так что или try/catch или его аналоги должны быть в любом случае.
И тут мы приходим к простоте — если сервер ответил 200, значит все хорошо, запрос прошел от клиента к серверу и выполнился как положено. Если же мы получили какую-то ошибку — запрос надо залогировать/повторить/отклонить.xPomaHx
20.02.2019 10:08Ну так всё верно, если что то сломалось это действительно исключение, а если просто нет контента, или доступа это не ошибка это просто нами созданная ситуация и значит должно быть 200 и в тексте уже поле какое нибудь о том что не ок.
cjbars
20.02.2019 10:48Если просто нет контента, это не ошибка, это нормальная ситуация. Поэтому код 404 Not Found это тоже не ошибка, это нормальная ситуация, и разруливаем мы ее одинаково.
lair
20.02.2019 12:19это то что в коде обращения к ресту если там не 200 будет бросаться исключение
Совершенно не обязательно.
значит будет ветвление кода с помощью try catch, а это антипатен.
Или, наоборот, будет прекрасный паттерн "если код выполнился без исключений, значит, он выполнился успешно".
flancer
20.02.2019 09:42Но с 500 вы получили профит — осознание того, что это НЕОЖИДАННАЯ ошибка.
Получается, что ожидаемые ошибки можно прокидывать с 400-м кодом или даже с 200-м?
apapacy
20.02.2019 10:20Тут можно сразу спросить ошибка чего? Ошибка веб сервера? Ошибка разработчика? Или ответ приложения?
404 статус — это нет api на сервере или не найден скажем товар с идентификатором
401 статус это у вас с клиента не пришел заголовок с токеном авторизации или пользователь с идентификатором указанном в токена заблокирован или удален
Ошибка уровня приложения это не ошибка как таковая а ответ другого типа. Например сервис Найти пользователя по идентификатору может дать ответ типа Пользователь а может дать ответ о мысленный принадлежащий другой у типу данных: пользователь не найден, не задан идентификатор пользователя, а также реальные ошибки: сеть недоступна, сервер не отвечает, нет запрашиваемого api, вызов сервиса завершился с программной ошибкой, тайм-аут соединения, тайм-аут на прокси,cjbars
20.02.2019 10:51Тут можно сразу спросить ошибка чего? Ошибка веб сервера? Ошибка разработчика? Или ответ приложения?
А какая разница? Мне как серверу, который работает с вашим api совершенно до лампочки кто мне ответил кодом 404: прокси, web-server, приложение, еще много кто может прислать мне 404. Мне как серверу понятно — Здесь рыбы нет.
justboris
20.02.2019 12:01+1Особенность этой ситуации в том, что как API ни проектируй, с использованием статус-кодов, или по принципу «всегда 200», в конечном счете все будет более-менее работать нормально. Напишем на клиентской стороне обертку, которая конвертирует ответ во что-то более удобное – и дело с концом.
Аргументы «за» и «против» у обеих сторон примерно равносильные, поэтому получается спор вида «табы против пробелов», без видимого конца.VolCh
20.02.2019 12:13Просто странно выглядит, когда API вовсю использует мощь HTTP как протокола прикладного уровня, но вот только на статус-коды забивает.
beduin01
20.02.2019 15:06Судя по тому что в REST столько неоднозначностей это очень плохая технология (если ее таковой можно назвать).
Видимо будущее за чем-то более вменяемым типа gRPC.VolCh
20.02.2019 16:45+1Именно, что нельзя REST называть технологией — это архитектурные принципы. А gRPC — конкретная технология. Они в разных измерениях.
powerman
20.02.2019 18:09REST отлично подходит, например, для простенького файл-сервера. Например, хостинг картинок. Там очень пригодятся основные плюшки REST: запрашиваемые картинки будут кешироваться промежуточными прокси, их легко докачивать/перевыкачивать, их может показывать обычный браузер без использования дополнительных приложений, даже добавлять, изменять и удалять картинки можно браузером (причём обходясь одним HTML, без JS), сторонние клиенты к такому API очень просто писать т.к. они не требуют ничего сверх базового HTTP… И это могут быть не только картинки, но и txt/json/видео/etc. файлы.
Когда REST используется в качестве полноценного API, и передаёт почти исключительно json, то, в принципе, он масштабируется до состояния, когда можно работать с отдельными полями json-объекта через
PATCH /user/:id
илиPUT /user/:id/field
, плюс можно запрашивать агрегированные/встроенные данные черезGET /user-with-something/:id
.
Но для большинства API этого недостаточно. И в этот момент, действительно, лучше сразу брать RPC вместо REST. Если в проекте есть какие-то ресурсы, которые действительно могут сильно выиграть от плюшек REST (напр. заливаемые юзерами видео/картинки), то можно рассмотреть вариант использования комбинированного API, когда часть операций выполняется через RPC, а часть через REST — но это усложнит клиенты, которым потребуется поддержка двух протоколов. Либо, если проблемных для REST операций API очень мало, и вряд ли они будут добавляться в будущем, можно напрячься, и преобразовать их к виду "операция над ресурсом", чтобы использование для них REST было естественным.
boblenin
21.02.2019 18:46Код 200, код 200. Много ли «типа REST» приложений приедрживается того, что GET не должен изменять состояние ресурса, к которому обращаются? Я даже не говорю о внутренней кухне типа логирования или отслеживания поведения пользователя, доводилось работать с тем, что — /resource/{id} создают новую пустую запись, если по данному id ничего не найдено.
powerman
21.02.2019 18:58+2Как ни странно — много. Любители удалять записи GET-запросами почти перевелись много лет назад, после того, как по их url-кам прошёлся любопытный паук гугла.
vintage
22.02.2019 09:26+1/resource/{id} создают новую пустую запись, если по данному id ничего не найдено.
С точки зрения клиента ресурс не меняется. Запрос полностью идемпотентен. Что там конкретно происходит в памяти сервера REST ни коим образом не регламентирует.
amarao
У меня есть одна фантастическая претензия к «кодам REST», это то, что путаются коды сервера и коды приложения.
Вот у нас есть база клиентов. У него есть микросервис. Он отвечает 200, когда есть клиент (с его данными) и 404 если такого клиента нет.
Вот у нас есть приложение, которое удаляет бесхозные ресурсы. Оно для каждого ресурса берёт владельца и спрашивает приложение с клиентами «есть такой клиент»? (GET /clients/454543), и если нет, то удаляет их.
Вот сисадмин.
И его nginx.
На свежеустановленном сервере.
В который приходит приложение чистки ресурсов с проверкой существования клиента.
И спрашивает GET /clients/454543
И получает 404, потому что такого файла в /var/www нет.
Через 30 милисекунд конфиг nginx'а reload и там уже есть proxy_pass на приложение
Которое знает, что клиент 454543 существует.
А вот его ресурсов уже нет, потому что nginx ответил 404 и ему поверили.
Никогда не используйте стандартные коды http для бизнес-логики.
lair
… а почему у этого сисадмина nginx отвечает клиентам до того, как он полностью сконфигурился?
amarao
Потому что такое иногда бывает. Сервис устанавливается и стартует, показывая дефолтную страницу. Тут же приходит конфигуратор и меняет конфиг, но даже если мы стартуем сервис после конфигуратора, есть вероятность накормить его неверной конфигурацией и получить 404 из-за того, что там не proxy_pass а root.
Просто поверьте оператору — не надо полагаться на стандартные коды, они не о том.
lair
А еще "бывает", что голый нгинкс отдал 200 с ответом, который мы не можем распознать, и приложение упало. Не надо так.
Надо, надо. Не надо полагаться только на стандартные коды (да и то от задачи зависит).
amarao
Конечно, не надо.
В IT есть ровно две категории людей, которые никогда не ошибаются. Это программисты и сисадмины. Первые пишут программы без багов, а у вторых идеальные конфигурации.
JustDont
Что вам мешает уточнять?
Приложение может сидеть и пробовать парсить тело ответа при любом коде, если ожидает там увидеть дополнительные уточняющие данные. Коды же позволяют обеспечивать нормальное функционирование транспортного уровня, вне зависимости от того, что там и по каким причинам у вас отвалилось.
Если вы завязываете важную логику на один только голый 404, то потом и не обижайтесь, что у вас эта важная логика отработала в том числе и когда не следовало.
amarao
Я не завязываю и старательно рекомендую никому не завязывать. Всюду, где я могу повлиять на API, используются коды, которые не могут прилететь с «голого nginx'а».
defuz
Срачи разжигаются и целые статьи пишутся из-за чей-то лени прочитать спеку того, чем пользуется:
Carduelis
Почему-то все всегда забывают про замечательный код 410.
Не понимаю, зачем городить свой велосипед, когда коды настолько нативны, просты и логичны.
Aquahawk
тогда придётся помнить, а был ли такой клиент. Да было бы замечательно, но нет, оно не работает.
lair
… иногда это, кстати, несложно (если в системе все равно уже сделано логическое удаление).
Chamie
Т.е. 410 = «такого клиента больше нет и не будет».
Carduelis
> Никогда не используйте стандартные коды http для бизнес-логики.
Не соглашусь в корне. Но есть нюанс.
Есть две ситуации:
1. Клиент — Микросервис
Здесь микросервис обязательно должен возвращать 404, 401, 503 и прочие. Может быть даже 402 (Payment required).
2. Клиент — Сервисный слой — Микросервис
Вот здесь Микросервис отдает Сервисному слою все те же базовые коды. А вот сам Сервисный слой уже может решить, а насколько данная ошибка является ошибкой бизнес-логики. И отработать, может быть даже вернув 200 с текстом: «По вашему запросу ничего не найдено». Так как это будет не ошибка, а корректный ответ этого сервисного слоя.
oxx
Абсолютно согласен. Ошибки сервера нужно отделать от ошибок бизнесс-логики. В конце концов, 404 может означать отсутствие самого сервиса, а не запрошенного ответа.
В наших проектах код 200 просто означает «у меня есть валидный JSON от REST сервиса», во всех остальных случаях означает, что требуется технический анализ ошибки.
Warrangie
Смешались в кучу кони, люди…
Сервис на запрос isUserExists должен отвечать только true или false, если для ответа используются стандартные HTTP-коды, то это плохой сервис, гоните его. HTTP-коды придуманы чтоб сообщить состояние(запроса?), вы запрашиваете не состояние сервера или запроса, у вас вполне конкретный запрос, на который не может быть другого ответа, кроме как true и false.
amarao
Так подождите, что должен отвечать православный REST API на запрос ресурса, которого не существует?
GET /client/434343
?
DrunkBear
Скорее, здесь более общая задача: что должен делать правильный обходчик сервисов, зная, что микросервис может сейчас ответить 404/5хх, а через минуту уже 2хх?
Посмотрите на мониторинг hadoop: если в течении n минут мы видим х ошибок / превышений контролируемых параметров — вешаем warning, если ошибок стало x+k за период n -вешаем critical. Но в обоих случаях продолжаем наблюдение и пишем логи.
Warrangie
Я об этом. Если у вас сервис отвечает 404 на несуществующего, а 200 на существующего, то ок. Я же говорю о вашем:
По вашим словам вы не запрашиваете клиента, а спрашиваете существует ли он. Это тоже самое что посмотреть фильм целиком чтоб узнать существует ли он. Возможно не слишком хорошая аналогия.
defuz
Вы правы, именно для этого в стандарте предусмотрен метод HEAD:
defuz
Это вы смешали
коней и людейREST и «RPC over HTTP».В REST нет никаких запросов вида «isUserExists который возвращет true/false», а есть стандартные (определенные заранее) запросы к ресурсам и такие же стандартные ответы на них.
VolCh
Тут лучше употреблять «унифицированные», а не «стандартные», а то кто-то может войти в заблуждение и решить, что есть какой-то стандарт REST
oxidmod
Но презентейшен у ресурсов может быть любым.
В зависимости от гет параметра или заголовка может вернуться как презентешен содержащий полную инфу о ресурсе, так и одно поле exists: true/false
defuz
Окей, просто тогда это уже не соответствует REST. Для получения информации о существовании ресурса, но не самого ресурса используется запрос HEAD (вместо GET), и да, он должен поддерживаться любым веб-сервером.
VolCh
Это не соответствует HTTP, а не REST. REST ничего о методах не говорит, кроме того, что способы должны быть унифицированы.