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

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

НЛО прилетело и опубликовало эту надпись здесь
RPC тяжело кэшировать и масштабировать. Чтобы раскидать по шардам — надо прочитать ответ и вычленить из него данные. Чтобы промежуточная прокси узнала, можно ли положить ответ в кэш — аналогично. А уж идемпотентна ли операция из сигнатуры запроса вообще никак не узнать.

Идея REST заключается строго в следующем: есть метаинформация запроса (http-коды, методы, URL, заголовки), давайте построим систему, в которой все агенты умеют их понимать и трактовать. Т.е. например если метод GET — значит, запрос немодифицирующий, можно его префетчить, как-то так.

Никто не мешает реализовать RPC-интерфейсы поверх HTTP, получая его семантику там, где она нужна. Более того, большинство из так называемых "RESTful API" именно так и делают — они представляют собой ориентированный на данные CRUD RPC, а термин REST используется сугубо как популярный баззворд.


Идея REST заключается строго в следующем: есть метаинформация запроса (http-коды, методы, URL, заголовки), давайте построим систему, в которой все агенты умеют их понимать и трактовать.

Центральная идея REST — Uniform Interface, неотъемлемой частью которого является т. н. HATEOAS: REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state. Филдинг подтверждает это в том числе в своем посте REST APIs must be hypertext-driven

> Никто не мешает реализовать RPC-интерфейсы поверх HTTP, получая его семантику там, где она нужна. Более того, большинство из так называемых «RESTful API» именно так и делают — они представляют собой ориентированный на данные CRUD RPC, а термин REST используется сугубо как популярный баззворд.

Абсолютно ничто не мешает, кроме того факта, что HTTP как-то знает и трактует большое количество ПО в мире, а под ваш протокол придётся написать кастомные имплементации.

> Центральная идея REST — Uniform Interface, неотъемлемой частью которого является т. н. HATEOAS: REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state. Филдинг подтверждает это в том числе в своем посте REST APIs must be hypertext-driven

Этот пост Фьелдинг написал спустя восемь лет после своего дисера — когда REST уже давно отправился в свободное плавание как концепция. Если б Фьелдинг что-то подобное написал в 2000, недалеко бы его дисер разошёлся.

REST по Фьелдингу-2000 небесспорная, но стройная концепция. REST по Фьелдингу-2008 попросту не существует.
Абсолютно ничто не мешает, кроме того факта, что HTTP как-то знает и трактует большое количество ПО в мире, а под ваш протокол придётся написать кастомные имплементации.

Как именно "большое количество ПО" должно трактовать какой-нибудь отдельно взятый HTTP API? Что конкретно имеется ввиду? Возьмем, к примеру, несколько примеров RPC-like API:
https://developer.twitter.com/en/docs/api-reference-index.html
https://api.slack.com/methods
https://www.flickr.com/services/api/
https://vk.com/dev/methods
https://core.telegram.org/methods
https://developers.google.com/youtube/v3/docs/
https://www.dropbox.com/developers/documentation/http/documentation
Здесь есть какие-то проблемы с трактовкой HTTP или еще нет?)


REST по Фьелдингу-2000 небесспорная, но стройная концепция. REST по Фьелдингу-2008 попросту не существует.

Не существует такого разделения. В чем именно состоит разница между REST 2000 и REST 2008?

> Как именно «большое количество ПО» должно трактовать какой-нибудь отдельно взятый HTTP API? Что конкретно имеется ввиду?

Имеется в виду следующее: когда вы пишете proxy_pass в конфигурации nginx — nginx начинает как-то трактовать стандарт. Он не пересылает запрос как есть. То же касается, скажем, используемого на клиенте фреймворка работы с сетью, и особенно API Gateway-ев, без которых микросервисную архитектуру не построишь.

> Не существует такого разделения. В чем именно состоит разница между REST 2000 и REST 2008?

В 2000 Фьелдинг написал абстрактно. Под «hypermedia as the engine of application» можно много чего понимать. Собственно так и вышло — каждый немедленно истрактовал REST в свою степь.
Имеется в виду следующее: когда вы пишете proxy_pass в конфигурации nginx — nginx начинает как-то трактовать стандарт.

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


В 2000 Фьелдинг написал абстрактно. Под «hypermedia as the engine of application» можно много чего понимать.

Много ли людей вообще знают о существовании этой диссертации, брендируя свой API вирусным термином REST? Мало кто опирается на первоисточники, обычно все заканчивается чтением коротких статей и заметок на персональных блогах которые вместо пруфлинков содержат только ИМХО их авторов.


Между прочим, вы знаете, что означает название "Representational State Transfer"?

Ну вот да, если использовать http-заголовки «для всего» — совершенно непонятно, на каком слое абстракций произошла проблема. Допустим, сервер прислал 500 ошибку. Что же это может быть?
1. Нас отфутболил балансировщик;
2. Упала какая-то нода;
3. Упала какая-нибудь очередь сообщений;
4. Упал конкретный целевой сервис;
Где искать проблему? По сопроводительному тексту? А надо ли выдавать пользователю трейсы конкретного сервиса? (нет, нельзя, коммерческая тайна и весь фарш)

По ответу вида
HTTP:200 {
  "service": "myservice",
  "datetime": "22.22.2222 22:22:22",
  "status": "SERVICE_ERROR",
  "response": {}
}

Хотя бы понятно где локализована проблема, нет необходимости перерывать лишние гигабайты логов (и хорошо если логи в одном месте). Хотя это очень не нравится фронтовикам, которым очень хочется написать один единственный обработчик всех ошибок вида if (status != 200) {}.

Вся задача вывода этих статусов/ошибок/кодов — выдать максимум информации для облегчения отладки, в наименее полезной для потенциальных хацкеров виде. Одним статусом не ограничишься.
Абсолютно ничего не мешает ровно те же данные и в заголовки записать.
Разница между тем, в какую часть ответа писать данные, заключается только в том, о чем я написал: манипулировать заголовками на уровне прокси гораздо проще и удобнее, нежели телом ответа, в т.ч. и логировать проще.

А на клиенте часто наоборот — из тела проще извлечь. И фронтовики нынешние о прокси часто и не слышали, если это не nginx перед апп-сервером на бэке

А вот не факт. К примеру, в C# если тело ответа уже прочитано — его не получится прочитать ещё раз, если только оно не было буферизовано. А если API позволяет, в том числе, передавать многомегабайтные файлы — то буферизация тела ответа есть плохая идея… Всё это мешает писать мидлвари для задач вроде авторизации.

Я специально уточнил — "часто" :) Опять же от специфики проекта зависит. Я больше про UI клиентов.

А на C# что, UI не пишется?

А пишется? Думал перестали давно, в 2014-м, когда перевели в фазу поддержки десктопную часть .Net и всюду прорывались вопли в стиле "MS опять нас подставила, всё что нажито непосильным трудом нужно переписывать теперь на client-server с клиентом на HTML+JS". "Прорывались" в смысле появлялись посты и комменты в неспециализированных на десктоп "пабликах". За специализированными не слежу с момента как MS сказла что-то типа "VC++/MFC больше не основной способ разработки UI для Windows" и я ушёл в веб-приложения на совсем других технологиях.

Ну, .NET Framework 4.8 всё ещё жив и поддерживает .NET Standard 2.0, пусть и не имеет некоторых новых фич. Буквально вчера я написал для себя на вин-формах новую программу.


А новый .NET Core 5.0 умеет использовать платформозависимые библиотеки, и в таком виде в него таки вернулись WinForms с WPF.


А ещё где-то есть Xamarin Forms, которые обычно рассматриваются как UI для смартфонов — но и на винде тоже вроде как работают.


Наконец, есть независимая Avalonia.

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

По поводу умных проксей — чаще всего решение — это просовывать Cache-Control даже для ошибок. Понятно, что «кривых» имплементаций полно, но тут кодами это не решить
Об этом просто не подумали. Почти никто не знает, что 404 по умолчанию кэшируются.
Многие фреймворки по дефолту выдают зарестрикченный Cache-Control (no-cache, maxage:0). То есть в такой парадигме — дать закешировать ответ — специальное телодвижение. Мне такой подход ближе
а что собственно клиенту или прокси делать с ошибкой.

А каких действий вы ожидаете от абстрактной прокси, в ответ, например, на ошибку, когда клиент передал значение выходящее за пределы допустимого? Прокся что ли должна исправить это значение и повторно послать запрос серверу? Максимум чего бы я ожидал от адекватных промежуточных "агентов" в плане обработки ответов — это кэширование этих ответов. А для этого вполне достаточно той информации, которая передаётся в заголовках и описана в RFC. Не вижу пока причин придумывать что-то совершенно новое.


когда 404-ки были возвращены ошибочно, но клиент их закэшировал, тем самым продлив факап на неопределённое время.

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


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

Как говорится: "Если бы знал где упаду — соломку подложил". На все случаи жизни не придумать универсального решения. Если вам нужны специфические "графики" по какой-то частной проблеме, то вам придётся позаботься об этом самому. И для этого не обязательно выдумывать новые http-код. Гораздо проще в том месте где случилась ошибка, отправить подробности о ней в логи или в сервис типа Sentry, а не пытаться "угадать" ошибку по HTTP-ответу, предназначенному в первую очередь для клиента, и только во вторую — для мониторинга. Существующих кодов в HTTP вполне достаточно, что бы описать класс любой ошибки, которая может случится в системе построенной на принципах REST. Если же почему-то не получается "смапить" ошибку на имеющиеся коды, то очень вероятно, что вы отклонились от REST (или дополнительных ограничений HTTP, накладываемых поверх него). Я за несколько лет разработки так называемого RESTful API не встретил случая, когда не получилось подобрать стандартный HTTP-статус для классификации конкретной ситуации.


подход #3 весьма дорог в реализации.

Любая качественно сделанная система будет дорогой в реализации (или её будут долго делать). В случае с правильным использованием HTTP и принципов REST сложность заключается, как мне кажется, в том что эти "штуки" не бьют моментально разработчиков по рукам, как, например, компиляторы языков программирования. Без опыта и знаний можно как в С играться с указателями, и всё будет даже работать. Косяки или вообще не заметят из-за незначительности системы. Или это случится сильно позднее, когда автор, вероятнее всего, уже уволится. И тогда такая система либо уйдёт на покой или за неё возьмутся дорогие специалисты и с матами будут исправлять.
Могу сказать, что лично мне "не дорого" использовать подход #3. Но это конечно будет лукавство, т.к. я относительно дорогой специалист. И у меня ушло несколько лет на то, что бы научится "варить" HTTP и REST по "заветам" Филдинга. У нас даже HATEOAS есть. И я на практике вижу плюсы всего этого.


стандартизировать дополнительные машиночитаемые данные в ответе, предпочтительно в форме заголовков HTTP

Сомневаюсь что это практично. Как я уже написал выше — нет серебряной пули. Сложно будет придумать единое решение для более подробного описания конкретных ситуаций. Тех же HTTP-статусов довольно много. Плюс есть ещё различные стандартные HTTP-заголовки, которые дополняют статусы. В совокупности этого уже достаточно для большей части задач обработки запросов и ответов агентами находящимися в промежутке между сервером и клиентом. А уже на клиенте и сервере можно "договорится" и реализовать дополнительную детализацию так как удобно в конкретном случае. Хоть в теле запроса/ответа, хоть в заголовках.


PS: Не скажу что эта статья плоха. В ней есть доля правды и видны попытки разобраться. Хотя есть и чисто технические косяки в плане понимания предназначения конкретных статусов ответа и для чего они подходят, а для чего — нет. К сожалению в интернете не мало статей и заметок с такими и другими ляпами. И это совсем не улучшает понимание HTTP и REST среди разработчиков. Скорее даже наоборот — увеличивает предвзятое мнение, что всё это бесполезные знания, не применимые на практике, и надо просто всегда использовать, например, RPC через GET/POST и 2-3 HTTP-статуса для ответов.
Но фишка в том, что несмотря на всё, у HTTP есть заметный плюс — подробная и продуманная спецификация, которая уже не одно десятилетие применяется на практике. А у многих "протоколов мечты" либо нет спецификации, либо они не так сильно распространены.
Любой web-разработчик знает HTTP хотя бы минимально. И может легко найти спецификации и другие материалы про HTTP и разобраться как всё это можно использовать. Почти все знают что означают коды 200, 404 и 500. И это уже неплохо для начала — можно вести диалог на одном "языке".

> А каких действий вы ожидаете от абстрактной прокси, в ответ, например, на ошибку, когда клиент передал значение выходящее за пределы допустимого?

Ну, например, на 429 и на разные виды 403-х вполне можно ожидать реакции от прокси.

> Не вижу пока причин придумывать что-то совершенно новое.

Так вроде никто и не выдумывает ничего нового.

> Если тут 404 заменить на 200, то ситуация кардинально не поменяется. Поэтому в той ситуации, которую автор «разгребал», в большей степени виноват не «дизайн» кодов, а сервис, который ошибочно возвращает не те коды.

А если б 400, то поменялась бы. Проблема была в том, что разработчики не в курсе, что 400 и 404 по-разному кэшируются.

> Я за несколько лет разработки так называемого RESTful API не встретил случая, когда не получилось подобрать стандартный HTTP-статус для классификации конкретной ситуации.

Это выглядит очень странно. Для всей бизнес-логики в номенклатуре HTTP-кодов по сути есть один 409, которым приходится покрывать кучу разных кейсов. На примере банального интернет-магазина:
* заказ уже был создан — 409
* заказ нельзя создать, потому что не разрешается иметь более N заказов одновременно — 409
* заказ нельзя создать, потому что товар в корзине закончился — 409
* заказ нельзя создать, потому что цена товара изменилась — 409
Скажете, эти кейсы не надо различать?

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

> Гораздо проще в том месте где случилась ошибка, отправить подробности о ней в логи или в сервис типа Sentry, а не пытаться «угадать» ошибку по HTTP-ответу, предназначенному в первую очередь для клиента, и только во вторую — для мониторинга.

Так а как оперативный мониторинг-то строить? На примере тех же 403-х — как сделать, чтобы мониторинг начинал орать, если превышено критическое значение протухающих токенов?
на 429 и на разные виды 403-х вполне можно ожидать реакции от прокси.

С 429 согласен, что можно придумать проксю, которая будет DOS-ить сервер за клиента. Правда клиент про это ничего не узнает и возможно даже отвалится по таймауту и сообщит пользователю, что сервер плохой и тормозной, вместо того что бы попросить его не посылать часто запросы.
Что делать проксе с 403 я не представляю. Сервер вполне конкретно дал понять: "Обрабатывать этот запрос не буду, потому что запрещено". Разве что прокся пошлёт запрос с другого IP адреса, если сервис использует авторизацию на основе диапазона IP адресов. Но это уже очевидно будет не прокся общего назначения, а какая-то специализированная, которая знает про специфические методы авторизации на сервере.


На примере банального интернет-магазина:

  • заказ уже был создан — зависит от ситуации. Например, если ресурс создаётся методом POST (т.е. клиент не знает URL нового ресурса, и просит другой ресурс выполнить его создание) я бы мог вернуть код 200 и в теле ответа уже существующий ресурс (у нас создание нового ресурса обозначается статусом 201 Created). Но это в случаях когда допустима "дедупликация". Тогда клиент понимает, что новый ресурс не был создан с нуля (обычно ему это и не важно) и сразу же получает данные по существующему ресурсу без дополнительных GET запросов. В этих случаях обычно клиенту всё равно до особенностей хранения данных на сервере — ему важно только, что бы на сервере был ресурс с требуемыми характеристиками.
    На моей практике очень в редких случаях приходилось использовать 409. Судя по спецификации это применимо в кейсах, когда два запроса пытаются изменить один и тот же ресурс (т.е. они знают его URL), и при этом второй запрос посылает более старую версию ресурса, который создал/изменил первый запрос. Т.е. эта ошибка предполагает, что в запросе будет информация, по которой сервер сможет определить "версию" ресурса, и запретить перезаписывать имеющуюся у него более новую версию. Статус 409 сообщает клиенту, что он должен синхронизировать своё состояние с сервером, запросив с него новую версию ресурса (и он может это сделать, т.к. знает его URL).


  • заказ нельзя создать, потому что не разрешается иметь более N заказов одновременно — я бы перефразировал "заказ запрещено создавать" и вернул вполне очевидный в этом случае 403 с пояснением причины в теле ответа.


  • заказ нельзя создать, потому что товар в корзине закончился — выше я уже описал область применения кода 409. Тут он не очень подходит, т.к. скорее всего клиент не знает URL "заказа", а потому и не может быть конфликта с его изменением. Тут может подойти 403 — запрещено создавать заказ на основе пустой корзины. Или 422 — клиент передал недопустимый (устаревший или не существующий) ID-версии корзины (это так же решает проблему, когда клиент видит у себя 2 товара в корзине, а на сервере их уже 3 или это 2 других товара).


  • заказ нельзя создать, потому что цена товара изменилась — похоже на предыдущий пункт. Клиент не пытается изменить существующий заказ, что бы был "конфликт". Он просто послал не правильные, устаревшие данные. Очевидно, для реализации такого кейса, клиент должен посылать либо ID-версии товара, или пару [ID-товара, цена], что бы сервер смог сопоставить ожидания клиента с тем что есть у него. Я бы вернул 422 с указанием что или версия товара устарела, или его цена указана не правильно.



практический пример: протухание токенов

Если авторизация выполняется средствами HTTP и запрос "запрещён" именно из-за авторизации, то надо отвечать кодом 401. Код 403 из-за авторизации возвращают только тогда, когда авторизация делается не средствами HTTP. Т.е. не через заголовок Authorization или другие заголовки, URL или параметры в теле запроса. А например авторизация по IP-адресу или SSL сертификату.


как сделать, чтобы мониторинг начинал орать

Логировать важные ошибки непосредственно в самом приложении и настроить мониторинг этих логов, а не только смотреть на access-логи, например nginx-а, который стоит перед приложением. Или использовать access-логи, которые пишет само приложение — ему то ведь известны все детали и оно может добавить их в свои access-логи.
Ну а кейс с протухшими токенами я выше разобрал — если бы возвращали 401, то наверное было бы немного проще фильровать эти ошибки.

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

Во-вторых, предлагаемые вами решения не решают проблему, а усугубляют. Теперь под `403` будут свалены и ошибки авторизации (кулхацкеры, которые пытаются найти дырки в безопасности) и чистая бизнес-логика (пользователю не разрешено иметь больше 3 одновременных заказов). Под `401` теперь живут и те же кулхацкеры, и протухшие токены. Ну и замена 409 на 422 ничего не меняет в постановке задачи.

> Если авторизация выполняется средствами HTTP и запрос «запрещён» именно из-за авторизации, то надо отвечать кодом 401.

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

> Код 403 из-за авторизации возвращают только тогда, когда авторизация делается не средствами HTTP. Т.е. не через заголовок Authorization или другие заголовки, URL или параметры в теле запроса. А например авторизация по IP-адресу или SSL сертификату.

Вы точно читали стандарт?


The 403 (Forbidden) status code indicates that the server understood
the request but refuses to authorize it. A server that wishes to
make public why the request has been forbidden can describe that
reason in the response payload (if any).

If authentication credentials were provided in the request, the
server considers them insufficient to grant access. The client
SHOULD NOT automatically repeat the request with the same
credentials. The client MAY repeat the request with new or different
credentials. However, a request might be forbidden for reasons
unrelated to the credentials.


В отношении 403 RFC не предписывает вообще ничего. Креденшалы могут присутствовать в любом виде.

А вот 401 как раз регламентирован другим RFC tools.ietf.org/html/rfc7235
Формально 401 можно использовать если, и только если авторизация устроена согласно этому RFC. Я, кстати, написал об этом в тексте явно, вот здесь: «Многие разработчики просто не читают спецификации ¯\_(ツ)_/¯. Самый очевидный пример — это ошибка 401 Unauthorized: по спецификации она обязана сопровождаться заголовком WWW-Authenticate — чего, в реальности, конечно никто не делает» и далее.

> Логировать важные ошибки непосредственно в самом приложении и настроить мониторинг этих логов, а не только смотреть на access-логи, например nginx-а, который стоит перед приложением. Или использовать access-логи, которые пишет само приложение — ему то ведь известны все детали и оно может добавить их в свои access-логи.

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

Насчёт 403 и аутентификации "средствами HTTP" — ваша правда. У меня глаза "замылились" описанием этой ошибки из русской википедии.


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

Про 409 я предпочитаю считать, что это применимо только в случае, когда мы явно пытаемся изменить на сервере существующий ресурс, на другую его версию, которая конфликтует с текущей (например передали старую версию).
"The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource."


В ваших кейсах это не так. В них идёт попытка создать второй ресурс другого типа (заказ), который каким-то образом зависит от первого (корзина или товар). Я не считаю что тут есть "конфликт" с созданием самого заказа. Тут есть либо запрет на создание заказа из-за текущего состояния всего приложения — это код 403. Или можно интерпретировать как ошибку в параметрах создания заказа (когда клиент передал не валидное значение цены товара, или номер версии корзины) — это 422.
Ещё можно было бы использовать статус "412 Precondition Failed", если придумать свой кастомный вариант условного заголовка, например:
If-Price-Match: product1=100$, product2=20$
Но это уже экзотика.

Это абсолютно одно и то же.
Есть ресурс «корзина» с каким-то id. В корзине лежит N предметов по определённой цене. У корзины есть ревизия, которую может менять как внутреннее, так и внешнее API.
Цена предмета изменилась → пришёл callback → ревизия корзины увеличилась. Клиент получит 409.
От того, что «ревизия» корзины меняется не по callback-у, а по запросу, смысл ошибки не меняется.
Клиент получит 409.

Из воздуха что-ли получил? Или через HTTP/2 Push-и прилетел? Клиент должен сам попытаться, что-то изменить на сервере, что бы получить ошибку. В ваших кейсах клиент не коризну меняет и не цену для товара. А создаёт новую сущность "заказ". С чего бы тут быть конфликту. Новое состояние заказа не может конфликтовать со старым, потому что на сервере нет вообще заказа с его старым состоянием.


PS: Про то как может измениться корзина или товар на сервере без ведома или участия клиента — это я прекрасно понимаю.

«Заказ» = «переведи корзину из статуса драфт в статус коммит». Эта операция возможна, только если клиент обладает полной информацией о статусе ресурса «корзина».
Ну и вообще, стандарт недвусмысленно заявляет, что


This code is used in situations where the user might be
able to resolve the conflict and resubmit the request. The server
SHOULD generate a payload that includes enough information for a user
to recognize the source of the conflict.

что автоматически делает 409 дефолтным кодом для ошибок в бизнес-логике.

А, ну если у вас "заказ" это просто новое состояние корзины, и вы переводите её в это состояние путём PUT или PATCH запроса на сам ресурс "корзина", то да, 409 тут логичен.
Я имел ввиду случай, когда "заказ" — это отдельная от корзины сущность.

Кстати, если в похожем случае допустимо отдать клиенту ответственность за правильное изменение ресурса, то вместо 409 можно использовать 412 код ошибки + стандартные заголовки для условных запросов (If-Match, If-Unmodified-Since). Тогда клиент на своё усмотрение может выполнить изменение с проверкой, а может и форсировать операцию без дополнительных проверок.

Что изменилось-то от этого? Разнородные ошибки все равно под одним кодом.

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

> Хотя есть и чисто технические косяки в плане понимания предназначения конкретных статусов ответа и для чего они подходят, а для чего — нет.

Я бы оценил конкретные примеры.
Почти все знают что означают коды 200, 404 и 500. И это уже неплохо для начала — можно вести диалог на одном "языке".

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


Что изменится для REST в этом случае?


ЗЫ. К стати мое понимание REST-а не требует обязатаельного использования HTTP кодов. Реальный статус от приложения замечательно передается или в заголовках, или в теле ответа. И не пересекается с HTTP.

К стати мое понимание REST-а не требует обязатаельного использования HTTP кодов

Да, тут всё верно. Но HTTP — это уже готовая система с архитектурой построенной в соответствии с принципами REST. И у вас есть возможность использовать готовое решение, которое описано в спецификациях, и ваш продукт смогут быстро понять те, кто знает HTTP. Или вы можете сами придумать, что-то своё и тогда вам придётся писать документацию и объяснять на каких принципах вы строили свою архитектуру, и как в ней интерпретировать ошибки.

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

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


Филдинг работал над REST с 1994 года параллельно c процессом стандартизации HTTP/1.0, описывая в своей работе архитектурные черты самого веба. Как архитектурный стиль, REST является более абстрактным, чем паттерны проектирования, потому что он описывает архитектуру приложение в целом (поэтому нет и никогда не будет официального RFC или ISO стандарта для REST, равно как и эталонной реализации)

Зато есть RFC и реализация HTTP. И это очень удобно. Особенно удобно посылать в эту спеку разработчиков клиентов :)
Конечно тупое следование спеке HTTP, не сделает автоматом архитектуру вашего приложения соответствующей принципам REST. Но оно хотя бы наталкивает на мысли о том как правильно строить архитектуру распределённого, масштабируемого приложения.

Если бы кто-то читал эти скучные тексты, мир стал бы немного лучше ;-) А так имеем дурацкие теории неофитов о CRUD в HTTP и красивом именование URI.

у HTTP есть заметный плюс — подробная и продуманная спецификация, которая уже не одно десятилетие применяется на практике.

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


Почти все знают что означают коды 200, 404 и 500.

  • 200 — да, все знают — запрос был отправлен, обработан и получен ответ.
  • 404 — нет объекта операции или нет endpoint? А если операция затрагивает больше одного объекта?
  • 500 — проблемы у LB, прокси или самого приложения?

Про остальные коды уже раньше высказывались — с ними те же проблемы.


Транспорт должен отвечать только за одну вещь — собственно, доставку запросов и ответов на них, то есть — 200 если доставлено приложению и оно дало ответ, и любой другой >= 300 если не получилось это сделать.


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


И ещё — любые сообщения об ошибках (структурированные или нет) должны давать достаточно информации чтобы клиент смог понять:


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

Очевидно, что в случае кодов HTTP (и даже дополнительных заголовков), их явно недостаточно даже для этого минимума.

пока HTTP используется по прямому назначению — как транспорт.

Выдержка из википедии:
"HTTP — протокол прикладного уровня передачи данных, изначально — в виде гипертекстовых документов в формате HTML, в настоящее время используется для передачи произвольных данных."


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


404 — нет объекта операции или нет endpoint? А если операция затрагивает больше одного объекта?

Если бы вы немного внимательнее читали про REST, то знали бы что в нём на одном URL может находится одна сущность. И быстро бы поняли, что у вас что-то не так, если вы пытаетесь одним URL-ом обозначить несколько несколько сущностей. Как вы заметили — даже 404 не понятно как возвращать в данном случае. Я про это уже написал — если у вас ошибки не мапятся http-статусы, значит вы отклонились от ограничений REST и HTTP. В REST поверх HTTP есть один простой шаблон для операций над множеством сущностей. Это делают посредством дополнительной сущности, назовём её "выполнятор", с помощью которой создают сущность "задача". В рамках каждой задачи сервер может выполнять любые действия, которые не "влезают" в простые CRUD-операции с одной сущностью. В такой реализации отпадает проблема "как вернуть 404 если операция затрагивает больше одного объекта". В этом случае эта информация возвращается либо как ошибка 422 на запрос к "выполнятору", и в теле перечисляют список ресурсов, которые клиент передал неправильно. Или в теле сущности "задача", если отсутствие ресурсов было определено в момент её выполнения, а не в момент валидации параметров её создания.


Очевидно, что в случае кодов HTTP (и даже дополнительных заголовков), их явно недостаточно даже для этого минимума.

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

> Так что всё-таки прямое назначение HTTP — прикладное, а не транспортное.

Не могу не отметить, что сама модель OSI является сферическим конём в вакууме, описывающим какой-то выдуманный сетевой стек (достаточно сказать, что TCP/IP в него не ложится, т.к. формально и TCP, и IP являются протоколами транспортного уровня). В данном случае прикладной уровень надо делить на два — прикладной уровень протокола (HTTP) и прикладной уровень бизнес-логики.
Так что всё-таки прямое назначение HTTP — прикладное, а не транспортное.

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


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

Здорово. А теперь представим что у нас ошибка в URL и вместо "GET /users/user-name" ушёл запрос "GET /uzars/user-name" — как клиент узнает что у него ошибка в URL? По спецификации нужно вернуть 404 в обоих случаях.


И нет, никто не прибил гвоздями это требование — у кого-то она одна, у кого-то их много, потому что REST — это не стандарт, не говоря уже просто многочисленные REST-like. Почему так? Да именно потому что в "чистом" виде REST применим ну в очень узком круге приложений, для чего-то реально сложного он уже недостаточен и скатывается только к транспортной роли.


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

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


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

как клиент узнает что у него ошибка в URL

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


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

Не хочу сейчас искать цитаты из дисера Филдинга, может я реально что-то не так понял. Но поверьте моему опыту — HTTP был сделан именно с такими идеями (а делался он по таким же принципам о которых писал Филдинг). Это очень хорошо видно, когда пытаешься работать с ресурсами через HTTP так как это в нём задумано — всё сразу становится на место, и нет ни каких не разрешимых затыков, и классификация ошибок оказывается вполне достаточной.
А если вы идёте "в чужой монастырь со своим уставом", то у вас и выходит что всё не так, и всё криво, и мало детализации и не понятно как использовать. В этом случае действительно надо использовать HTTP только как транспортный уровень и обходится только GET/POST-запросами (если действовать последовательно то надо только POST, но вы наверняка захотите кеширование сторонними решениями), и двумя кодами ответа: 200 — запрос обработан, 500 — запрос не обработан.


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

Вы противоречите сами себе. С одной стороны вы хотите "больше деталей", но при этом согласны с тем что никто не будет с этим заморачиваться. "Ленивые" разработчики даже не хотят использовать сделанную и описанную систему классификации ошибок в HTTP. И начинают выдумывать свою классификацию. Но винят в этом почему-то HTTP с его "недостаточно богатой системой ошибок".
Большинство претензий к статусам HTTP-ответов порождается узкостью мышления разработчиков. У разработчика ошибка в ТЗ называется "Нельзя создать пользователя с возрастом < 0". Разработчик смотрит в таблицу кодов HTTP и возмущается: "Как так, где ошибка 'XXX User is not born yet'? Говно ваш HTTP!".

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


Эти коды нужны только как часть протокола HTTP. И в большинстве случаев народ использует их по минимому: 200 — все хорошо, 300 редирект, 400 — неправильный запрос (не может быть распаршен), 403 — требуется авторизация, 404 — нет ресурса, 500 — не определенные проблемы (могут пофиксится перезапросом, могут и не пофикситься, в общем попробуй позже).


А сообщения от бизнес логики это уровень выше. Причем тут тоже нет универсального решения. Я стараюсь подходить к этому следующим образом: в лог детальную инфу об ошибке, а сообщение привожу к десятку "юзер френдли": Отказано в доступе, фатальная ошибка, таймаут, объект (файл, папка, ...) не найден итп те пытаюсь выдавать сообщение которое может быть понятно тому кто запрашивает, и явно связано со входными данными, которые можно поменять, и пере-запрос с ними пройдет.


Попытка выдать детальную информацию не приведет к ее исправлению в 99% случаев. Просто код обработки ошибок будет толще, и будет проверять не пару кодов. Если пришли вот эти сто кодов то это фатал, и пользователю выдать слащавое сообщение.

Во-первых, грань, где здесь «бизнес»-логика, а где не бизнес — весьма тонка. Тот же 403 как пример.
Во-вторых, конечно, выдавать клиенту наружу детали 500-ки почти никогда не нужно. А вот 400-ки редко лишними бывают.
В-третьих, один из пойнтов следующий: мониторинг все равно нужен, и мониторинг как-то группировать ошибки все равно должен, одних подробных логов недостаточно.
Во-первых, грань, где здесь «бизнес»-логика, а где не бизнес — весьма тонка. Тот же 403 как пример.

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


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

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

Так ничем, я как раз и говорю, что в идеальном мире что залогировано (какая причина ошибки), то и передано должно быть в ответе (заголовком ли, боди, неважно).

Не понял, в чём проблема HTTP статус-кодов.


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

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


статус-коды HTTP используются вовсе не в целях поддержания чистоты протокола

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


нельзя задизайнить по ошибке на каждый потенциально неправильный параметр вместо единой 400-ки,

… противоречит парадигме REST

В чём противоречие? Откуда возникла мысль, что REST как-то связан с кодом ошибки? Вполне достаточно 1 кода на все ошибки, с точки зрения парадигмы REST.


графики и мониторинги

И их можно настроить, тот же ELK стек не только на коды ошибок смотрит, но и на содержание месседжа. Да, это требует доп. усилий, но всё в этом мире требует усилий. Я не представляю, если бы на каждую ошибку в НТТР-заголовке был бы свой код ошибки — был бы вообще протокол НТТР юзабелен сколь-нибудь.


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

А как RFC должно с этим помогать?

Например вот так:


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

или вот так:


Самый очевидный пример — это ошибка 401 Unauthorized: по спецификации она обязана сопровождаться заголовком WWW-Authenticate — чего, в реальности, конечно никто не делает, и по очевидным причинам, т.к. единственное разумное значение этого заголовка — Basic (да-да, это та самая логин-парольная авторизация времён Web 1.0, когда браузер диалоговое окно показывает). Более того, спецификация в этом месте расширяема, никто не мешает стандартизовать новые виды realm-ов авторизации — но всем традиционно всё равно.

На прочие ваши вопросы также есть ответы в тексте.


Отдельно хотелось бы отметить, что:


Я не представляю, если бы на каждую ошибку в НТТР-заголовке был бы свой код ошибки — был бы вообще протокол НТТР юзабелен сколь-нибудь.

Тем не менее, в HTTP полно ошибок на невалидное значение одного конкретного параметра запроса — 405, 411, 414, 416, например. HTTP юзабелен?

А как RFC должно с этим помогать?
Например вот так:
… какое конкретно поле имеет недопустимое значение

Так что, на каждый заголовок и каждое поле свой код ошибки?


ошибка 401 Unauthorized: по спецификации она обязана сопровождаться заголовком WWW-Authenticate — чего, в реальности, конечно никто не делает

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


На прочие ваши вопросы также есть ответы в тексте.

Вы про "А какие ваши предложения?" Ну там особо ответов то и нет, а) не использовать вообще б) оставить всё как есть в) "прибрать бардак" — полностью переделать протокол???


Тем не менее, в HTTP полно ошибок на невалидное значение одного конкретного параметра запроса

Некоторые коды присутствуют совершенно обоснованно, если показывают не "параметр запроса", а валидность самого запроса. Можно конечно спорить, но длина payload / URI не является параметром запроса, как и метод. Например, заголовок Expect является критичным в конкретном случае и тоже обоснованно имеет соответствующий код ошибки 417. Хотя в данном случае, возможно, не самое оптимальное решение вообще инициировать protocol switch используя заголовок.


Но по крайней мере это всё однозначно определяет действия клиента — как раз то, ради чего вы эту статью писали. Чётко понятно, что не так в данном запросе. При длине payload, регулярно вываливающейся за пределы, может быть отрегулируем client_max_body_size или что-то ещё. При длине URI — может у нас в приложении урлы некорректно генерируются (впрочем, обоснованность ограничения длины URI тоже сомнения вызывает, если на то пошло). Не тот метод? Тоже повод проверить приложение.


Теперь представим, что на каждый header свой код ошибки — и что вы с этим будете делать? Как это вам поможет, если скажем, тот же Accept не имеет чёткого множества допустимых параметров? Вы будете это вообще использовать в вашем приложении? Вам нужен график, сколько запросов с неправильным Accept header?


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


[INFO] Continue
[OK] OK / Created / Accepted ...
[REDIRECT] ...
...

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


С другой стороны, мы, можно сказать, уже потеряли возможность писать НТТР-запросы вручную, в связи с массовым приходом SSL, и сейчас все эти текстовые поля скорее существуют в нашем воображении, и не исключено, что в НТТР/3 будет принципиально другой подход, например, непосредственная двоичная кодировка. Реальность уже далеко ушла от названия Hypertext Transfer, а что будет дальше, предсказать вообще сложно.


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

и не исключено, что в НТТР/3 будет принципиально другой подход

Он уже в HTTP/2 другой — бинарный. Там убрали reason phrase после кода ответа (вот те самые строчки типа Ok, Created, Not Found), т.к. по изначальной спеке HTTP клиенты всё равно должны его игнорить. Все стандартные заголовки заменены на числовые коды. А не стандартные по моему тоже как то сжимаются, но это не точно.

Да, точно. Спасибо за коммент. Я мыслил в правильном направлении =)


Цитируя себя:


не исключено, что в НТТР/3 будет принципиально другой подход, например, непосредственная двоичная кодировка

s/3/2/
s/будет//


:)

единственное разумное значение этого заголовка — Basic

Правильнее сказать "единственное" описанное в RFC для HTTP. И в общем то не единственное. Есть улучшенный вариант — Digest. В нём логин и пароль не передаются в открытом виде. Передаётся хэш от них. Аналогичная аутентификация использует в SIP.


Кроме того есть Bearer описанный в RFC на oAuth 2.0, и который также используется для аутентификации на базе JWT.


логин-парольная авторизация времён Web 1.0

Потребность в такой аутентификации никуда не делись со времён Web 1.0. Правильнее назвать её аутентификацией по "ключу" и "секрету". Не обязательно "ключом" может быть логин пользователя, а "секретом" — его пароль. Это пара может генерироваться на сервере и выдаваться клиенту на время. Альтернативой такого способа является пожалуй только что-то вроде JWT, когда вся нужная информация для авторизации находится внутри токена, а на сервере есть только секрет для проверки сигнатуры и больше ничего. И тот и другой способ обладает своими плюсами и минусами.
У Basic аутентификации только один заметный минус — бесполезность при использовании не шифрованного транспорта. Но если у вас https — это вполне себе рабочий вариант.

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

А что у вас отдаёт прокси на запрос, который есть в кэше? )

Не использую HTTP-прокси для кэширования нестатического контента.

Иногда полезно бывает...

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

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