#1: Пилот: слепая печать, коды ошибок
Пилотный выпуск без гостей. Александр Пахомов рассказывает, почему десятипальцевая (слепая) печать — важный навык для программиста и где её освоить, а затем разбирает проектирование ошибок публичного REST API: какие HTTP-коды возвращать и почему тело ошибки стоит отдавать в формате RFC 7807 (problem+json).
Главное
- Слепая (десятипальцевая) печать ценна не столько скоростью, сколько лёгкостью входа в состояние потока и комфортом работы без подсказок IDE.
- Осваивать слепую печать удобно короткими подходами на тренажёрах TypingClub и Clava; Clava умеет тренировать символы языков программирования.
- Для внутреннего API достаточно команде договориться о формате ошибок; публичный API обязан следовать внешним конвенциям HTTP.
- Коды 4xx сигнализируют об ошибке клиента и требуют понятного тела с объяснением, что не так и что делать; коды 5xx означают ошибку на стороне сервера.
- В ответах 5xx нельзя раскрывать детали (stack trace, версии библиотек) — это security-антипаттерн; возвращайте абстрактный «internal server error».
- Коды 503 и 504 обычно возвращает инфраструктура (например, Kubernetes), а не код приложения.
- Стандарт RFC 7807 (медиатайп application/problem+json) задаёт единое тело ошибки с полями type, title, status, detail и instance.
- В экосистеме Spring формат problem+json даёт библиотека Zalando Problem; на Micronaut обёртку легко написать самостоятельно.
Ссылки
Расшифровка
[00:19] Здарова! Меня зовут Саша Пахомов, и я инженер, который любит своё дело. Это пилотный выпуск подкаста, у которого пока нет ни названия, ни гостей — зато есть куча тем, которые я хочу вместе с вами разобрать. Сегодня я расскажу, почему слепая печать — необходимый навык для программиста, а потом мы погрузимся в дебри разработки публичного REST API: поймём, в каком формате возвращать ошибки, и разберёмся с популярными кодами ошибок. Поехали!
[00:56] Я помню первый урок информатики в школе — там мы изучали, как правильно печатать на клавиатуре, тот самый метод слепой печати. Но, как и большинство изученного в школе, изучено оно не было. По-настоящему погрузиться в десятипальцевую печать мне удалось, только уже будучи программистом, на четвёртом курсе университета.
[01:16] Давайте разберёмся, что такое десятипальцевая, или слепая, печать. Это не просто когда мы не смотрим на клавиатуру, используя два-четыре пальца, — это когда мы задействуем все десять пальцев при наборе символов.
[01:35] Кто-то может возразить, что скорость десятипальцевой печати не так сильно отличается от беглого набора четырьмя пальцами — мол, куда ты денешь сэкономленные две секунды жизни? Но лично для меня слепая печать — это не про скорость и не про сэкономленные секунды. Это про ощущения: в первую очередь — про удовольствие и удовлетворённость от самого процесса работы.
[02:02] К неоспоримым преимуществам я бы отнёс то, что состояние потока достигается гораздо проще: подсказки IntelliJ IDEA уже не так нужны в отсутствие самой IntelliJ IDEA, и ты чувствуешь себя комфортно. Приведу пример: когда нужно быстро напечатать пример кода во время объяснения коллеге в Zoom-митинге, имея под руками IDE, мы быстренько печатаем PSVM, жмём Tab, получаем public static void main и погнали дальше. Но если IDE под руками нет — это становится проблемой. Я скорее предпочту не печатать и не объяснять, чем заставлять коллегу смотреть, как я с ошибками набираю public static void main. Для меня это создаёт психологический барьер. А если ты умеешь быстро печатать — этого барьера почти нет.
[02:58] Следующее преимущество — так называемое «ощущение гика» на собеседовании. Когда тебя собеседуют, ты можешь спокойно зашерить экран, открыть любой редактор — хоть Notepad, хоть Vim — и начать показывать пример кода, печатая со скоростью своей речи. Это воспринимается намного профессиональнее и производит больше впечатления на собеседующего.
[03:27] И последнее преимущество — глаза не так сильно устают. Когда взгляд бегает между клавиатурой и экраном, это создаёт дополнительное напряжение для мышц глаз, и устают они сильнее.
[03:43] Как изучить слепую печать, ведь у нас так мало свободных ресурсов? Пока билдится проект или ждём CI; на скучных митингах — например, на daily, когда сам уже всё сказал и вроде слушаешь коллег, но не во всём внимании — можно открыть тренажёр и пройти 5–10-минутный урок. Почему бы и нет? Или, как делал я, — в начале дня: это был мой ритуал. Каждый рабочий день я садился за компьютер, включал приятную музыку и с пользой проводил полчаса.
[04:19] Где изучать? Лично я осваивал слепую печать на ресурсе typingclub.com — там в игровой форме, с ачивками и звёздочками, с ростом сложности и прохождением уровней, потихоньку начинаешь печатать вслепую. А после моего поста в Telegram-канале мне в комментариях подсказали ещё один интересный ресурс — clava.org. Его особенность в том, что он позволяет печатать не только предложения на английском или русском, но и на языках программирования — то есть тренировать специфичные символы: открывающие-закрывающие скобки, равно, точку с запятой и так далее. Признаюсь, после typingclub.com мне было не очень удобно ставить точку с запятой в конце строки на Java, и я дополнительно себя заставлял. Так что если вам интересна слепая печать именно с точки зрения программирования — посмотрите clava.org. Все ссылки приложу.
[05:31] А теперь — к основной теме подкаста. Я разрабатываю распределённую open-source базу данных, которая предоставляет публичные REST API. Недавно передо мной встала задача стандартизации формата возвращаемых ошибок, и эта тема оказалась очень интересной — сейчас поделюсь.
[05:52] Вообще, REST расшифровывается как Representational State Transfer. Простыми словами, это подход к разработке распределённых веб-сервисов, когда клиент получает доступ к ресурсам сервера поверх протокола HTTP. Чёткого определения REST в индустрии нет: первое историческое упоминание было в 2000 году в диссертации Роя Филдинга, но с тех пор прошло 22 года, и канонический REST претерпел некоторые инженерные адаптации — в частности, в том, что касается ошибок, возвращаемых клиенту.
[06:24] Поговорим о том, в каком формате возвращать ошибки из публичного REST-сервиса. Я делаю акцент на слове «публичного», потому что внутренний API должен соблюдать лишь один критерий — о нём нужно договориться. Если мы договорились, что все ошибки возвращаем с кодом 200, а реальный код передаём в теле, — давайте так и будем делать везде; главное, что договорились. Но когда мы разрабатываем публичный API, который используют за пределами компании, внутренних договорённостей уже нет — есть внешние. Странно возвращать 200 на internal error.
[07:01] И во внутреннем, и во внешнем сервисе к ошибкам есть негласные требования. Первое: должно быть понятно, что пошло не так и что с этим делать, если виноват клиент. Второе: формат тела ошибки должен быть всегда одинаковым — мы не должны на один 400 возвращать одно тело, а на другой 400 — другое. И третье: код должен соответствовать ситуации.
[07:47] Коды делятся на группы по первой цифре — 1, 2, 3, 4, 5. Всё, что начинается на 1 (100, 101, 102), — это informational; на практике я их почти никогда не использовал, и вам вряд ли понадобится.
[08:09] Дальше — двойки. 200 OK — всё хорошо, запрос выполнен. 201 Created — когда клиент попросил создать ресурс, и мы его создали: возвращаем не 200, а 201, подтверждая, что эффект произошёл. 202 Accepted — когда мы принимаем асинхронную задачу (например, «зашедуль мне таску»): можно вернуть и 200, но 202 более конкретно говорит, что задачу приняли и когда-нибудь выполним. 204 No Content — например, когда пытаемся удалить ресурс, которого нет.
[09:03] Тройки — это редиректы; скорее всего, вам их возвращать не придётся, но сталкиваться иногда приходится (всякий middleware). В своей практике я с ними сталкивался очень редко.
[09:23] Самое интересное — четырёхсотки: 400, 401, 403 и так далее. Суть в том, что это ошибки по вине клиента: он сформировал неправильный запрос, обратился к несуществующему ресурсу, превысил количество запросов. Причина не в баге на бэке, а в ошибке клиента. И раз мы, сервер, возвращаем ошибку и хотим сказать клиенту «чувак, ты не прав», нужно сделать это максимально понятно. Недостаточно вернуть 400 и «bad request» — нужно объяснить, что конкретно не так.
[10:06] Самая первая ошибка — 400 Bad Request: клиент сформировал запрос, который мы не можем распарсить или который невалиден (вариантов сотни). В ответе нужно вернуть максимально чёткое и понятное тело: что пошло не так и что сделать. 401 Unauthorized — говорящая сама за себя; тела обычно не нужно, и чаще всего её за нас возвращают Spring, Micronaut и другие фреймворки. То же и с 403 Forbidden — «запрещено»: когда пользователь без нужных прав пытается добраться до ресурса (например, обычный пользователь хочет открыть admin page). Скорее всего, Spring Security проверит, что пользователь есть и authorized, но в доступе ему отказано.
[11:28] Самая знаменитая — 404 Not Found: возвращаем, когда ресурса нет (например, запросили account/3, а у нас всего два аккаунта с id 1 и 2). Тело тут обычно не нужно — и так всё очевидно. 405 Method Not Allowed — экзотика: есть GET на /users/pictures, но нет POST на него — фреймворк вернёт 405. Из технических четырёхсоток ещё стоит знать 429 Too Many Requests — мы упёрлись в rate limiting на стороне сервера: например, клиенту разрешено 500 запросов в минуту, и на 501-м он получит 429. Про rate limiting, наверное, стоит записать отдельный выпуск. И 413 Payload Too Large — обычно генерируется фреймворком автоматически.
[13:19] Последняя группа — пятисотки, и их немного. На стороне кода мы, скорее всего, будем генерировать только 500 — internal server error. Важно: в отличие от четырёхсоток, 500 вызвана не клиентом, а сервером (упал, NPE, out of memory и так далее). Ключевая мысль: в пятисотках нельзя отдавать никакой детальной информации о том, что пошло не так.
[14:17] Почему нельзя, например, отдать null pointer exception из internal-сервиса? Раскрывать информацию о внутреннем устройстве кода — всегда антипаттерн, причём security-антипаттерн. Что плохого в том, чтобы выплюнуть stack trace в пятисотке? Мы показываем структуру кода и инфраструктуру: если это Spring-приложение — будет спринговый stack trace, возможно, Spring Security в фильтрах. Потенциальный злоумышленник поймёт, что здесь Spring, определит версию библиотеки по классам — и если в этой версии есть уязвимость, сможет её проэксплуатировать. Поэтому 500 нужно возвращать максимально абстрактной: «internal server error» — отличная формулировка, которая говорит, что что-то пошло не так, а что именно — знать не надо. Сорян, чувак.
[15:17] 503 и 504, скорее всего, вернёт не наш код, а инфраструктура — тот же Kubernetes. 503 Service Unavailable — например, один контейнер с сервисом уже остановлен, а второй ещё поднимается и не готов обслуживать (хотя в идеале должен быть и третий, который продолжает обслуживать). 504 — Gateway Timeout. На этом про коды всё.
[16:00] Дальше — про тело ошибки. Как я уже говорил, в четырёхсотках хочется возвращать не только код, но и то, что пошло не так: детали, типы ошибки, внутренние коды. Чётких спецификаций тут найти сложно — каждый возвращает тело в удобном ему формате. В Java с этим неплохо справляются современные фреймворки — Spring, Micronaut, Quarkus: они не показывают stack trace, а отдают структурированный JSON. В Spring это timestamp, status (error code), error (название, например internal server error), message и path, по которому произошла ошибка. Выглядит нормально. Но Micronaut возвращает ошибки в другом формате, с другими названиями полей — и клиенту от этого разнобоя довольно неудобно: нужно знать, какая там DTO и реализация.
[17:32] Разрабатывая публичный REST API, я подумал: наверняка есть какой-то принятый стандарт тела ошибки — и да, он есть. Это RFC 7807, который описывает стандартное тело ошибки. Называется оно Problem (problem+json) — это медиатайп application/problem+json. У него есть несколько хорошо специфицированных полей.
[18:24] Первое поле — type, строка, URI-ссылка на описание ошибки. Если у нас есть специфицированное описание внутренней ошибки, желательно иметь веб-страницу, которая её описывает. Честно говоря, у себя в сервисе я это поле не использую. Следующее — title, строка: короткое человекочитаемое описание проблемы (например, «User Not Found»). Затем status, число — HTTP-статус-код, который должен совпадать со статус-кодом в заголовке ответа. Поле detail, строка, — то самое, что объясняет клиенту, что пошло не так и что с этим делать; оно должно содержать минимум внутренних деталей и быть максимально простым (например: «У пользователя недостаточно средств: вы хотели списать 200 рублей, на счёте — 100. Пополните кошелёк»). И последнее поле — instance, строка, тоже URI: ссылка на конкретный инстанс, где возникла ошибка (полезно, когда сервис распределённый и ошибка возникает не везде).
[20:24] RFC определяет не только набор полей, но и конвенции между ними — какой URI должен быть относительным, какой нет. Если будете реализовывать problem+json — этот RFC легко нагуглить и почитать. Моя цель — сказать, что такой стандарт есть: если у вас нет причин делать иначе и вы думаете, какой формат ошибки выбрать, обратите внимание на problem+json. Он максимально понятный и простой, и главное — вы не изобретёте ещё один стандарт, потому что он уже существует. Введён в 2016 году — почему бы им и не пользоваться?
[21:13] Кстати, Spring поддерживает этот тип — точнее, есть библиотека от Zalando под названием Problem: она описывает DSL, которым удобно создавать problem+json и оборачивать все внутренние ошибки в единый стандарт. Очень удобно, советую. Я сейчас пишу веб-сервис на Micronaut, и написать обёртки над DTO самому — тривиальная задача: минут за 20 всё сделал, причём, как мне кажется, даже красивее, чем библиотека.
[21:49] На этом основная часть завершается. В заключение я делюсь полезными ссылками, видео и книгами. Сегодня это мой Telegram-канал «Душный enterprise», где я делюсь мыслями, иногда бомблю, а последнее время просто молчу. Оставляйте комментарии на подкаст-площадках или под постом в Telegram-канале — это очень важно для развития подкаста и важно для меня лично. На этом всё. Услышимся!