Давно я хотел написать эту статью. Все думал — с какой стороны зайти правильнее? Но, вдруг, недавно, на Хабре появилась подобная статья, которая вызвала бурю в стакане. Больше всего меня удивил тот факт, что статью начали вбивать в минуса, хотя она даже не декларировала что-то, а скорее поднимала вопрос об использовании кодов ответа 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 это нормально. И более того — правильно.

Давайте еще раз вернемся к вопросу — почему это правильно? Потому что:

  1. Код 500 это инфраструктурный маркер, на основании которого нода на которой возникает проблема может быть отключена;
  2. Коды 5xx это то, что мониторится и если такой код возникает, любая система мониторинга тут же вас известит об этом. И служба поддержки вовремя сможет подключиться к решению проблемы;
  3. Вы не пишите дополнительный код. Не тратите на это драгоценное время. Не усложняете архитектуру. Вы не занимаетесь несвойственными вам проблемами — вы пишите прикладной код. То, что от вас хотят. За что платят.
  4. Трейс который выпадет по ошибке 500 будет куда как полезнее, чем ваши попытки его превзойти.
  5. Если REST запрос вернет 500 код, фронт уже на моменте обработки ответа будет знать, по какому алгоритму его обрабатывать. Причем, суть дела никак не изменится, вы как ничего толкового не получили и с 200, так и с 500. Но с 500 вы получили профит — осознание того, что это НЕОЖИДАННАЯ ошибка.
  6. Код 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)


  1. amarao
    19.02.2019 17:08
    -5

    У меня есть одна фантастическая претензия к «кодам REST», это то, что путаются коды сервера и коды приложения.

    Вот у нас есть база клиентов. У него есть микросервис. Он отвечает 200, когда есть клиент (с его данными) и 404 если такого клиента нет.

    Вот у нас есть приложение, которое удаляет бесхозные ресурсы. Оно для каждого ресурса берёт владельца и спрашивает приложение с клиентами «есть такой клиент»? (GET /clients/454543), и если нет, то удаляет их.

    Вот сисадмин.
    И его nginx.
    На свежеустановленном сервере.
    В который приходит приложение чистки ресурсов с проверкой существования клиента.
    И спрашивает GET /clients/454543
    И получает 404, потому что такого файла в /var/www нет.
    Через 30 милисекунд конфиг nginx'а reload и там уже есть proxy_pass на приложение
    Которое знает, что клиент 454543 существует.
    А вот его ресурсов уже нет, потому что nginx ответил 404 и ему поверили.

    Никогда не используйте стандартные коды http для бизнес-логики.


    1. lair
      19.02.2019 17:13
      +5

      … а почему у этого сисадмина nginx отвечает клиентам до того, как он полностью сконфигурился?


      1. amarao
        19.02.2019 18:38

        Потому что такое иногда бывает. Сервис устанавливается и стартует, показывая дефолтную страницу. Тут же приходит конфигуратор и меняет конфиг, но даже если мы стартуем сервис после конфигуратора, есть вероятность накормить его неверной конфигурацией и получить 404 из-за того, что там не proxy_pass а root.

        Просто поверьте оператору — не надо полагаться на стандартные коды, они не о том.


        1. lair
          19.02.2019 18:43

          Потому что такое иногда бывает.

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


          Просто поверьте оператору — не надо полагаться на стандартные коды, они не о том.

          Надо, надо. Не надо полагаться только на стандартные коды (да и то от задачи зависит).


          1. amarao
            19.02.2019 18:48
            -1

            Конечно, не надо.

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


    1. JustDont
      19.02.2019 17:36

      Что вам мешает уточнять?
      Приложение может сидеть и пробовать парсить тело ответа при любом коде, если ожидает там увидеть дополнительные уточняющие данные. Коды же позволяют обеспечивать нормальное функционирование транспортного уровня, вне зависимости от того, что там и по каким причинам у вас отвалилось.

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


      1. amarao
        19.02.2019 18:40

        Я не завязываю и старательно рекомендую никому не завязывать. Всюду, где я могу повлиять на API, используются коды, которые не могут прилететь с «голого nginx'а».


    1. defuz
      19.02.2019 17:45

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

      The 404 (Not Found) status code indicates that the origin server did not find a current representation for the target resource or is not willing to disclose that one exists. A 404 status code does not indicate whether this lack of representation is temporary or permanent; the 410 (Gone) status code is preferred over 404 if the origin server knows, presumably through some configurable means, that the condition is likely to be permanent.


      1. Carduelis
        19.02.2019 17:52
        +1

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


        1. Aquahawk
          22.02.2019 10:52

          тогда придётся помнить, а был ли такой клиент. Да было бы замечательно, но нет, оно не работает.


          1. lair
            22.02.2019 11:23
            +1

            … иногда это, кстати, несложно (если в системе все равно уже сделано логическое удаление).


      1. Chamie
        22.02.2019 12:55

        Т.е. 410 = «такого клиента больше нет и не будет».


    1. Carduelis
      19.02.2019 17:50

      > Никогда не используйте стандартные коды http для бизнес-логики.
      Не соглашусь в корне. Но есть нюанс.

      Есть две ситуации:
      1. Клиент — Микросервис
      Здесь микросервис обязательно должен возвращать 404, 401, 503 и прочие. Может быть даже 402 (Payment required).

      2. Клиент — Сервисный слой — Микросервис
      Вот здесь Микросервис отдает Сервисному слою все те же базовые коды. А вот сам Сервисный слой уже может решить, а насколько данная ошибка является ошибкой бизнес-логики. И отработать, может быть даже вернув 200 с текстом: «По вашему запросу ничего не найдено». Так как это будет не ошибка, а корректный ответ этого сервисного слоя.


    1. oxx
      20.02.2019 12:17

      Абсолютно согласен. Ошибки сервера нужно отделать от ошибок бизнесс-логики. В конце концов, 404 может означать отсутствие самого сервиса, а не запрошенного ответа.
      В наших проектах код 200 просто означает «у меня есть валидный JSON от REST сервиса», во всех остальных случаях означает, что требуется технический анализ ошибки.


    1. Warrangie
      20.02.2019 14:28

      Смешались в кучу кони, люди…
      Сервис на запрос isUserExists должен отвечать только true или false, если для ответа используются стандартные HTTP-коды, то это плохой сервис, гоните его. HTTP-коды придуманы чтоб сообщить состояние(запроса?), вы запрашиваете не состояние сервера или запроса, у вас вполне конкретный запрос, на который не может быть другого ответа, кроме как true и false.


      1. amarao
        20.02.2019 14:41
        +1

        Так подождите, что должен отвечать православный REST API на запрос ресурса, которого не существует?

        GET /client/434343

        ?


        1. DrunkBear
          20.02.2019 15:20

          Скорее, здесь более общая задача: что должен делать правильный обходчик сервисов, зная, что микросервис может сейчас ответить 404/5хх, а через минуту уже 2хх?
          Посмотрите на мониторинг hadoop: если в течении n минут мы видим х ошибок / превышений контролируемых параметров — вешаем warning, если ошибок стало x+k за период n -вешаем critical. Но в обоих случаях продолжаем наблюдение и пишем логи.


        1. Warrangie
          20.02.2019 15:46
          +1

          Вот у нас есть база клиентов. У него есть микросервис. Он отвечает 200, когда есть клиент (с его данными) и 404 если такого клиента нет.

          Я об этом. Если у вас сервис отвечает 404 на несуществующего, а 200 на существующего, то ок. Я же говорю о вашем:
          Вот у нас есть приложение, которое удаляет бесхозные ресурсы. Оно для каждого ресурса берёт владельца и спрашивает приложение с клиентами «есть такой клиент»? (GET /clients/454543), и если нет, то удаляет их.

          По вашим словам вы не запрашиваете клиента, а спрашиваете существует ли он. Это тоже самое что посмотреть фильм целиком чтоб узнать существует ли он. Возможно не слишком хорошая аналогия.


          1. defuz
            20.02.2019 15:56
            +1

            По вашим словам вы не запрашиваете клиента, а спрашиваете существует ли он. Это тоже самое что посмотреть фильм целиком чтоб узнать существует ли он. Возможно не слишком хорошая аналогия.

            Вы правы, именно для этого в стандарте предусмотрен метод HEAD:
            The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response. The metainformation contained in the HTTP headers in response to a HEAD request SHOULD be identical to the information sent in response to a GET request. This method can be used for obtaining metainformation about the entity implied by the request without transferring the entity-body itself. This method is often used for testing hypertext links for validity, accessibility, and recent modification.


      1. defuz
        20.02.2019 15:51
        +1

        Это вы смешали коней и людей REST и «RPC over HTTP».

        В REST нет никаких запросов вида «isUserExists который возвращет true/false», а есть стандартные (определенные заранее) запросы к ресурсам и такие же стандартные ответы на них.


        1. VolCh
          20.02.2019 16:39

          Тут лучше употреблять «унифицированные», а не «стандартные», а то кто-то может войти в заблуждение и решить, что есть какой-то стандарт REST


        1. oxidmod
          21.02.2019 11:46

          Но презентейшен у ресурсов может быть любым.
          В зависимости от гет параметра или заголовка может вернуться как презентешен содержащий полную инфу о ресурсе, так и одно поле exists: true/false


          1. defuz
            21.02.2019 23:37

            Окей, просто тогда это уже не соответствует REST. Для получения информации о существовании ресурса, но не самого ресурса используется запрос HEAD (вместо GET), и да, он должен поддерживаться любым веб-сервером.


            1. VolCh
              22.02.2019 10:18

              Это не соответствует HTTP, а не REST. REST ничего о методах не говорит, кроме того, что способы должны быть унифицированы.


  1. RussDragon
    19.02.2019 17:42
    +1

    Мысли может в статье-то и правильные (хотя называть HTTP транспортным – сильно), но вот стиль изложения никуда не годится. Больше похоже на большой комментарий, чем на полноценный материал.


    1. rpiontik Автор
      19.02.2019 17:46
      +1

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


      1. RussDragon
        19.02.2019 17:47
        +1

        Вот вам еще статья для «давать ссылку» :)

        www.mnot.net/blog/2017/05/11/status_codes


        1. rpiontik Автор
          19.02.2019 17:50

          Спасибо! Не видел ранее. Коротко и ясно.


        1. defuz
          19.02.2019 18:08
          +1

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


          1. rpiontik Автор
            19.02.2019 18:10
            +1

            Прям вот в точку. Каждый видит «слона» с одной стороны.


  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 в качестве тупого транспорта.


    1. 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…


      1. VolCh
        19.02.2019 20:29
        +2

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


      1. arthuriantech
        19.02.2019 23:59

        Вообще говоря, REST никоим образом не связан с CRUD. Эти вещи могут использоваться как вместе, так и независимо.


  1. maxzh83
    19.02.2019 21:47
    +7

    Мне другой вопрос не дает покоя. Есть ресурс /user/{id}, который по id отдает пользователя. Что должен вернуть такой запрос, если пользователь с заданным id не найден, 404? Если так, то это вводит в ступор, не понятно это пользователя не существует или ендпоинт неправильно указан. Возвращать 500 тоже сурово как-то.


    1. ilving
      19.02.2019 22:11

      Ответ сильно зависит от логики приложений клиента и сервера.
      204 No Content, например — если кривой id это валидная ситуация
      Или все-таки 404, если на линки типа /user/{id} можно попасть только вбив руками кривой id


    1. tuxi
      19.02.2019 22:49
      +2

      Есть такой паттерн в разработке «возвращать всегда список». Не готов обсуждать верный ли он для всех случаев, смысл в нем простой: возвращается всегда список. Типа этого

      Найдено 10 пользователей — список из 10 пользователей.
      Найден 1 пользователь — список из 1 пользователя.
      Не найдено пользователей — список, но пустой.

      Это надежно и удобно во многих случаях.

      Допустим некий ресурс/api следует этому паттерну. Вот вам и код 200. Ничего не найдено, код 200. Надо смотреть внутрь чтобы понять сколько найдено.

      Второй момент: многие api начинаются в полном соответствии REST идеалогии, но спустя некоторое время возникают ситуации, когда их превращают уже в то, что в одном комменте в обсуждениях назвали «недоRPC» и вся архитектура «ломается»…
      Ну это реальная жизнь, куда ж от нее скрыться :)


    1. powerman
      20.02.2019 00:14
      +2

      Если используется REST, в его оригинальном смысле — то 404. REST это операции над ресурсами, ресурс определяется url, данного ресурса-юзера нет — значит единственный корректный ответ это 404.


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


    1. 411
      20.02.2019 01:53

      При необходимости можно уточнить в теле ответа детали.


    1. michael_vostrikov
      20.02.2019 07:26

      Еще пара ситуаций, где непонятно как это правильно сделать в REST.

      — Есть пользователи, обращение идет по id, но в некоторых сценариях использования надо достать пользователя по email.
      — Есть уникальные длинные хеши, надо достать связанную сущность по хешу, связь 1:1.
      — Есть билеты на некоторое мероприятие, есть понятие бизнес-логики «Активировать билет», выполняются некоторые действия, меняется состояние билета.


      1. cjbars
        20.02.2019 08:19
        +1

        1. ну например так, создаем ресурс Поиск. И добавляем новую сущность в ресурс поиска (создаем элемент поиска). На человеческом языке это звучит как «создай мне поиск пользователей с вот такими вот данными»
          POST /api/users/search { field: 'email', value: 'user@example.com'}

          либо просто отдельный ресурс поиск, и искать сущность и поле тогда можно например так
          GET /api/search/users?email=user@example.com

        2. Если я правильно понял задачу то (достать у пользователя {id} сущности по хешу {hash}):
          GET /api/users/{id}/entities/{hash}
        3. можно добавить билетам связнную сущность статус билета и крутить автивации например так:
          POST /api/tikets/{id}/status { isActive: true, date: now}
          заодно можно историю активаций смотреть, и деактивировать после активации


        1. michael_vostrikov
          20.02.2019 10:16

          1. Ну это тогда получается нелогично, не соответствует модели данных. Email однозначно идентифицирует пользвателя, а в ответе всегда будет массив.
          2. Нет, у пользователя есть хеш, типа промокод, надо достать срок действия или условия скидки. Возможные хеши генерируются заранее и лежат отдельно, акции на них создаются потом как потребуется. Мы сделали что-то вроде GET /api/hash/{hash}/discount, но перед этим долго обсуждали, как это правильно в REST.
          3. Ну как бы да, примерно это обычно и советуют.

          То есть получается так.
          RPC:
          Один эндпойнт POST /api/tikets/{id}/activate, с конкретными действиями в обработчике.
          Один дополнительный раздел в документации, похожий на другие.
          Даже без документации примерно понятно, что подавать на вход и какой результат ожидать.
          Изменение статуса отдельно, лог изменений отдельно, API и внутренняя архитектура соответствуют бизнес-логике.


          REST:
          4 возможных эндпойнта для глаголов GET/POST/PUT/DELETE.
          По каждому свой раздел в документации.
          В обработчике POST /api/tikets/{id}/status надо проверять новый статус и вызывать отдельный более специализированный обработчик.
          Что будет возвращаться по GET /api/tikets/{id}/status так сходу и не скажешь — текущий статус, история изменения статусов?
          Каким-то образом надо задавать связь между билетом и последней строкой в истории статусов.
          Или сделать реализацию как в первом варианте и получить расхождение между API и архитектурой с соответствующими сложностями в поддержке.


          1. 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, а опоминаемся дописывая глаголы к эндпоинту )))


            1. michael_vostrikov
              20.02.2019 11:01

              Поэтому на выходе и будет массив, все логично, я считаю.

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


              вы просто достаете сущность hash по его параметру {hash}, пользователя тут нет.

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


              1. cjbars
                20.02.2019 11:11

                В ответе вдруг пришло 2 пользователя, кого логинить?

                Уточнять. Например я могу быть зарегистрирован как исполнитель и как заказчик, и иметь один email. Спросите меня в качестве кого я хочу войти. А если такого не предусмотрено, то двойных емейлов в базе быть не должно. Еще на этапе регистрации или смены это нужно отсекать.

                Да и просто если в коде возвращать массив там где должен быть объект, то это считается неправильным, почему в API должно быть по-другому.

                Если мы идем на /search — то ответ подразумевает наличие нескольких элементов.
                Если же это логин, то тут должно быть что-то, типа:
                POST /api/users/authorizations {field:email, data:'user@example.com'}

                хотя если в базе два юзера с одним мылом, вопрос кого логинить остается открытым :-)


      1. VolCh
        20.02.2019 10:48

        1. 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 если нужна история состояний.


        1. michael_vostrikov
          20.02.2019 11:19

          PUT/PATCH /tickets/:id

          Тут возникает проблема, как определить, что мы хотим сделать. В коде будет куча веток if (prevFieldValue != currentFieldValue) runSpecialLogic(), непонятно как показать сообщение "Билет уже активирован". Может пользователь хочет какую-нибудь надпись на активированном билете поменять, а отправляются все поля. Если отправлять только поля, которые надо поменять, то в модели они будут все необязательные. а это тоже не подходит.


          1. VolCh
            20.02.2019 12:11

            Причём тут модель к полям запроса? Сначала валидируем запрос (например, если меняется больше одного поля в одном запросе, то ошибку 400 или 422 выдаём), а потом дёргаем уже нужный метод модели, не prevFieldValue != currentFieldValue, а fieldName == "state" ticket.setState(newFieldValue)


            1. michael_vostrikov
              20.02.2019 14:29

              Ну а как вы определите, что поле изменилось, не используя prevFieldValue?


              Если сравнивать fieldName == "state", то при повторном запросе повторятся все действия в системе — отправка email, начисления/списания.


              1. VolCh
                20.02.2019 14:53

                В сеттере, вернее там определю можно ли установить новое состояние или оно нарушает бизнес-логику. что-то вроде if (this.state === 'draft') { if (newState === 'approved') { this.onApproved() }}


                А вообще, я о том, что запрос к модели весьма слабое отношение имеет. Задача контроллера преобразовать http-запрос в вызов методов модели.


      1. lair
        20.02.2019 14:49

        Есть билеты на некоторое мероприятие, есть понятие бизнес-логики «Активировать билет», выполняются некоторые действия, меняется состояние билета.

        POST /tickets/{ticket-id}/activate


        Причем, что занятно, выгоднее эту урлу не записывать ни в какую документацию сервиса, а отдавать как ссылку внутри самого билета (который получается по GET /tickets/{ticket-id}).


        1. 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 – это всегда про работу с состоянием ресурсов (получение, изменение, удаление и т.д.), а не про вызов методов.


          1. lair
            20.02.2019 16:42

            Не верно, посколько во-первых активация – идемпотентный запрос, а POST – не идемпотентная операция

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


            Более того, если читать определение идемпотентности в RFC 7231, то эта операция — не идемпотентна, поскольку если клиент не получит (успешного) ответа на первый свой POST (например, из-за таймаута), и пошлет второй POST, то он получит ответ "уже активировано", из которого нельзя разумным образом понять, кем и когда активирован билет.


            REST – это всегда про работу с состоянием ресурсов (получение, изменение, удаление и т.д.), а не про вызов методов.

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


            Но я, в принципе, ничего не имею против POST /tickets/<ticket_id>/activations (привет гитхабу).


            1. defuz
              20.02.2019 17:42

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

              Согласен, на счет не-идемпотентности POST я погорячился. Хотя я бы все равно рекомендовал стараться все идемпотентные операции реализовывать через PUT, а не идемпотентные через POST, как правило хорошего тона.


              1. lair
                20.02.2019 18:27

                а не идемпотентные через POST

                … а учитывая, что я считаю эту операцию не-идемпотентной, мое применение POST полностью оправдано.


            1. powerman
              20.02.2019 17:43

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


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


              Но если посмотреть немного глубже, то, формально, потери идемпотентности в этом случае вообще-то нет. Идемпотентность гарантирует одинаковый эффект на сервере после одного или более одинаковых запросов. Она не гарантирует одинаковый ответ сервера на все эти запросы (и, по факту, ответ сервера всегда отличается — начиная с того, что в нём есть текущая дата, и заканчивая тем, что между нашими запросами данные на сервере могли измениться кем-то другим — причём это касается не только идемпотентных, но даже и безопасных запросов). Так что мы можем вернуть ошибку "уже активирован" на второй запрос не теряя при этом свойства идемпотентности. НО. В большинстве случаев это просто создаст лишние сложности на клиенте без какой-либо пользы.


              1. lair
                20.02.2019 18:29

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

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


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

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


                1. powerman
                  20.02.2019 19:27

                  Я внимательно почитал определение в RFC перед написанием предыдущего комментария, просто на всякий случай. Там ничего не сказано про то, что ответ сервера на первый и второй запрос должен быть идентичен. Там говорится исключительно про "intended effect on the server", что абсолютно корректно.


                  1. lair
                    20.02.2019 23:09

                    Idempotent 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.


                    1. powerman
                      20.02.2019 23:26

                      Да, но из этого не следует, что при повторе запроса сервер должен вернуть такой же ответ, как и при первом запросе. Идемпотентность предъявляет требование к состоянию сервера после повтора — оно должно быть таким же, как после первого запроса — но не к ответу сервера.


                      Грубо говоря, запрос "установить значение переменной A в 42" — идемпотентный, потому что после одного или нескольких таких запросов эффект на сервере будет одинаковы: значение переменной A станет равно 42, каким бы оно ни было до того. При этом сервер может отвечать абсолютно что угодно, хоть случайным значением, отличающимся при каждом запросе — это не отменит свойство идемпотентности: возможность безопасно повторить запрос если предыдущий по какой-то причине выполнить не удалось. (Для контраста, запрос "увеличить значение A на 1" идемпотентным не является, даже если сервер всегда присылает один и тот же ответ.)


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


                      Получение при этом идентичного ответа не является критичным — мы всегда можем отдельно запросить у сервера текущее состояние ресурса, можем запросить лог всех изменений ресурса, лог всех отправленных сервером ответов, etc. — всё это возможно реализовать, если оно кому-то нужно.


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


                      1. lair
                        20.02.2019 23:28

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

                        Нет, не следует. Но я этого и не говорил, в общем-то.


                        Я говорил о том, что конкретный сценарий нельзя реализовать идемпотентным с сохранением разумной функциональности клиента.


                        1. powerman
                          20.02.2019 23:34

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


                          1. lair
                            20.02.2019 23:35

                            Мы всё ещё говорим про сценарий "активации" некоего ресурса?

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


                            1. powerman
                              20.02.2019 23:42

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


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


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


                              1. lair
                                20.02.2019 23:44

                                И тут внезапно выясняется, что пользователи-то не аутентифицированные.


                                1. powerman
                                  20.02.2019 23:54

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


                                  Но на практике обычно такую задачу всё-таки решают идемпотентно, просто добавив в запрос дополнительным параметром сгенерированный клиентом UUID запроса — но Вы наверняка скажете что это не честно т.к. в некотором смысле аутентифицирует юзера (хотя это не так — это идентифицирует отдельный запрос) нарушая условие задачи, либо поломается при перезапуске клиента перед повтором запроса. :)


                                  1. lair
                                    20.02.2019 23:58

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

                                    Да.


                                    На первый взгляд у такой задачи просто нет идемпотентного решения

                                    Я про это и говорю.


                                    либо поломается при перезапуске клиента перед повтором запроса

                                    Это основная проблема.


                                  1. michael_vostrikov
                                    21.02.2019 06:40

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


              1. arthuriantech
                20.02.2019 19:37
                +1

                Ничто не запрещает использовать POST вообще для всего, но тогда в использовании REST просто не остаётся никакого смысла.

                roy.gbiv.com/untangled/2009/it-is-okay-to-use-post


          1. powerman
            20.02.2019 17:30

            Первый пример скорее PATCH, нежели PUT.


            1. defuz
              20.02.2019 17:43

              Вы правы, PUT обычно заменяет объект целиком, так что использовать его таким способом нельзя – должны быть указаны все поля.


          1. arthuriantech
            20.02.2019 19:41

            Извините за мой французский, но именование URI не влияет на то, является ли сервис RESTful.


  1. cjbars
    19.02.2019 21:54
    +1

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

    Я своим студентам коды ответов на первой же лекции давал, и очень настоятельно требовал их разумно использовать. Теперь у них в головах недоумение по поводу 200 ОК error:true.

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

    REST вообще очень легко начать использовать не правильно. Начиная от глаголов в урлах, и get запросов на добавление сущности, заканчивая 200 Ок ( все нормально, падаем! )


    1. VolCh
      20.02.2019 10:53
      +1

      REST вообще про статусы и глаголы ничего не говорит, он говорит про унифицированные интерфейсы. Как унифицировать — ваше дело и ваше ответственность, стандарта REST нет.


  1. RouR
    19.02.2019 22:22

    Ошибки валидации на стороне сервера каким кодом будете отдавать?


    1. ilving
      19.02.2019 22:38

      400 Bad Request (что логичнее, если на мой вкус) \ 501 Not Implemented
      Опять-таки, в зависимости от логики приложений. Ну и плюс в body написать что именно не прошло валидацию


      1. UksusoFF
        19.02.2019 23:00

        Ну и плюс в body написать что именно не прошло валидацию

        Некоторые библиотеки для работы с HTTP (например https://github.com/Alamofire/Alamofire) внезапно не отдают body для всего что отлично 200. Поэтому и начинаются велосипеды.


        1. ilving
          19.02.2019 23:10
          +2

          «Некоторые не отдают» или «некоторые отдают»?
          Если первый вариант — то может стоит поискать другую библиотеку?
          Если второй (во что верится слабо) — то стоит поискать другую планету )
          Ну и да, велосипеды на крайний случай


        1. VolCh
          20.02.2019 10:58

          Реально? Из описания этого не следует:


          By default, Alamofire treats any completed request to be successful, regardless of the content of the response.


      1. 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 сервером.


    1. sentyaev
      19.02.2019 23:19
      +2

      422 Unprocessable Entity
      Отлично подходит для валидации.
      Горорит о том, что сервер понял тип запроса (415 Unsupported Media Type в этом случае не вариант) и объект запроса синтаксически корректен (в этом случае 400 Bad Request не вариант), но не смог его выполнить.


  1. bgnx
    19.02.2019 22:39

    А у меня такой вопрос к сторонникам rest-подхода — допустим вы не можете использовать http по причинам слабой производительности и есть необходимость использовать вебсокеты для браузеров (либо tcp для мобильных клиентов) — будете ли вы кодировать в передаваемых сообщениях схему http протокола и дальше в своем привычном режиме организовывать взаимодействие клиента с сервером согласно вашим представлениям rest-а или может вы решите не кодировать в сообщениях всю спецификацию http а ограничить себя каким-то подмножеством или может даже изменить какие-то моменты, или может вообще не будете смотреть на спецификацию http-протокола и закодируете в сообщениях формат общения удобный клиенту и серверу?


    1. powerman
      20.02.2019 00:22

      Почитайте для чего нужен REST. В смысле, какая польза от тех ограничений, которые он накладывает. Вы там обнаружите возможность кеширования на прокси, возможность автоматически повторять идемпотентные/безопасные запросы, etc. Ничто из этого просто не актуально, если не используется HTTP. А, следовательно, нет и смысла притворяться, что у нас тут REST (или HTTP).


      1. VolCh
        20.02.2019 11:03

        Почему не актуально? Вы как разработчик прикладного протокола можете всё это заложить в требования к клиентам, серверам и инфраструктуре. Другое дело, что для соответствия принципам REST вам нужно будет реализовать синхронную модель клиентский запрос-серверный ответ поверх асинхронного дуплексного транспорта.


    1. GrigoryPerepechko
      20.02.2019 02:56

      Долгоживущий сокет на сессию, бинарный rpc протокол (хорошо grpc подойдет). Если яйца из стали — можно libquic подключить к мобильным клиентам, или хотя бы постараться использовать tls1.3 с нулевым rtt на установлении соединения.


    1. VolCh
      20.02.2019 11:00

      Если принято решение использовать REST over WebSockets, то смотреть на спецификацию HTTP в этом случае странновато. Имеет смысл разработать свой прикладной протокол.


      1. Dolbe
        21.02.2019 22:04

        Возможно, товарищ выше имел ввиду, что если реализация Rest уже использует http коды, то будет крайне неудобно переделывать ее под WebSockets.
        С другой стороны, такой переход будет не таким болезненным, если изначально у REST были свои коды ответов поверх HTTP 200.


  1. 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 – это даже не протокол, это просто договоренность


  1. apapacy
    20.02.2019 02:31
    +5

    Задумывались ли Вы кода-нибудбна двум я такими вопросами?
    1. Почему до сих пор нет стандарта RESTAPI (я не называю это REST) в то время как есть стандарты SOAP, oData, json-api, graphql
    2. Почему на 13-м году победного шествия RESTAPI по-прежнему идут горячие споры (не только на форумах но среди реальных разработчиков) какой статус чему соответсвует?


    1. lair
      20.02.2019 12:18
      +1

      У меня есть встречный неудобный вопрос: насколько много пользы вы получаете от "стандарта" SOAP или "стандарта" oData? Например, если вы видите SOAP-endpoint, в каком виде вы будете получать от него ошибки?


      1. apapacy
        20.02.2019 15:29

        Тут нет никакого неудобства. В каждой из приведенных систем вопросы с ошибкой решаются по-своему. Общий подход такой в ответе содержится
        {
        error,
        data
        }
        Чтобы не усложнять ответ я в наших реалиях допускаю частично использовать статус http (и то только потому что на это уже настроены практически все библиотеки которые обращаются по сети к RESTAPI) то есть 200-е ответы — в ответе содержится data без дополнительного уровня вложенности и какой-нибудь из 400-х содержит те случаи когда возвращается значение другого типа (не хочу называть это ошибкой)


        1. lair
          20.02.2019 15:30

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

          Да я вроде конкретно про SOAP спросил.


          1. 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 с подробным описанием.


            1. lair
              20.02.2019 15:54

              Ну так это решается там.

              Угу. Рассказать вам, сколько я видел имплементаций, в том числе государственного уровня, многократно проанализированных и одобренных институциями, в которых soap:Fault не использовались, а был дополнительный внутренний конверт, самописный, конечно же, в котором был статус обработки?


              1. 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>
                


                (Пришлось немного исказить синтаксис т.к. редактор заменяет сущности на текст)
                Не знаю это лень или прокрастинация?


                1. JustDont
                  20.02.2019 16:31
                  +2

                  Не знаю это лень или прокрастинация?

                  Это обычное «да чё я буду с этим транспортным слоем возиться?» (с определенной точки зрения и SOAP будет «транспортным»). Если модель не ложится гладко на схему коммуникаций — тем хуже для схемы коммуникаций!
                  Именно поэтому стандарты коммуникаций у нас либо простые, как палка, и натягиваемые на что угодно (json-rpc), но одновременно не уточняющие никаких деталей, и внутри всё равно будет местячковый колхоз, либо сложные, навороченные, и всё равно требующие серьезного внутреннего описания, чё ж мы собрались передавать и в каком порядке (oData, graphQL, итд). А в этом внутреннем описании можно конечно же нагородить колхоз.

                  REST в этом смысле хотя бы честен — все знают, что под REST каждый понимает что-то хотя бы немного своё, и что в итоге будет местячковый колхоз.


                1. lair
                  20.02.2019 16:44

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


                  "Так зачем платить больше?"


  1. xPomaHx
    20.02.2019 08:54
    -1

    Для меня аргумент что всё ответы рест должны быть 200, это то что в коде обращения к ресту если там не 200 будет бросаться исключение, а значит будет ветвление кода с помощью try catch, а это антипатен.


    1. Ogra
      20.02.2019 09:55
      +3

      У вас всегда должна быть обработка ошибок — 4хх/5хх может прилететь в любой момент, сервер может быть тупо недоступен, у клиента может отвалиться сеть, ну и так далее. Да, даже если вы обмениваетесь информацией между двумя серверам в одном датацентре, у вас может пропасть линк между ними, бывали случаи. Так что или try/catch или его аналоги должны быть в любом случае.
      И тут мы приходим к простоте — если сервер ответил 200, значит все хорошо, запрос прошел от клиента к серверу и выполнился как положено. Если же мы получили какую-то ошибку — запрос надо залогировать/повторить/отклонить.


      1. xPomaHx
        20.02.2019 10:08

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


        1. cjbars
          20.02.2019 10:48

          Если просто нет контента, это не ошибка, это нормальная ситуация. Поэтому код 404 Not Found это тоже не ошибка, это нормальная ситуация, и разруливаем мы ее одинаково.


    1. MikailBag
      20.02.2019 10:47

      Можно настроить HTTP-библиотеку, чтобы она не кидала исключения.


    1. lair
      20.02.2019 12:19

      это то что в коде обращения к ресту если там не 200 будет бросаться исключение

      Совершенно не обязательно.


      значит будет ветвление кода с помощью try catch, а это антипатен.

      Или, наоборот, будет прекрасный паттерн "если код выполнился без исключений, значит, он выполнился успешно".


  1. flancer
    20.02.2019 09:42

    Но с 500 вы получили профит — осознание того, что это НЕОЖИДАННАЯ ошибка.

    Получается, что ожидаемые ошибки можно прокидывать с 400-м кодом или даже с 200-м?


    1. apapacy
      20.02.2019 10:20

      Тут можно сразу спросить ошибка чего? Ошибка веб сервера? Ошибка разработчика? Или ответ приложения?
      404 статус — это нет api на сервере или не найден скажем товар с идентификатором
      401 статус это у вас с клиента не пришел заголовок с токеном авторизации или пользователь с идентификатором указанном в токена заблокирован или удален

      Ошибка уровня приложения это не ошибка как таковая а ответ другого типа. Например сервис Найти пользователя по идентификатору может дать ответ типа Пользователь а может дать ответ о мысленный принадлежащий другой у типу данных: пользователь не найден, не задан идентификатор пользователя, а также реальные ошибки: сеть недоступна, сервер не отвечает, нет запрашиваемого api, вызов сервиса завершился с программной ошибкой, тайм-аут соединения, тайм-аут на прокси,


      1. cjbars
        20.02.2019 10:51

        Тут можно сразу спросить ошибка чего? Ошибка веб сервера? Ошибка разработчика? Или ответ приложения?


        А какая разница? Мне как серверу, который работает с вашим api совершенно до лампочки кто мне ответил кодом 404: прокси, web-server, приложение, еще много кто может прислать мне 404. Мне как серверу понятно — Здесь рыбы нет.


  1. justboris
    20.02.2019 12:01
    +1

    Особенность этой ситуации в том, что как API ни проектируй, с использованием статус-кодов, или по принципу «всегда 200», в конечном счете все будет более-менее работать нормально. Напишем на клиентской стороне обертку, которая конвертирует ответ во что-то более удобное – и дело с концом.

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


    1. VolCh
      20.02.2019 12:13

      Просто странно выглядит, когда API вовсю использует мощь HTTP как протокола прикладного уровня, но вот только на статус-коды забивает.


  1. beduin01
    20.02.2019 15:06

    Судя по тому что в REST столько неоднозначностей это очень плохая технология (если ее таковой можно назвать).
    Видимо будущее за чем-то более вменяемым типа gRPC.


    1. VolCh
      20.02.2019 16:45
      +1

      Именно, что нельзя REST называть технологией — это архитектурные принципы. А gRPC — конкретная технология. Они в разных измерениях.


    1. powerman
      20.02.2019 18:09

      REST отлично подходит, например, для простенького файл-сервера. Например, хостинг картинок. Там очень пригодятся основные плюшки 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 было естественным.


  1. boblenin
    21.02.2019 18:46

    Код 200, код 200. Много ли «типа REST» приложений приедрживается того, что GET не должен изменять состояние ресурса, к которому обращаются? Я даже не говорю о внутренней кухне типа логирования или отслеживания поведения пользователя, доводилось работать с тем, что — /resource/{id} создают новую пустую запись, если по данному id ничего не найдено.


    1. powerman
      21.02.2019 18:58
      +2

      Как ни странно — много. Любители удалять записи GET-запросами почти перевелись много лет назад, после того, как по их url-кам прошёлся любопытный паук гугла.


    1. vintage
      22.02.2019 09:26
      +1

      /resource/{id} создают новую пустую запись, если по данному id ничего не найдено.

      С точки зрения клиента ресурс не меняется. Запрос полностью идемпотентен. Что там конкретно происходит в памяти сервера REST ни коим образом не регламентирует.