Проверка входного запроса
В предыдущих уроках мы определили адрес, метод и форму JSON. Теперь сервер должен решить, можно ли применять полученный документ. Проверка входа — не одно действие, а несколько границ: поддерживаемое представление, корректная структура и допустимые предметные значения. Порядок помогает объяснить отказ и не менять каталог частично.
Результат урока — правило принятия POST и PUT с конкретными примерами ошибок. Начальный каталог снова содержит четыре курса. Считаем, что право редактора уже установлено; аутентификация появится отдельным этапом. Здесь читаем и проектируем обработку запроса, не запускаем сервер и не выдаём схемы ответов за фактически полученные сообщения.
Сначала представление
Сервер принимает редактируемые поля только как JSON. Если клиент объявил Content-Type: application/xml, тело не передаётся в JSON-разбор с надеждой, что «вдруг получится». Предполагаемый отказ — 415. Такой выбор обнаруживает несогласованность до предметной проверки и сообщает, что клиенту нужно изменить способ представления входа.
Даже при правильном Content-Type байты способны содержать неверный JSON. Рассмотрим полный ошибочный вход с пропущенной запятой:
{"title":"HTTP и API" "topic":"backend","lessons":32}
Эта строка намеренно дана как текст, поскольку не является корректным JSON. Парсер должен отказать; сервис возвращает 400. Нельзя объявить ошибку поля lessons или начать сохранять title, потому что целостного документа ещё нет. До принятия входа предметное состояние остаётся прежним.
Сюда же наш договор относит неоднозначный JSON с повторяющимися именами. Парсер, который сразу оставляет последнее значение, может скрыть такую проблему. Реализации потребуется способ обнаружения повторов до потери информации. Урок не утверждает, что произвольная стандартная функция разбора уже обеспечивает это правило: его нужно включить в выбор конкретной библиотеки.
Реальный сервер также ограничивает размер входа и глубину структуры. У нас всего три коротких поля, но отсутствие ограничения позволило бы прислать огромный документ до проверки title. Протокольная и ресурсная граница располагаются раньше работы с объектом. Подробности защитного стенда изучались в другой серии; здесь важна последовательность принятия данных API.
Затем форма объекта
После успешного разбора верхний уровень должен быть объектом. Массив из одного похожего курса не считается равнозначным входом. Он может иметь правильный JSON-синтаксис, но не соответствует форме POST нашей коллекции. Такой случай получает 422 как неприемлемое содержимое.
Полный корректный документ содержит три обязательных поля:
{"title":"HTTP и API","topic":"backend","lessons":32}
Отсутствующее поле не заменяется произвольным значением по умолчанию. Если lessons пропущено, сервис не берёт число из прежней записи при PUT и не подставляет один при POST. Иначе два клиента могли бы отправлять одинаковый документ, ожидая разные результаты. В нашем договоре обязательность является общей для обеих операций.
Неизвестные входные поля отвергаются. Документ с id не переименовывает ресурс, а документ с isAdmin не изменяет права. Сервер выделяет разрешённую модель, прежде чем передавать значения в хранение. Это предотвращает ситуацию, когда новое внутреннее свойство неожиданно становится доступным через массовое присваивание.
Проверка значений
Изменим lessons на строку, сохранив остальные поля. JSON остаётся корректным, но тип нарушает договор:
{"title":"HTTP и API","topic":"backend","lessons":"32"}
Ожидается 422 с причиной по /lessons. Сервер не преобразует строку в число автоматически. Такое преобразование кажется дружелюбным, однако скрывает ошибку клиента и усложняет допустимые формы: придётся решить, что делать с пробелами, дробью и строкой из нескольких чисел. Явный числовой тип уменьшает эту неоднозначность.
Далее число должно быть целым и попадать в диапазон от одного до тысячи. Значение 32.5 имеет числовой тип, но не подходит для количества уроков; 0 тоже отвергается. Это разные причины внутри одного поля. Клиенту полезно различать стабильные машинные коды, а человеку — видеть конкретное объяснение.
Направление принимается только из объявленного перечня. Значение Backend не приводится автоматически к backend: регистр является частью машинного договора. В интерфейсе редактору можно предложить список вариантов, но сервер повторяет проверку независимо от формы. Программа другого клиента способна отправить произвольную строку, поэтому элемент select не является защитой входа.
Название сначала теряет крайние пробелы, затем проверяется на длину от одной до 120 кодовых точек Unicode. Внутренние пробелы не исчезают. Строка из одних пробелов после нормализации становится пустой и отвергается. Ограничение не измеряется размером HTTP-тела или количеством байтов; эти границы имеют разные задачи.
Одинаковое правило в разных клиентах
Браузерная форма может заранее подсветить отрицательное lessons, а редакторский скрипт — отправить документ сразу. Серверный договор одинаков для обоих: приемлемость определяется полученными значениями и текущим контекстом, а не тем, какой интерфейс их собрал. Название User-Agent не становится обходом валидации.
Полезно сохранять правила в документации вместе с примерами граничных значений. Минимально допустимое количество — один, максимально допустимое — тысяча; пустое title отличается от отсутствующего свойства. Такие конкретные границы лучше неопределённого пожелания «введите разумные данные». Клиент способен повторить часть правил ради удобства, но действительный отказ всё равно сообщает сервис.
Предметная проверка также не должна рассчитывать на ранее загруженные условия. Между открытием формы и отправкой курс может исчезнуть или измениться. Поэтому синтаксически корректный документ проходит ещё проверку цели и, позднее, условие версии. Это не повтор одной функции, а разные границы: какой вход прислан и можно ли применить его сейчас.
Если ошибка меняет форму ответа, её документируется вместе с успешным результатом. Иначе клиент окажется подготовлен только к счастливому пути и потеряет введённые значения при первом отказе. Контракт принятия полезен именно потому, что объясняет обе ветки действия.
Отказ без частичного изменения
Пусть редактор меняет title курса c001, но отправляет неподходящее lessons. Сервер должен отклонить весь документ, сохранив прежнее редактируемое представление. Нельзя принять название и отдельно сообщить, что число не обновлено, если договор обещает целую замену PUT.
Предполагаемая подробная ошибка нашего API выглядит так:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
Cache-Control: no-store
{"type":"https://catalog.example/problems/validation","title":"Недопустимые поля курса","status":422,"detail":"Проверьте значение количества уроков.","errors":[{"pointer":"/lessons","code":"integer-required","detail":"Ожидается целое число от 1 до 1000."}]}
Массив errors — собственное расширение нашего типа проблемы. Следующий урок объяснит его связь с общей формой Problem Details. Здесь обратите внимание на ожидаемый эффект: новая запись не появилась, либо существующая не изменилась. Статус сообщает отказ, расширение связывает его с полем, а деталь помогает исправить вход.
При нескольких независимых ошибках сервер может собрать несколько элементов errors за один проход. Но не стоит продолжать проверку вложенных свойств, когда верхний уровень имеет неверный тип. Хорошая последовательность даёт человеку полезные сведения, сохраняя причины понятными. Иначе сообщения о десятках отсутствующих полей замаскируют один неверный контейнер.
После предметной проверки остаётся текущий контекст. Например, запись могла быть удалена другим редактором: корректный документ не создаёт право восстановить её через PUT, потому что наш API разрешает только замену существующей записи. Для неизвестного id будет 404. Позже условие версии добавит ещё одну границу между корректностью входа и допустимостью изменения текущего состояния.
Клиентская проверка полезна для быстрого объяснения, но не заменяет серверную. При этом клиент должен сохранить введённый документ после отказа, чтобы человек мог исправить поле, а не вводить всё заново. Его интерфейс опирается на ошибки API, не выводя непроверенные внутренние сведения. Назначение статуса 422.
Теперь измените в корректном примере сначала тип, затем диапазон, затем имя поля. Для каждого варианта назовите границу отказа и подтвердите ожидаемое отсутствие изменения каталога. Именно так проверка превращается в договор поведения. Следующий урок даст единую форму описания этих причин, сохранив самостоятельный смысл настоящего HTTP-статуса.