Ситуацию с использованием кодов ответов 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 и жестко регламентировать возможные значения;
    • настроить графики и мониторинги так, чтобы они работали по доп. данным из предыдущего пункта в дополнение к статус-кодам (или вместо них);
    • убедиться, что клиенты верно трактуют и статус-коды, и дополнительные данные, особенно в случае неизвестных ошибок.


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


— Этот текст написан в рамках подготовки будущего раздела про HTTP API для моей книги, работы ведутся на Гитхабе.


Англоязычная версия этого же текста здесь.


Я буду признателен, если кто-то пошарит её на реддите, я сам по правилам реддита не могу.