Давно я хотел написать эту статью. Все думал — с какой стороны зайти правильнее? Но, вдруг, недавно, на Хабре появилась подобная статья, которая вызвала бурю в стакане. Больше всего меня удивил тот факт, что статью начали вбивать в минуса, хотя она даже не декларировала что-то, а скорее поднимала вопрос об использовании кодов ответа web-сервера в REST. Дебаты разгорелись жаркие. А апофеозом стало то, что статья ушла в черновики… килобайты комментариев, мнений и т.д. просто исчезли. Многие стали кармо-жертвами, считай, ни за что :)
В общем, именно судьба той статьи побудила меня написать эту. И я очень надеюсь, что она будет полезна и прояснит многое.
Предупреждаю, все ниже написанное является реальным опытом, а не когнитивной эквилибристикой. И так, погнали.
HTTP
Первым делом нужно очень четко разделить слои. Слой транспорта — http. Ну и собственно REST. Это фундаментально важная вещь в принятии всего и “себя” в нем. Давайте сначала поговорим только о http.
Я использовал термин “слой транспорта”. И я не оговорился. Все дело в том, что сам http реализует функции транспортировки запросов к серверу и контента к клиенту независимо от tcp/ip. Да, он базируется на tcp/ip. И вроде, нужно считать именно его транспортным. Но, нет. И вот почему — сокет-соединения не являются прямыми, т.е. это не соединение клиент-сервер. Как http запрос, так и http ответ могут пройти длинный путь через уйму сервисов. Могут быть агрегированы или напротив декомпозированы. Могут кэшироваться, могут модифицироваться.
Т.е. у http запроса как и http ответа есть свой маршрут. И он не зависит ни от конечного бэка, ни от конечного фронта. Прошу на это обратить особое внимание.
Маршруты http не являются статическими. Они могут быть очень сложными. Например, если в инфраструктуру встроен балансировщик, полученные запросы он может отправить на любую из нод бэка. При этом, сам бэк может реализовывать собственную стратегию работы с запросами. Часть из них пойдет на микросервисы напрямую, часть будет обработана самим web-сервером, часть дополнена и передана кому-то еще, а часть выдана из кэша и т.п. Так работает Интернет. Ничего нового.
И тут важно понять — зачем нам коды ответов? Все дело в том, что вся вышеописанная модель принимает решения на их базе. Т.е. это коды, позволяющие принимать инфраструктурные и транспортные решения в ходе маршрутизации http.
К примеру, если балансировщик встретится с кодом ответа от бэка 503, при передаче запроса, он может принять это за основание считать, что нода временно недоступна. Отмечу, что в ответе с кодом 503 предусмотрен заголовок Retry-After. Получив из заголовка интервал для повторного опроса, балансировщик оставит ноду в покое на указанный срок и будет работать с доступными. Причем, подобные стратегии реализуются “из коробки” web-серверами.
Небольшой офтопик для глубины понимания — а если нода ответила 500? Что должен сделать балансировщик? Переключать на другую? И многие ответят — конечно, все 5xx основание для отключение ноды. И будут неправы. Код 500 это код неожиданной ошибки. Т.е. той, которая может больше никогда и не повториться. И главное, что переключение на другую ноду может ничего и не изменить. Т.е. мы просто отключаем ноды без малейшей пользы.
В случае с 500 нам на помощь приходит статистика. Локальный WEB-сервер ноды, может переводить саму ноду в статус недоступности при большом количестве ответов 500. В этом случае, балансировщик обратившись на эту ноду, получит ответ 503 и не будет ее трогать. Результат тотже, но теперь, такое решение осмысленно и исключает “ложные” срабатывания.
Но и это еще не все. В такой ситуации мониторинг позволит админам подключиться к ситуации для обслуживания ноды. Т.е. мы получаем не просто реализацию высокодоступного сервиса, с балансировками и т.п., но еще и эффективный процесс поддержки.
И все это позволяют делать коды ответа сервера. Любая архитектура WEB-приложения должна начинаться с проектирования транспортного слоя. Надеюсь, сомнений в этом не осталось.
REST
Задам риторический вопрос — что это такое? И что вы ответили себе на него? Не буду давать ссылки на очевидные пруфы, но скорее всего не совсем то, чем он является по сути :) Это лишь идеология, стиль. Некие соображения на тему — как лучше общаться с бэком. И не просто общаться, а общаться в WEB инфраструктуре. Т.е. на базе http. Со всеми теми “полезными штуками”, о которых я написал выше. Конечные решения по реализации вашего интерфейса остаются всегда за вами.
Вы задумывались почему не придуман отдельный транспорт для REST? Например, для websocket он есть. Да, он тоже начинается с http, но потом, после установки соединения, это вообще отдельная песня. Почему бы не сделать такую же для REST?
Ответ прост — а зачем? Есть прекрасный, уже готовый, выверенный протокол — http. Он хорошо масштабируется. Позволяет реализовывать сложные, высокодоступные сервисы, способные справляться с большой нагрузкой. Все, что нужно — ввести некие концептуальные правила, чтобы разработчики друг друга понимали.
Отсюда следует простой, очевидный вывод — все, что присуще http, присуще и REST. Это неотделимые сущности. Нет отдельного заголовка REST, нет даже намека на то, что REST это REST. Для любого сервера REST запрос ровно такой же, как и любой другой. Т.е. REST это только то, что у нас “в голове”.
Коды ответа http в REST
Давайте поговорим о том, каким же кодом должен отвечать ваш сервер на REST запрос? Лично мне кажется, что из всего выше написанного уже очевиден ответ, что т.к. REST не отличается от любого другого запроса, он должен быть подчинен ровно тем же правилам. Код ответа — неотъемлемая часть REST и он должен быть релевантен сути ответа. Т.е. если не найден объект по запросу, это 404, если клиент обратился с некорректным запросом 400 и т.д. Но, чаще всего, дебаты на сём не заканчиваются. Поэтому, продолжу и я.
Можно ли отвечать на всё кодом 200? А кто вам запретит? Пожалуйста… код 200 такой же код как и другие. Правда, в основе такого подхода лежит очень простой тезис — моя система идеальная, у нее не бывает ошибок. Если вы человек, который может создавать такие системы — этому можно только позавидовать!
Но скорее всего… она не идеальна. И ошибки все же случаются. А бывает, что они случаются по независящим от нас обстоятельствам. И тут типовым решением является создание собственной системы кодирования ошибок. Это плохо? Да, это плохо. Это супер-плохо. Давайте разбираться почему.
И так, принимая код 200 как единственно верный, мы берем на себя обязанности на разработку целого слоя (критического слоя) системы — обработку ошибок. Т.е. труд многих людей по разработке этого слоя отправляется в утиль. И начинается постройка своего “велосипеда”. Но эта мегастройка обречена на провал.
Начнем с кода. Если мы собираемся на все отвечать 200, нам самим придется обрабатывать ошибки. Классическим методом является try конструкции. Каждый сегмент кода мы оборачиваем дополнительным кодом. Обработчиками, которые что-то делают полезное. Например, что-то кладут в лог. Что-то важное. Что позволит локализовать ошибку. А если ошибка возникла не там где ее ожидали? Или если ошибка возникла в обработчике ошибки? Т.е. эта стратегия на уровне кода нерабочая априори. И в конце концов, обрабатывать ваши баги будет интерпретатор или платформа. ОС, наконец. Суть бага в том, что вы его не ждете. Не нужно его прятать, его нужно находить и фиксить. Поэтому, если на какие-то запросы REST ответит ошибкой 500 это нормально. И более того — правильно.
Давайте еще раз вернемся к вопросу — почему это правильно? Потому что:
- Код 500 это инфраструктурный маркер, на основании которого нода на которой возникает проблема может быть отключена;
- Коды 5xx это то, что мониторится и если такой код возникает, любая система мониторинга тут же вас известит об этом. И служба поддержки вовремя сможет подключиться к решению проблемы;
- Вы не пишите дополнительный код. Не тратите на это драгоценное время. Не усложняете архитектуру. Вы не занимаетесь несвойственными вам проблемами — вы пишите прикладной код. То, что от вас хотят. За что платят.
- Трейс который выпадет по ошибке 500 будет куда как полезнее, чем ваши попытки его превзойти.
- Если REST запрос вернет 500 код, фронт уже на моменте обработки ответа будет знать, по какому алгоритму его обрабатывать. Причем, суть дела никак не изменится, вы как ничего толкового не получили и с 200, так и с 500. Но с 500 вы получили профит — осознание того, что это НЕОЖИДАННАЯ ошибка.
- Код 500 придет гарантированно. Независимо от того насколько плохо или хорошо вы написали свой код. Это ваша точка опоры.
Отдельно забью гвоздь во все “тело” кода 200:
7. Даже если вы очень сильно постараетесь избежать иных кодов ответа от сервера кроме как 200 на ваши запросы, вы не сможете это сделать. Вам может ответить на ваш запрос любой сервер посредник, совершенно любым кодом. И вы ДОЛЖНЫ будете такой ответ обработать корректно.
Итого, на логическом уровне борьба за код 200 бессмысленна.
Теперь давайте вернемся к инфраструктурному уровню. Очень часто слышу мнение — код 5xx не прикладного уровня, его нельзя отдавать бэком. Кхм, ну… тут есть противоречие в самом утверждении. Отдавать можно. Но код этот не прикладного уровня. Вот так вернее. Для понимания этого, предлагаю рассмотреть кейс:
Вы реализуете шлюз. У вас несколько ДЦ, на каждом свой канал связи к некоему приватному сервису. Ну, к примеру, к платежке по VPN. И есть канал коммуникации с Интернет. Вы получаете запрос на операцию со шлюзом, но… сервис оказывается недоступен.И так, что вы должны ответить? Кому? Это проблема именно инфраструктурная и, именно, бэк столкнулся с ней. Конечно, нужно смело отвечать 503. Эти действия приведут к тому, что нода будет отключена балансировщиком на какое-то время. При этом, балансировщик при правильной настройке, не разрывая соединение с клиентом, отправит запрос в другую ноду. И… конечный клиент, с великой долей вероятности получил 200. А не кастомное описание ошибки, которая ему ничем не поможет.
Где и какой код использовать
Вопрос непростой. На него нет однозначного ответа. Для каждой системы проектируется транспортный слой и коды в нем могут быть специфичные.
Есть принятые стандарты. Их можно легко найти и, опять же, не буду очевидные пруфы приводить. Но, приведу неочевидный — developer.mozilla.org/ru/docs/Web/HTTP/Status
Почему его? Все дело в том, что обработчики кода могут вести себя по разному, в зависимости от реализации и контекста “понимания кода”. К примеру, в браузерах есть стратегия кеширования, завязанная на коды ответа. А в некоторых сервисах есть свои, кастомные коды. Например, CloudFlare.
Т.е. принятие решений об использовании кодов, нужно базировать на всех элементах входящих в транспортный слой от вашего кода на бэке до кода на клиенте. Только так можно найти верные ответы. Я даже пытаться тут дать всем универсальную пилюлю не буду.
Корни зла
Уже третий проект, в который я прихожу страдает кодом 200 в REST. Именно страдает. Другого слова нет. Если вы внимательно прочли всё до текущего момента, вы уже понимаете, что как только проект начинает расти, у него появляется потребность в развитии инфраструктуры, в ее устойчивости. Код 200 убивает все эти потуги на корню. И первое, что приходится делать — ломать стереотипы.
Корень зла, мне кажется лежит в том, что код 500, это первое, что web-разработчик встречает в своей профессиональной деятельности. Это, можно сказать, детская травма. И все его старания поначалу сводятся к тому, чтобы получить код 200.
Кстати, по какой-то причине, на этом же этапе развивается устойчивое мнение, что только ответы с кодом 200 могут быть снабжены телом. Конечно, это не так и с любым кодом может “приехать” любой ответ. Код это код. Тело это тело.
Далее, с развитием разработчика, у него возникают потребности в управлении багами собственного приложения. Но…, он не умеет пользоваться логами. Не умеет настраивать web-сервер. Он учится. И рождаются те самые «велики». Потому что, они ему доступны и он может их быстро сделать. Далее, на этот «велик» он монтирует новые колеса, усиливает раму и т.д. И этот велик становится его спутником на достаточно длительный промежуток времени, пока… пока у него не появляются реально сложные, многокомпонентные задачи. И тут, как говорится — вход в супермаркет с «великами» и на роликах запрещен.
P.S.: Автор упомянутой статьи восстановил ее из черновиков — habr.com/ru/post/440382, поэтому можно ознакомиться с ней тоже.
P.P.S.: Я постарался изложить все грани необходимости использования релевантных кодов ответа в REST. Я не буду отвечать на комментарии, прошу понять меня правильно. С большим вниманием буду их читать, но добавить мне нечего. Огромное спасибо за то, что вам хватило терпения прочесть статью!