Как стать автором
Обновить

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

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

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

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

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

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

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

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

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

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


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

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

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

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

Если вы завязываете важную логику на один только голый 404, то потом и не обижайтесь, что у вас эта важная логика отработала в том числе и когда не следовало.
Я не завязываю и старательно рекомендую никому не завязывать. Всюду, где я могу повлиять на API, используются коды, которые не могут прилететь с «голого nginx'а».
Срачи разжигаются и целые статьи пишутся из-за чей-то лени прочитать спеку того, чем пользуется:
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.
Почему-то все всегда забывают про замечательный код 410.
Не понимаю, зачем городить свой велосипед, когда коды настолько нативны, просты и логичны.
тогда придётся помнить, а был ли такой клиент. Да было бы замечательно, но нет, оно не работает.

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

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

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

2. Клиент — Сервисный слой — Микросервис
Вот здесь Микросервис отдает Сервисному слою все те же базовые коды. А вот сам Сервисный слой уже может решить, а насколько данная ошибка является ошибкой бизнес-логики. И отработать, может быть даже вернув 200 с текстом: «По вашему запросу ничего не найдено». Так как это будет не ошибка, а корректный ответ этого сервисного слоя.
Абсолютно согласен. Ошибки сервера нужно отделать от ошибок бизнесс-логики. В конце концов, 404 может означать отсутствие самого сервиса, а не запрошенного ответа.
В наших проектах код 200 просто означает «у меня есть валидный JSON от REST сервиса», во всех остальных случаях означает, что требуется технический анализ ошибки.
Смешались в кучу кони, люди…
Сервис на запрос isUserExists должен отвечать только true или false, если для ответа используются стандартные HTTP-коды, то это плохой сервис, гоните его. HTTP-коды придуманы чтоб сообщить состояние(запроса?), вы запрашиваете не состояние сервера или запроса, у вас вполне конкретный запрос, на который не может быть другого ответа, кроме как true и false.
Так подождите, что должен отвечать православный REST API на запрос ресурса, которого не существует?

GET /client/434343

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

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

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

Вы правы, именно для этого в стандарте предусмотрен метод 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.
Это вы смешали коней и людей REST и «RPC over HTTP».

В REST нет никаких запросов вида «isUserExists который возвращет true/false», а есть стандартные (определенные заранее) запросы к ресурсам и такие же стандартные ответы на них.
Тут лучше употреблять «унифицированные», а не «стандартные», а то кто-то может войти в заблуждение и решить, что есть какой-то стандарт REST
НЛО прилетело и опубликовало эту надпись здесь
Окей, просто тогда это уже не соответствует REST. Для получения информации о существовании ресурса, но не самого ресурса используется запрос HEAD (вместо GET), и да, он должен поддерживаться любым веб-сервером.
Это не соответствует HTTP, а не REST. REST ничего о методах не говорит, кроме того, что способы должны быть унифицированы.
Мысли может в статье-то и правильные (хотя называть HTTP транспортным – сильно), но вот стиль изложения никуда не годится. Больше похоже на большой комментарий, чем на полноценный материал.
Поэтому, стараюсь лишнего не писать:) Не мню себя профи эпистолярного жанра. И вы правы, по сути, это сборник комментов которые я в очередной, и очередной раз вынужден выдавать. Причем, вынужден в прямом смысле. По долгу службы. И будет как минимум «удобно» давать теперь ссылку на статью :)
Спасибо! Не видел ранее. Коротко и ясно.
К сожалению это не всегда помогает. Статья не столько о том «как правильно», а о том, почему эти правила нельзя игнорировать, и убеждение разработчика что он может городить свои правила в собственном царстве скорее всего не верны (потому что помимо приложения разработчика есть еще огромной инфраструктурный зоопарк, полагающийся на эти коды для коректного и/или эфективного функционирования).
Прям вот в точку. Каждый видит «слона» с одной стороны.
> Отсюда следует простой, очевидный вывод — все, что присуще http, присуще и REST. Это неотделимые сущности. Нет отдельного заголовка REST, нет даже намека на то, что REST это REST.

rest отделим от http, а http от rest нет. Потому что http построен на базе rest.

Собственно выбор отдавать статусы или нет, использовать все методы HTTP семантически (читай — в соответствии со стандартом) или ограничиться GET/POST, а то и только POST (или только GET) — это выбор между использованием в нашей REST-архитектуре REST-возможностей HTTP, заложенных в нём by design, или просто использование HTTP в качестве тупого транспорта.

К сожалению, автор ведёт себя как воинствующий невежа — видит только часть общей картины, вцепился в неё из всех сил, не слышит когда ему пытаются показать ситуацию с другой точки зрения и продолжает повторять одно и то же (и его 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…

REST не такая уж конкретная вещь, это лишь принципы. И только если решили, что будем приложение строить на этих принципах, и будем использовать HTTP в нём не просто как транспортный протокол, а как прикладной, во многом на тех же принципах построенный, то тогда и следует стараться возвращать ошибки предусмотренным этим протоколом способом. Просто чтобы уменьшить вероятность неожиданного для разработчиков, пользователей, эксплуататоров и т. п. поведения.
Вообще говоря, REST никоим образом не связан с CRUD. Эти вещи могут использоваться как вместе, так и независимо.
Мне другой вопрос не дает покоя. Есть ресурс /user/{id}, который по id отдает пользователя. Что должен вернуть такой запрос, если пользователь с заданным id не найден, 404? Если так, то это вводит в ступор, не понятно это пользователя не существует или ендпоинт неправильно указан. Возвращать 500 тоже сурово как-то.
Ответ сильно зависит от логики приложений клиента и сервера.
204 No Content, например — если кривой id это валидная ситуация
Или все-таки 404, если на линки типа /user/{id} можно попасть только вбив руками кривой id
Есть такой паттерн в разработке «возвращать всегда список». Не готов обсуждать верный ли он для всех случаев, смысл в нем простой: возвращается всегда список. Типа этого

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

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

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

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

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


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

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

— Есть пользователи, обращение идет по id, но в некоторых сценариях использования надо достать пользователя по email.
— Есть уникальные длинные хеши, надо достать связанную сущность по хешу, связь 1: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. Ну это тогда получается нелогично, не соответствует модели данных. 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 и архитектурой с соответствующими сложностями в поддержке.

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

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


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

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

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

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

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

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

хотя если в базе два юзера с одним мылом, вопрос кого логинить остается открытым :-)
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 если нужна история состояний.
PUT/PATCH /tickets/:id

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

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

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


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

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


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

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

POST /tickets/{ticket-id}/activate


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

Не верно, посколько во-первых активация – идемпотентный запрос, а POST – не идемпотентная операция, а во-вторых «аctivate» — это действие, а не ресурс. Так что:

PUT /tickets/{ticket-id}
{'activated': true}


Или

POST /activations-queue
{'ticket_id': ...}


Или

PUT /tickets/<ticket_id>/activation

REST – это всегда про работу с состоянием ресурсов (получение, изменение, удаление и т.д.), а не про вызов методов.
Не верно, посколько во-первых активация – идемпотентный запрос, а POST – не идемпотентная операция

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


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


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

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


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

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

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

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

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


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


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

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

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


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

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

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

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.

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


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


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


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


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

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

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


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

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

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

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

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


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


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

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

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


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

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

Да.


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

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


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

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

Так а куда его «активировать»-то, если пользователь анонимен? Что означает эта «активация»? Что в результате неё изменится (на сервере, на клиенте)?
Можно привязывать к заказу. У заказа часто несколько шагов, и состояние хранится на сервере, купон является как бы отдельным товаром. Это тоже уникальный идентификатор, но не конкретного запроса, а сессии. Его можно хранить между перезапусками клиента.
Ничто не запрещает использовать POST вообще для всего, но тогда в использовании REST просто не остаётся никакого смысла.

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

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

Вы правы, PUT обычно заменяет объект целиком, так что использовать его таким способом нельзя – должны быть указаны все поля.
Извините за мой французский, но именование URI не влияет на то, является ли сервис RESTful.
Спасибо вам за статью. После срача в прошлой статье меня тоже подрывало написать подобную, вы опередили.

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

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

REST вообще очень легко начать использовать не правильно. Начиная от глаголов в урлах, и get запросов на добавление сущности, заканчивая 200 Ок ( все нормально, падаем! )
REST вообще про статусы и глаголы ничего не говорит, он говорит про унифицированные интерфейсы. Как унифицировать — ваше дело и ваше ответственность, стандарта REST нет.
Ошибки валидации на стороне сервера каким кодом будете отдавать?
400 Bad Request (что логичнее, если на мой вкус) \ 501 Not Implemented
Опять-таки, в зависимости от логики приложений. Ну и плюс в body написать что именно не прошло валидацию
Ну и плюс в body написать что именно не прошло валидацию

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

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

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


By default, Alamofire treats any completed request to be successful, regardless of the content of the response.
Давайте вместе почитаем доку:
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 сервером.
НЛО прилетело и опубликовало эту надпись здесь
А у меня такой вопрос к сторонникам rest-подхода — допустим вы не можете использовать http по причинам слабой производительности и есть необходимость использовать вебсокеты для браузеров (либо tcp для мобильных клиентов) — будете ли вы кодировать в передаваемых сообщениях схему http протокола и дальше в своем привычном режиме организовывать взаимодействие клиента с сервером согласно вашим представлениям rest-а или может вы решите не кодировать в сообщениях всю спецификацию http а ограничить себя каким-то подмножеством или может даже изменить какие-то моменты, или может вообще не будете смотреть на спецификацию http-протокола и закодируете в сообщениях формат общения удобный клиенту и серверу?

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

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

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

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

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


Вот вы правильно начали.

Есть слои (описанные в ISO/OSI) И http согласно стандарту – это уровень приложения. Но в современном фронте http часто используют как транспортный/сессионный уровень. Старый прием, инкапсуляция протоколов. Например, сейчас стек RS-485/MODBUS часто инкапсулируют в TCP/IP ибо так проще, чем тянуть физический RS-485. И при инкапсуляции важно, что вышестоящий уровень не зависит от предыдущего. В нашем случае MODBUS должен работать на своем уровне, а не слать NACK на TCP пакеты протокола, в который он инкапсулирован.

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

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

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

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

Ну так это решается там. В 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 с подробным описанием.
Ну так это решается там.

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

Да знаю что это 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>


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

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

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

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


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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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 было естественным.

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

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

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

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории