Ситуацию с использованием кодов ответов HTTP можно заносить в палату мер и весов: вот что происходит, когда благие намерения разработчиков спецификации сталкиваются с жестокой реальностью. Даже с двумя жестокими реальностями.
Как мы обсудили в Главе 10, одна из целей существования семантических ошибок — помочь клиенту понять, что стало причиной ошибки. При разработке спецификации HTTP (в частности, RFC 7231) эта цель очевидно была одной из главных. Более того, архитектурные ограничения REST, как их описал Фьелдинг в своей диссертации, предполагают, что не только клиенты должны понимать семантику ошибки, но и все сетевые агенты (прокси) между клиентом и сервером в «многослойной» архитектуре. И, в соответствии с этим, номенклатура статус-кодов HTTP действительно весьма подробно описывает почти любые проблемы, которые могут случиться с HTTP-запросом: недопустимые значения Accept-*
-заголовков, отсутствующий Content-Length
, неподдерживаемый HTTP-метод, слишком длинный URI и так далее.
Но вот с чем RFC совершенно не помогает — это с вопросом, а что собственно клиенту или прокси делать с ошибкой. Как мы обсуждали, ошибки могут быть устранимыми или неустранимыми. Если ошибки неустранимая, то клиентам по большому счёту наплевать на всю эту петрушку со статус-кодами и заголовками, а уж промежуточным прокси тем более. Для этого на самом деле трёх кодов было бы достаточно:
400
для персистентных ошибок (если просто повторить запрос — ошибка никуда не денется);404
для статуса неопределённости (повтор запроса может дать другой результат);500
для проблем на стороне сервера плюс заголовокRetry-After
, чтобы дать понять клиенту, когда прийти снова.
Замечание: кстати, обратите внимание на проблему дизайна спецификации. По умолчанию все 4xx
коды не кэшируются, за исключением: 404
, 405
, 410
, 414
. Мы не сомневаемся, что это было сделано из благих намерений, но подозреваем, что количество людей, знающих об этой тонкости, примерно равно количеству редакторов спецификации. В результате мы имеем множество ситуаций (автор лично разгребал последствия одной из них), когда 404
-ки были возвращены ошибочно, но клиент их закэшировал, тем самым продлив факап на неопределённое время.
Что касается устранимых проблем — то да, статус-коды в чем-то помогают. Некоторые из них вполне конкретны, например 411 Length Required
. А некоторые — нет. Можно привести множество ситуаций, где под одним кодом прячутся разнородные ошибки:
400 Bad Request
для ситуаций, когда часть параметров отсутствует или имеет недопустимое значение. От этой ошибки клиентам нет абсолютно никакого толку, если только в ответе не указано, какое конкретно поле имеет недопустимое значение — и вот как раз именно это стандарт и не стандартизирует! Да, конечно, можно самому стандарт придумать — но это как минимум противоречит идее прозрачности в REST.
NB: некоторые пуристы считают, что
400
означает проблемы с самим запросом, т.е. кривой URI, заголовок, невалидный JSON и т.д., а для логических ошибок с параметрами предлагают использовать422 Unprocessable Entity
или412 Precondition Failed
. Как вы понимаете, это влияет примерно ни на что.
403 Forbidden
для ошибок аутентификации и/или авторизации. И вот тут есть множество совершенно разныхForbidden
-ов, которые требует совершенно разных действий от клиента:
- токен авторизации отсутствует — надо предложить клиенту залогиниться;
- токен протух — надо выполнить процедуру подновления токена;
- токен принадлежит другому пользователю — обычно свидетельствует о протухании кэша;
- токен отозван — пользователь выполнил выход со всех устройств;
- злоумышленник брутфорсит авторизационный эндпойнт — надо выполнить какие-то антифродные действия.
Каждая
403
связана со своим сценарием разрешения, некоторые из них (например, брутфорсинг) вообще ничего общего не имеют с другими.
409 Conflict
;
тысячи их.
Таким образом, мы вполне естественным образом приходим к идее отдавать детальное описание ошибки в заголовках и/или теле ответа, не пытаясь изобрести новый код для каждой ситуации — абсолютно очевидно, что нельзя задизайнить по ошибке на каждый потенциально неправильный параметр вместо единой 400
-ки, например.
Замечание: авторы спецификации тоже это понимали, и добавили следующую фразу: ‘The response message will usually contain a representation that explains the status’. Мы с ними, конечно, полностью согласны, но не можем не отметить, что эта фраза не только делает кусок спецификации бесполезным (а зачем нужны коды-то тогда?), но и противоречит парадигме REST: другие агенты в многоуровневой системе не могут понять, что же там «объясняет» представление ошибки, и сама ошибка становится для них непрозрачной.
Казалось бы, мы пришли к логичному выводу: используйте статус-коды для индикации «класса» ошибки в терминах протокола HTTP, а детали положите в ответ. Но вот тут теория повторно на всех парах напарывается на практику. С самого появления Web все фреймворки и серверное ПО полагаются на статус-коды для логирования и построения мониторингов. Я не думаю, что сильно совру, если скажу, что буквально не существует платформы, которая из коробки умеет строить графики по семантическим данным в ответе ошибки, а не по статус-кодам. И отсюда автоматически следует дальнейшее усугубление проблемы: чтобы отсечь в своих мониторингах незначимые ошибки и эскалировать значимые, разработчики начали попросту придумывать новые статус-коды — или использовать существующие не по назначению.
Это в свою очередь привело не только к распуханию номенклатуры кодов, но и размытию их значений. Многие разработчики просто не читают спецификации ?\_(?)_/?. Самый очевидный пример — это ошибка 401 Unauthorized
: по спецификации она обязана сопровождаться заголовком WWW-Authenticate
— чего, в реальности, конечно никто не делает, и по очевидным причинам, т.к. единственное разумное значение этого заголовка — Basic
(да-да, это та самая логин-парольная авторизация времён Web 1.0, когда браузер диалоговое окно показывает). Более того, спецификация в этом месте расширяема, никто не мешает стандартизовать новые виды realm
-ов авторизации — но всем традиционно всё равно. Прямо сейчас использование 401
при отсутствии авторизационных заголовков фактически является стандартом индустрии — и никакого WWW-Authenticate
при этом, конечно, не шлётся.
В современном мире мы буквально живём в этом бардаке: статус-коды HTTP используются вовсе не в целях поддержания чистоты протокола, а для графиков; их истинное значение забыто; клиенты обычно и не пытаются хоть какие-то выводы из кода ответа сделать, редуцируя его до первой цифры. (Честно говоря, ещё неизвестно, что хуже — игнорировать код или, напротив, писать логику поверх кодов, использованных не по назначению.) Ну и, конечно, нельзя не упомянуть о широко распространённой практике отдавать ошибки внутри 200
-ок.
А какие ваши предложения?
На самом деле есть три подхода к решению этой ситуации:
отказаться от REST и перейти на чистый RPC. Использовать статус-коды HTTP только для индикации проблем с соответствующим уровнем сетевого стэка. Достаточно двух:
200 OK
если сервер получил запрос, независимо от результата — ошибки исполнения запроса все равно возвращаются как200
.500 Internal Server Error
если запрос до сервера не дошёл.
Можно ещё использовать
400 Bad Request
для клиентских ошибок. Это чуть усложняет конструкцию, но позволяет пользоваться ПО и сервисами для организации API Gateway;
«и так сойдёт» — ну раз сложилась такая ситуация, ну в ней и жить, только осторожненько, совсем уж явно не нарушая стандарт. Графики строить по кодам; нужно поделить ошибки по типу — используй какой-нибудь экзотический код. Клиенты код ответа игнорируют и смотрят на данные в теле ответа.
NB: некоторые признанные лидеры индустрии умудряются при этом делать и то, и другое: использовать RPC-подход и, одновременно, кучу статус-кодов для каких-то частных проблем (например,403
и429
, которые вообще-то явно связаны с бизнес-логикой работы клиентов, а не с самим HTTP). В чисто практическом смысле такой подход работает, хотя и трудно предсказать наперёд, какие проблемы могут притаиться в современной инфраструктуре, где любая «умная» прокси норовит прочитать запрос. Ну и эстетические чувства соответствующие;
прибрать бардак. Включая, но не ограничиваясь:
- использовать HTTP-коды для проблем, которые можно описать в терминах HTTP (т.е. использовать
406 Unacceptable
при недопустимом значении заголовкаAccept-Language
, например, а не для чего-то ещё); - стандартизировать дополнительные машиночитаемые данные в ответе, предпочтительно в форме заголовков HTTP (потому что чтение заголовков не требует вычитывания и разбора всего тела ответа, так что промежуточные прокси и гейтвеи смогут понять семантику ошибки без дополнительных расходов; а так же их можно логировать) — например, использовать что-то наподобие
X-My-API-Error-Reason
и жестко регламентировать возможные значения; - настроить графики и мониторинги так, чтобы они работали по доп. данным из предыдущего пункта в дополнение к статус-кодам (или вместо них);
- убедиться, что клиенты верно трактуют и статус-коды, и дополнительные данные, особенно в случае неизвестных ошибок.
- использовать HTTP-коды для проблем, которые можно описать в терминах HTTP (т.е. использовать
Выбор за вами, но на всякий случай заметим, что подход #3 весьма дорог в реализации.
— Этот текст написан в рамках подготовки будущего раздела про HTTP API для моей книги, работы ведутся на Гитхабе.
Англоязычная версия этого же текста здесь.
Я буду признателен, если кто-то пошарит её на реддите, я сам по правилам реддита не могу.
welovelain
Использование хттп кодов для ошибок в ресте всегда напоминало натягивание совы на глобус. Мне лично импонирует RPC-подход, но прочитанные свежими сеньорами за вечер трех статей по ресту редко дают пространство для обсуждений, бест практис же.
forgotten Автор
RPC тяжело кэшировать и масштабировать. Чтобы раскидать по шардам — надо прочитать ответ и вычленить из него данные. Чтобы промежуточная прокси узнала, можно ли положить ответ в кэш — аналогично. А уж идемпотентна ли операция из сигнатуры запроса вообще никак не узнать.
Идея REST заключается строго в следующем: есть метаинформация запроса (http-коды, методы, URL, заголовки), давайте построим систему, в которой все агенты умеют их понимать и трактовать. Т.е. например если метод GET — значит, запрос немодифицирующий, можно его префетчить, как-то так.
arthuriantech
Никто не мешает реализовать RPC-интерфейсы поверх HTTP, получая его семантику там, где она нужна. Более того, большинство из так называемых "RESTful API" именно так и делают — они представляют собой ориентированный на данные CRUD RPC, а термин REST используется сугубо как популярный баззворд.
Центральная идея 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
forgotten Автор
> Никто не мешает реализовать 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 попросту не существует.
arthuriantech
Как именно "большое количество ПО" должно трактовать какой-нибудь отдельно взятый 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?
forgotten Автор
> Как именно «большое количество ПО» должно трактовать какой-нибудь отдельно взятый HTTP API? Что конкретно имеется ввиду?
Имеется в виду следующее: когда вы пишете proxy_pass в конфигурации nginx — nginx начинает как-то трактовать стандарт. Он не пересылает запрос как есть. То же касается, скажем, используемого на клиенте фреймворка работы с сетью, и особенно API Gateway-ев, без которых микросервисную архитектуру не построишь.
> Не существует такого разделения. В чем именно состоит разница между REST 2000 и REST 2008?
В 2000 Фьелдинг написал абстрактно. Под «hypermedia as the engine of application» можно много чего понимать. Собственно так и вышло — каждый немедленно истрактовал REST в свою степь.
arthuriantech
У нас нет проблем с RPC поверх HTTP, пока HTTP не нарушается значимым образом. HTTP остается в своем слое, пока приложение работает в RPC-стиле, используя GET для чтения и POST для всего остального (между прочим, POST предназначен для любых операций, которые не покрыты в рамках остальных методов)
Много ли людей вообще знают о существовании этой диссертации, брендируя свой API вирусным термином REST? Мало кто опирается на первоисточники, обычно все заканчивается чтением коротких статей и заметок на персональных блогах которые вместо пруфлинков содержат только ИМХО их авторов.
Между прочим, вы знаете, что означает название "Representational State Transfer"?