HTTP и REST в Java
Отправил в одно и то же приложение шесть запросов и записал ответы. POST дважды - 201 и 201, но с разными адресами: /orders/1 и /orders/2, то есть создались ДВА заказа. PUT дважды - 200 и 200, результат одинаковый. DELETE дважды - 204 и 204. Обращение чужим методом - 405 и заголовок Allow со списком разрешённых.
Всё это не украшательство. По кодам и заголовкам принимает решения не твой код, а то, что стоит по дороге: клиентская библиотека, кэширующие прокси-серверы, балансировщик. Они смотрят на код ответа и решают, можно ли повторить запрос и что положить в кэш. Тема проверяет, читаешь ли ты HTTP как договор, а не как транспорт для JSON.
// Формулировки: «что такое идемпотентность метода?», «чем 401 отличается от 403?», «когда 400, а когда 422?», «зачем ETag?».
Идемпотентность: почему повтор безопасен не всегда
Идемпотентный метод - тот, повторное выполнение которого не меняет результат по сравнению с однократным. Мой замер показывает это буквально. PUT /orders/7 дважды дал один и тот же итог: ресурс просто заменяется целиком. DELETE дважды тоже: после первого удаления второй запрос ничего нового не ломает. А POST дважды создал ДВА разных заказа - потому и не идемпотентен.
Практическая ценность в повторах. Сеть ненадёжна: клиент отправил запрос, ответ потерялся, и клиент не знает, дошло или нет. Повторить идемпотентный запрос безопасно, и именно поэтому библиотеки и балансировщики повторяют GET, PUT и DELETE автоматически, а POST нет.
// Когда создание всё-таки надо защитить от дублей, вводят ключ идемпотентности: клиент присылает заголовок с уникальным идентификатором попытки, сервер запоминает его вместе с результатом и на повтор отдаёт тот же ответ вместо второго заказа. Так работают платёжные API - и именно поэтому у тебя не списывают деньги дважды при обрыве связи.
POST /orders -> 201 Location: /orders/1
POST /orders -> 201 Location: /orders/2 два разных заказа
PUT /orders/7 -> 200
PUT /orders/7 -> 200 тот же результат
DELETE /orders/7 -> 204
DELETE /orders/7 -> 204 тот же результат- идемпотентность
- повтор запроса даёт тот же результат, что и один вызов
- ключ идемпотентности
- идентификатор попытки: сервер отдаёт на повтор прежний ответ
Коды, которые путают
401 против 403. 401 значит «я не понял, кто ты»: токена нет, он просрочен или испорчен - имеет смысл переполучить его и повторить. 403 значит «понял, кто ты, и тебе нельзя» - повторять бессмысленно, нужны другие права.
400 против 422. 400 - запрос сломан по форме: не разбирается JSON, не тот тип поля. 422 - форма верна, но содержание не проходит по смыслу: дата окончания раньше даты начала. Различие полезно клиенту, потому что подсказывает, чинить разбор или данные.
// Ещё три пары. 404 против 403 на чужом ресурсе: иногда специально отвечают 404, чтобы не подтверждать существование объекта. 409 - конфликт состояния: например, повторная попытка занять уже занятое имя. 429 - слишком много запросов, и к нему полагается заголовок Retry-After, иначе клиент будет долбиться дальше. А 405, как в моём замере, обязан нести Allow со списком поддерживаемых методов - у меня туда попали POST и GET.
- 401 / 403
- неясно кто ты / ясно, но нельзя
- 400 / 422
- запрос сломан по форме / верен по форме, но не проходит по смыслу
Заголовки и отсутствие состояния
Заголовки несут всё, что не является данными. Content-Type и Accept договариваются о формате. Authorization несёт токен. Cache-Control и ETag управляют кэшированием.
ETag - это отпечаток текущего содержимого ресурса. Я замерил, как он работает: первый GET вернул 200 и 25 байт тела вместе с ETag. Второй GET с заголовком If-None-Match, куда я подставил этот отпечаток, вернул 304 и НОЛЬ байт тела. Данные не изменились, и сервер честно сэкономил передачу - клиент берёт то, что у него уже есть.
// Отсутствие состояния (stateless) означает, что сервер не помнит клиента между запросами: всё нужное приходит в самом запросе. Это условие горизонтального масштабирования - любой экземпляр приложения может обработать любой запрос, и не нужно привязывать пользователя к конкретной машине. А для долгих операций есть отдельный приём: сервер отвечает 202 «принято» и адресом, по которому можно спрашивать статус, вместо того чтобы держать соединение открытым несколько минут.
GET /item/1 -> 200 25 байт ETag: "08e8b45..."
GET /item/1 If-None-Match: ... -> 304 0 байт- ETag
- отпечаток содержимого: при совпадении сервер отвечает 304 без тела
- stateless
- сервер не помнит клиента между запросами - всё в самом запросе
Как отвечать: «Что такое идемпотентность метода и почему она важна?»
Идемпотентный метод даёт тот же результат при повторе, что и при одном вызове. Я это проверял: PUT одного и того же ресурса дважды дал одинаковый ответ, DELETE дважды тоже, а вот POST дважды создал два разных заказа с адресами /orders/1 и /orders/2. Важно это из-за ненадёжной сети: клиент отправил запрос, ответ потерялся, и он не знает, дошло или нет. Идемпотентный запрос можно спокойно повторить, поэтому клиентские библиотеки и балансировщики повторяют GET, PUT и DELETE, а POST не повторяют. Когда мне нужно защитить именно создание, я ввожу ключ идемпотентности: клиент присылает уникальный идентификатор попытки, сервер запоминает его вместе с результатом и на повтор возвращает прежний ответ вместо второго заказа. Так устроены платёжные API, чтобы при обрыве связи деньги не списались дважды.
Почему это сильный ответ: определение подкреплено наблюдаемым поведением, объяснена связь с повторами в ненадёжной сети и назван рабочий приём для неидемпотентного случая.
На чём валят
- −Отвечать 200 на ошибку, положив описание в тело. Клиент, прокси и мониторинг видят успех.
- −Считать POST безопасным для повтора. У меня два одинаковых POST создали два заказа - нужен ключ идемпотентности.
- −Путать 401 и 403. Первое лечится новым токеном, второе не лечится повтором вообще.
- −Отдавать 405 без заголовка Allow. Клиент не узнает, какие методы поддерживаются, хотя это часть контракта.
- −Держать соединение открытым на долгую операцию. Для этого есть 202 и отдельный адрес для проверки статуса.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 16, остальные разбираются в тренажёре.
- Что означает код ответа 401 в отличие от 403?A)401 — ресурс не найден на сервере, а 403 — сервер временно недоступен из-за перегрузки запросамиB)401 и 403 — синонимы «доступ запрещён», клиент выбирает один из них по своему усмотрениюC)401 — не аутентифицирован (кто ты?); 403 — аутентифицирован, но нет правD)401 — ошибка на стороне сервера, а 403 — ошибка в самом запросе клиента из-за неверного формата
показать ответ и разбор
+C)401 — не аутентифицирован (кто ты?); 403 — аутентифицирован, но нет прав// разбор: 401 Unauthorized (по факту — «не аутентифицирован»): сервер не знает, КТО ты — учётные данные отсутствуют, невалидны или истёк токен; клиенту стоит (пере)аутентифицироваться. 403 Forbidden: сервер ЗНАЕТ, кто ты (аутентификация прошла), но у тебя НЕТ ПРАВ на это действие/ресурс — повторная аутентификация не поможет. Путаница этих кодов — частая ошибка API. Оба — из семейства 4xx (ошибка клиента). Ещё смежные: 404 (нет ресурса, иногда специально вместо 403 чтобы не раскрывать существование).
- К какому семейству относятся коды 4xx и 5xx?A)4xx — успешные ответы с предупреждением, 5xx — перенаправления на другой адрес ресурсаB)4xx — ошибки сервера, которые нужно чинить в коде, а 5xx — нормальные клиентские ситуацииC)4xx и 5xx оба означают успех, разница лишь в том, кэшируется ответ или нет по умолчаниюD)4xx — ошибка клиента (запроса); 5xx — ошибка сервера
показать ответ и разбор
+D)4xx — ошибка клиента (запроса); 5xx — ошибка сервера// разбор: Семейства статусов: 1xx информационные, 2xx успех (200 OK, 201 Created, 204 No Content), 3xx перенаправление (301, 304 Not Modified), 4xx — ошибка КЛИЕНТА/запроса (400 Bad Request, 401, 403, 404, 409 Conflict, 422 Unprocessable, 429 Too Many Requests), 5xx — ошибка СЕРВЕРА (500 Internal, 502 Bad Gateway, 503 Service Unavailable, 504 Timeout). Правильный выбор кода — часть корректного REST: невалидный ввод → 400/422, нет прав → 403, конфликт версий → 409, а не «всё 200 с ошибкой в теле». По коду клиент (и мониторинг) понимает, кто виноват и что делать.
- Что значит принцип statelessness в REST?A)Каждый запрос самодостаточен; сервер не хранит клиентскую сессию между запросамиB)Сервер обязан хранить полное состояние каждого клиента, чтобы отвечать на его следующие запросыC)Клиент не имеет права передавать никаких данных в запросе — всё состояние держится на сервереD)Все запросы к серверу должны выполняться строго последовательно, без параллельной обработки
показать ответ и разбор
+A)Каждый запрос самодостаточен; сервер не хранит клиентскую сессию между запросами// разбор: Statelessness — один из ограничений REST: каждый запрос содержит ВСЮ информацию для обработки (идентификаторы ресурса, токен аутентификации), а сервер НЕ хранит клиентский контекст между запросами. Плюсы: горизонтальная масштабируемость (любой инстанс обслужит любой запрос — не нужна «липкая» сессия), простота, устойчивость. Состояние приложения (что за пользователь) переносят в токен/запрос, а данные — в БД. Отсюда популярность stateless JWT вместо серверных сессий в распределённых системах.
- Как по REST-соглашению назвать эндпоинт получения заказов пользователя?A)GET /getUserOrders?userId={id} — действие в имени пути, чтобы сразу было понятно, что делает эндпоинтB)GET /users/{id}/orders — ресурсы существительными, иерархией, действие в методеC)POST /orders/fetchByUser с id в теле запроса, ведь чтение данных удобнее оформлять методом POSTD)GET /user_order_list_{id}_all — максимально подробное имя, кодирующее все детали выборки в пути
показать ответ и разбор
+B)GET /users/{id}/orders — ресурсы существительными, иерархией, действие в методе// разбор: REST моделирует РЕСУРСЫ (существительные), а ДЕЙСТВИЕ выражает HTTP-метод. Поэтому «заказы пользователя» — это GET /users/{id}/orders (коллекция orders, вложенная в конкретного user). Глаголы в пути (/getUserOrders), чтение через POST, кодирование действий/фильтров в самом имени — антипаттерны (RPC-стиль поверх HTTP). Множественное число для коллекций (/users, /orders), id в пути для конкретного ресурса, фильтры/сортировка/пагинация — в query. Единообразный ресурсный нейминг делает API предсказуемым.
- Почему GET-запрос не должен изменять состояние на сервере?A)Потому что GET не способен передать данные на сервер, поэтому изменить что-либо им не получитсяB)Потому что GET-запросы шифруются, а операции записи требуют открытого незашифрованного каналаC)GET — safe-метод: его кэшируют, повторяют, префетчат; изменения приведут к побочным эффектамD)Потому что браузеры запрещают отправку GET-запросов к эндпоинтам, меняющим данные
показать ответ и разбор
+C)GET — safe-метод: его кэшируют, повторяют, префетчат; изменения приведут к побочным эффектам// разбор: GET определён как SAFE (не меняющий состояние) и идемпотентный. На это полагается вся инфраструктура: прокси и браузеры КЭШИРУЮТ GET, поисковики и префетчеры ОБХОДЯТ ссылки, клиенты спокойно ПОВТОРЯЮТ GET при сбоях. Если GET что-то меняет (удаляет по ссылке, инкрементит), эти действия сработают неожиданно и многократно — классическая уязвимость/баг («краулер прошёл по ссылкам-действиям и всё поудалял»). Поэтому любые изменения — только через POST/PUT/PATCH/DELETE, а GET оставляют чистым чтением.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.