Самое массовое заблуждение вокруг REST-архитектуры

    Столкнувшись на одном крупном ресурсе для разработчиков с неправильной реализацией REST-архитектуры, я, вслед за компанией Intel, развенчивающей мифы о законе Мура, хочу обратить внимание, на самую частую ошибку при реализации RESTful приложений, нарушающую целостность сети. При чем грешат практически все разработчики, веб-фреймворки и даже очень популярные ресурсы (например GitHub), но...


    Начну с небольшой предыстории. Разрабатывая для сайта авторизацию через Oauth, на очередной запрос, мне пришел ответ “404 Not found”. Ничего страшного, подумал я, сверился с документацией, посмотрел в код и удивился – 404-й ошибки быть не должно. Как вы уже догадались, далее я проверил каждую строчку документации и своей программы и потратил некоторое время на поиск ошибки. Когда я ее обнаружил, то был рассержен, и на себя и на разработчиков github. В чем же было дело?

    Проблема заключалась в том, что в коде и документации не совпадал HTTP-метод, который был указан в запросе. Постойте! Ведь URL – это идентификатор (точнее его подвид), а значит ни при каких обстоятельствах я не должен был получить 404-ю ошибку (именно эта уверенность меня и подвела). В крайнем случае, я ожидал получить 400-й заголовок “Bad Request”, но уж точно не 404!

    Что же должно было произойти на самом деле? В стандарте для этого случая есть HTTP-статус 405 “Method Not Allowed”, в этом случае сервер обязан отправлять с ответом список допустимых методов. Стандарт не уточняет в каком виде должен быть отправлен этот список, но, очевидно, в том виде, в котором клиент сможет его прочесть с учетом заголовка “Accept”.

    Как вы уже догадались сегодня тяжело найти веб-фреймворк или ресурс, который бы не повторял эту ошибку, больше свойственную новичкам, нежели суровым профессионалам. По-моему, личному мнению, особенно ярко и, возможно, даже комично это смотрится на фоне развернувшейся во времена популяризации REST-архитектуры борьбы за стандартизацию HTML с высмеиванием Microsoft – HTTP к тому моменту уже был стандартизирован. Результатом этого – возможная потеря обратной совместимости узлов сети, а сегодня к невозможности быстрой интеграции современных продуктов в программные комплексы в виду необходимости индивидуальной настройки каждого такого приложения.
    Поделиться публикацией

    Похожие публикации

    Комментарии 31

      +19
      Всё очень просто: люди, использующие термин «REST», в большинстве своём попросту не знают, что такое REST и используют его как синоним для «API, работающее поверх протокола HTTP».
        +4
        Да, но как раз таки реализация в веб-фреймворках говорит о не понимании этого принципа и не знании стандартов разработчиками, а не обывателями.
          +5
          А я про разработчиков и говорю.
            +2
            HTTP только в качестве транспорта — и жизнь становится гораздо проще
              0
              * На самом деле нет.
                +1
                Ну ок :)
        +6
        Нет никаких массовых заговоров заблуждений, есть только локальные.
        В REST нет методов (точнее они есть, это GET, PUT, POST ...), а URI это идентификатор ресурса.
        Вы запросили неверный ресурс и вам ответили «Not found» (404).
          +4
          Как я уже написал выше ресурс верный, метод – нет. Это все равно, что при вызове метода объекта в коде, вы вместо отсутствия метода получите сообщение об отсутствии самого объекта.
            +3
            Приведите конкретный пример.
              0
              Неверное сравнение, HTTP-методы это не методы объекта, а фиксированный (CRUD) набор операции над объектом в хранилище.
              Ресурсы обычно делят на коллекции (collections) и элементы (items).
              Групповые операции PUT collection и DELETE collection обычно не реализуют, в частности потому что они могут привести к неправильному использованию.
              Аналогично может быть не реализована операция POST collection/item, т.к. не всегда можно однозначно описать ее поведение.
              Соответственно, если вы вызвали не ту операцию на не том объекте, вам в ряде случаев могут вернуть описанную ошибку.
                +1
                Согласен с автором статьи.

                Возвращается «404 Not found» — но ресурс (коллекция, элемент) ведь есть и это вводит в заблуждение.
                По логике вещей должен вернуться, например, «405 Method Not Allowed» — ресурс (коллекция, элемент) есть, но метод к нему не применим — не реализован или запрещён.
                  –1
                  Все зависит от состояния объектов и логики.
                  Если метод не реализован в принципе, логично что серверный NotImplementedException будет конвертироваться в "Method Not Allowed" (для 3 описанных случаев лучше так и делать).
                  Но если использование метода подразумевается при определенных условиях, то можно вернуть "Not found"
                  Например: можно запросить GET /issues и получить пустой список, а потом вызвать DELETE /issues и получить "Not found", хотя можно вернуть и 200
                  Из статьи же следует, что 404 нельзя использовать, иначе попадешь в ад.
                    +2
                    Из статьи не следует, что 404 нельзя использовать. У вас какая-то неправильная статья. Из той статьи, что идет над комментариями, следует, что сервер должен отвечать адекватно, а не как попроще. Если магазин не принимает пластиковые карты, какой ответ вы ожидаете услышать — «покупки? какие покупки?» или «мы не принимаем пластик»? Чем строже следовать спекам и чем адекватней программировать реакции, тем меньше головной боли сторонним разработчикам (и вам в будущем, когда вы через полгода откроете свой код и будете вспоминать, в каких же случаях ваш сервер вернет 666...). И ведь вы сами правильно написали, что «все зависит от состояния объектов и логики»…

                    P.S. Почему DELETE /issues должен вернуть ошибку? Если я говорю rm /foobar, я хочу удалить директорию (легко — директория существует), а если rm /foobar/*, то файлы в ней (тут можно вернуть ошибку, если в локальных спецификациях прописано, что у DELETE должен быть существующий аргумент (в RFC, ЕМНИП, нет такого положения), а можно 200/204/etc, ибо нет человекаресурса — результат удаления достигнут; чтобы API был православней, можем вместо DELETE /issues/* в аналогичном HTTP-запросе писать DELETE /issues/all).
                      0
                      Да, пример с DELETE получился некорректный.
                        0
                        В частности вот тут developer.github.com/v3/troubleshooting/ есть описание почему на существующем репозитории может возвращаться 404. Все дело в логике приложения.
                          –2
                          Кстати, тоже спорное решение. Понятно, что они не хотят «provide any information about private repositories», но почему именно 404, а не 403 на абсолютно любые запросы (даже невалидные — пофиг, мы тебя не знаем, и поэтому запрос обработчику не отдадим), касающиеся приватных репозиториев? Хоть натолкнуло бы разработчиков, которым влом читать инструкции и FAQ, на мысли (кстати, для совсем уж красивого решения можно было через кастомный заголовок отклика написать — мол, авторизируйся, а потом стучись). Если только это не паранойя, такая же, как в форме аутентификации вместо ошибок «пользователь не найден» и «неверный пароль» писать «фиг знает, где ты накосячил, но я тебя не пущу». Безопасность, защита от перебора и все такое, но пользователей отпугивает.
                            +4
                            Если только это не паранойя, такая же, как в форме аутентификации вместо ошибок «пользователь не найден» и «неверный пароль» писать «фиг знает, где ты накосячил, но я тебя не пущу». Безопасность, защита от перебора и все такое, но пользователей отпугивает.

                            Вот тут вы сильно не правы. Это паранойя не просто так взялась, она очень оправдана практически. Это называется уязвимость типа padding oracle, случай, когда мы «слишком сильно детализуем» ошибку, вместо того, чтобы просто сказать «ошибка, хрен знает где, иди нафиг». Более того, популярный timing attack тоже работает на этом — просто для того, чтобы узнать, какая из проверок выдала ошибку, используется не само сообщение об ошибке, а то, с какой задержкой оно появляется: чем позже, тем более поздняя проверка сработала. Что тут говорить, в некторых вузах (Стэнфорд) каждый студент, обучающийся по программе безопасности, пишет программу, способную сломать систему с подобной уязвимостью. Это настолько не мифическая уязвимость, что её используют в качестве учебного примера!

                            Так что это нормально, если мы говорим о безопасности, ошибка должна быть одна и та же, и до её вывода должно проходить одно и то же время (а в идеале — и другие аспекты поведения системы должны быть неотличимы), вне зависимости от того, в каких именно входных данных была ошибка. А для веб-сервера что метод, что uri, что версия http, что заголовки, что тело запроса — всё одно, входные данные.
                    –3
                    REST – это частное применение RPC поверх HTTP. Изучите стандарт и посмотрите, как пример, тот же WebDAV – он определяет новые методы для работы с файлами через HTTP. И реализация REST не может противоречить стандарту на котором базируется, а должна полностью ей соответствовать. Но даже, если посмотреть с вашей точки зрения на веб-приложение как на хранилище, то объясните мне как возможно, что id – верный, объект в базе есть, в моем случае это /user, но когда я пытаюсь объект сохранить (POST), а не получить (GET) я получаю собщение, что пользователь с таким id не найден?
                      +3
                      А при чем здесь RPC? RPC и REST просто разные подходы для решения одних и тех же проблем. Говорить что одно является полным подмножеством другого не совсем корректно.
                      RPC подразумевает выполнение команд на сервере, повторное выполнение одной и той же команды может возвращать разный результат, результаты могут зависеть от текущего состояния системы, предыдущих вызовов, конкретных параметров сессии. Описание методов, транспорта и всего прочего остается на совести разработчика и прочие радости поддержки совместимости, сериализации и т.д…
                      REST это прежде всего стиль архитектуры приложения и набор соглашений о поведении, он появился для того чтобы упростить жизнь разработчиков. Основываясь на HTTP он не обязан ему на 100% соответствовать. Вы можете не использовать HTTP-методы и передавать их в теле запроса, аналогично с ошибками возвращать всегда 200 OK и внутри уже указывать error code. Можете использовать PUT или PATCH, можете передавать offset и limit в качестве параметров, а можете использовать http range. Конечно все это делают не от хорошей жизни, а чтобы обойти существующие ограничения.
                      GitHub для сохранения (а точнее обновления объектов) использует PATCH, а POST используется для создания новых объектов. Применение POST к существующему пользователю скорее всего не определено и тут логично было бы возвращать «Method Not Allowed». Иначе вам бы пришлось указывать полный список параметров для переопределения/перезаписи пользователя, и не совпадение login или id могло привести к результату «Not found». Из теории заговоров можно предположить, что есть еще какое-то служебное api, где подобный метод доступен. :)
                        +1
                        Еще логичней при использовании POST на существующем пользователе возвращать 409 Conflict
                    0
                    Список допустимых методов в теле ответа возвращать нельзя (есть риск поломать такими «ответами» клиентов), и заголовок «Accept» тут ни при чем.
                    В приведенной вами ссылке стандарт уточняет, что для возврата списка методов нужно использовать заголовок «Allow»
                    404 Not Found
                    Allow: HEAD,GET,PUT,DELETE,OPTIONS
                    

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

                    +13
                    Руки потянулись написать статью «Самое массовое заблуждение вокруг XML», но дал себе по рукам. Не хочу продолжать неделю самых массовых заблуждений.
                      +18
                      А о чём была бы эта статья? О том, что XML для машин, а не для людей?
                      +8
                      А это не только всяким (псевдо-)REST свойственно. Очень редко вспоминают, что есть что-то еще, кроме 404 и 500.
                        +3
                        Если появится третья статья про заблуждения, то нужно будет писать мета-статью: Массовое заблуждение отдельных авторов относительно массовости заблуждений. Почитайте RESTful Web APIs, там очень хорошо написано про стандарты.
                        Спойлер
                        Подобно ложке, их не существует.


                        В данном конкретном случае — я на вашей стороне. Действительно, получить 405 было бы приятнее. Но нельзя требовать этого. Некоторые библиотеки вообще всегда отвечают 20x, а код ошибки посылают в теле ответа. По-вашему, в них вообще всё неправильно? REST — не про это. А проблемы HTTP — совершенно другая тема.
                          0
                          Spring, Jersey фреймворки для Java всегда возвращают 405 ошибку в таких случаях.
                            0
                            Спасибо за мотивацию прочитать классику: диссертацию Thomas Fielding (кто придумал REST) и стандарт HTTP 1/1 (RFC 2616 Fielding, et al.). Рекомендую. Изначально REST описывает интерфейсы взаимодействия с набором ограничений (глава 5.1). Что такое методы и ресурс четко не описывается. Потом предлагается применить REST для HTTP (глава 6). Тогда появляется понятие идентификатора ресурса — URL и метода интерфейса — GET, POST,… Методы можно расширять (глава 6.3.1.2). Так как клиент может не знать про новые методы, то код HTTP ответа используется для информирования о состоянии выполнения. В частности, в RFC ошибка 405 Method not allowed как раз предполагает, что запрошенный метод GET, POST, HEAD и любой другой «расширенный» не доступен в данном интерфейсе и сервер должен указать список доступных методов в Allow заголовке. Если метод поддерживается, но ресурса с таким идентификатором нет (или идертификатор неправильный), то по стандарту надо отдавать 404. Как удобнее, вам виднее, но в классике дожно быть так!
                              0
                              Для ситуации с неправильным URL, кстати, подходит 410 Gone «ресурс не найден и никогда не будет найден»
                                +2
                                410 — это ресурс был доступен ранее, но затем был удалён и больше уже доступен не будет.
                              0
                              Если честно, заголовок статьи желтоват. Ведь по сути это не проблема REST или чего угодно, просто одни разработчики не внимательно вникают в стандарты, а другие смотрят на «старших» товарищей и делают «как у них». Если по существу вопроса, то да, 404 должна возвращаться только при отсутствии ресурса, более того, если мы запрашиваем коллекцию в которой нет элементов, то даже в этом случае это 200ОК и пустой список, а вовсе не 404. Все правильно автор написал о том, что должен был быть 405 и список разрешенных методов, более того «разрешенность» может быть основана как на правах доступа (например, не админ не может удалить коллекцию), так и на просто нереализованности конкретного метода для ресурса. Более того, для каждого ресурса гоже также реализовать метод OPTIONS, чтобы клиент мог заранее узнать о своих возможностях приминительно к конкретному ресурсу. В итоге, могу сказать, что концепция REST'а прекрасна, но хороших реализаций мало, а идеально правильных просто нет. Всем кому интересна тема советую статьи и презы ребят из apigee.

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

                              Самое читаемое