Обработка запросов от пользователя #
Васильев Андрей Михайлович, 2023
Версии презентации
Базовый цикл работы веб-приложения #
Получение запроса, формирование ответа, отправка ответа клиенту
Вход — запрос от клиента, клиентом может выступать:
- Веб-браузер
- Клиентские приложения curl, wget, HTTPie
- Скрипты и собственные приложения
- Специализированные приложения для тестирования приложения
- Автоматические тесты приложения
Выход — ответ серверного веб-приложения
- Статус
- Заголовки
- Тело ответа
Клиентское приложение должно всегда получать корректный ответ. Если был составлен некорректный запрос, то серверное приложение должно оповещать об этом
Данные клиентского запроса #
- Маршрут к документу
- HTTP-заголовки
- Наличие: установлен или нет
- Содержимое: корректно или нет
- Путь к документу с переменными
- Наличие: установлен или нет
- Содержимое: корректно или нет
- Параметры запроса
- Наличие: установлен или нет
- Содержимое: корректно или нет
- Содержимое полей формы
- Наличие: установлен или нет
- Содержимое: корректно или нет
Для каждого случая необходимо подготовить разумную стратегию обработки данных с предоставлением информации об ошибках
Неразумные стратегии обработки ошибок #
- Обработка только успешного сценария, отказ от обработки некорректных сценариев
- «Техническая обработка»:
- возвращение исключительно HTTP-статусов
- возвращение единого ответа на все некорректные запросы
- Потеря некорректных данных, переданных пользователем
Обработка некорректного маршрута #
Источники некорректных маршрутов #
- Логические ошибки внутри приложения
- Ошибки, вызванные данными, которые ввели пользователи
- Некорректная ссылка, сформированная вне приложения
- Корректная ранее ссылка, которая в настоящее время не работает
Подходы к решению проблемы #
- Показать страницу, с которой он сможет открыть другие части сайта
- Показать информацию по пришедшему запросу, чтобы пользователь мог сообщить о проблеме
- Если это возможно, перенаправить запрос на другую страницу с корректным содержимым
Фильтры http4k #
fun interface Filter : (HttpHandler) -> HttpHandler
Фильтры позволяют выполнить действия, общие для ряда запросов от пользователя:
- Проверить авторизацию пользователя
- Выполнить обработку внутренних ошибок сервера
- Выполнить обработку ошибочных запросов от пользователя
- Выполнить журналирование запросов
- Настроить общие политики для предоставления содержимого, установка CORS-заголовков
- Выполнить кеширование ответов на запросы
Процесс обработки запроса с фильтрами #
- С помощью фильтров определяются действия, которые могут выполняться до и после работы HTTP-обработчика
- Фильтр может применяться к одиночному HTTP-обработчику
- Фильтр может применяться ко всему приложению
запроса activate filterOne filterOne ->> filterOne: Выполнение кода
до работы HTTP-обработчика filterOne ->> handler: Передача запроса
на обработку deactivate filterOne handler ->> filterOne: Возврат результата activate filterOne filterOne ->> filterOne: Выполнение действий
после работы HTTP-обработчика filterOne ->> browser: Возвращение
ответа клиенту deactivate filterOne
Реализация собственного фильтра #
Фильтр принимает в качестве аргумента HttpHandler и должен вернуть HttpHandler
val handler: HttpHandler = ...
val myFilter = Filter {
next: HttpHandler -> {
request: Request ->
val start = System.currentTimeMillis()
val response = next(request)
val latency = System.currentTimeMillis() - start
println("I took $latency ms")
response
}
}
val latencyAndBasicAuth: Filter = ServerFilters.BasicAuth(
"my realm", "user", "password")
.then(myFilter)
val app: HttpHandler = latencyAndBasicAuth.then(handler)
Стандартные фильтры http4k #
Пакет org.http4k.filter содержит описание фильтров, поставляемых с помощью http4k
- DebuggingFilters — фильтры, помогающие в отладке работы приложения
- PrintRequest — отображать запрос,
- PrintRequest — отображать ответ
- PrintRequestAndResponse — отображать и запрос и ответ
- ServerFilters — основные серверные фильтры
- BasicAuth — реализация базовой HTTP-авторизации
- CatchAll — перехватывание всех внутренних исключений, возникающих при обработке сообщений
- Cors — установка CORS-заголовков
- HandleRemoteRequestFailed — обработка ошибок от HTTP-запросов к внешним ресурсам
- RequestTracing — структурированное отслеживание запросов в формате Zipkin
Обработка статуса 404 #
Статус NOT_FOUND используется для оповещения пользователя о том, что по указанному запросу данные не были найдены
- Маршрутизатор по умолчанию возвращает ответ со статусом 404 при обращении к несуществующему маршруту
- Стандартные обработчики тоже могут возвращать ответы со статусом 404, например если параметр в пути был передан некорректно
На уровне фильтра при возникновении ошибки 404 можно сформировать страницу с информацией для пользователя
Подход к реализации:
- Надо выбрать уровень обработки сообщений:
- Либо только динамические маршруты
- Либо все маршруты приложения
- Необходимо реализовать фильтр для обработки неуспешного запроса, отслеживать состояние ответа можно с помощью Status.successful
Реализация фильтра «в лоб» #
Рассмотрим наивный подход к реализации такого фильтра
val errorFilter = Filter { next: HttpHandler ->
{ request: Request ->
val response = next(request)
if (response.status.successfull) {
response
} else {
response.body(renderer(ErrorMessage(request)))
}
}
}
val application = errorFilter.then(router)
Обработка переменных внутри обработчика #
Рассмотрим порядок обработки маршрута с переменной внутри. Стратегия решения проблемы — отображение страниц с деталями информации по ошибкам
class Handler(renderer: TemplateRenderer,
getEntityByData: (Int) -> Entity?) : HttpHandler {
override fun invoke(request: Request) {
val paramemter: String? = request.path("parameter")?.toIntOrNull()
if (parameter == null) {
return Response(UNSATISFIABLE_PARAMETERS).body(
renderer(EmptyParameterRequestFoundVM())
)
}
val entity: Entity? = getEntityByData(parameter)
if (entity == null) {
return Response(NOT_FOUND).body(
renderer(EntityNotFoundVM())
)
}
Response(OK).body(renderer(ShowEntityVM(entity)))
}
}
Выдача одного сообщения об ошибке #
Упростим обработчик, чтобы он выдавал один вид ошибки при разных конкретных деталях ошибки:
class Handler(renderer: TemplateRenderer,
getEntityByData: (Int) -> Entity?) : HttpHandler {
override fun invoke(request: Request) {
return request
.path("parameter")
?.let { parameter ->
getEntityByParameter(parameter)
}?.let { entity ->
Response(OK).body(renderer(ShowEntityVM(entity)))
} ?: Response(NOT_FOUND).body(renderer(EntityNotFoundVM()))
}
}
Использование фильтра ошибочного кода #
Выполним следующее упрощение: будем возвращать только код 404, а обработку ошибочной ситуации доверим общему фильтру приложения
class Handler(renderer: TemplateRenderer,
getEntityByData: (Int) -> Entity?) : HttpHandler {
override fun invoke(request: Request) {
return request
.path("parameter")
?.let { parameter ->
getEntityByParameter(parameter)
}?.let { entity ->
Response(OK).body(renderer(ShowEntityVM(entity)))
} ?: Response(NOT_FOUND)
}
}
Обработка в слое представления #
Или путём обработки данной ситуации на уровне шаблона Pebble
Разрешаем null-состояние для модели
data class ShowEntityVM(entity: Entity?)
Передаём неопределённое состояние в модель
class Handler(renderer: TemplateRenderer,
getEntityByData: (Int) -> Entity?) : HttpHandler {
override fun invoke(request: Request) {
val entity = request
.path("parameter")
?.let { parameter ->
parameter.toIntOrNull()
}?.let { number ->
getEntityByParameter(parameter)
}
return Response(OK).body(renderer(ShowEntityVM(entity)))
}
}
Обработка в слое представления #
Проверка на null в шаблоне Pebble:
{% if model.entity is null %}
...
{% else %}
...
{% endif %}
Однако такой подход не следует активно использовать:
- Шаблонизатор исполняет код медленнее компилированного кода приложения
- Шаблонизатору приходится работать в рамках враждебного окружения, что значительно повышает сложность восприятия, следовательно повышается риск ошибок
В рамках курса такой подход запрещён
Параметры в URI-запросах #
Структура URI
[ схема ":" ] [ // источник ] путь [ "?" запрос ] [ "#" фрагмент ]
- Запрос отделяется от пути знаком вопроса
- В запросе данные могут быть отформатированы любым образом, однако обычно применяется схема с передачей набора параметры
- параметр — это пара ключ-значение, разделённые знаком
=
- параметры отделяются друг от друга знаком
&
- параметр — это пара ключ-значение, разделённые знаком
http://some.domain/some/path?key1=value1&key2=value2&key3=value3
- Передаётся 3 пары параметров с именами key1, key2 и key3
- Значения параметра key1 — это value1
Особенности параметров #
На первый взгляд параметры очень напоминают структуру данных словарь, т.к. состоят из пар ключ-значение
Однако есть важное отличие: клиент может передать несколько параметров с одинаковыми ключами
- Параметры представляют собой набор пар
- Нет никаких ограничений между параметрами нет
- Ключи и значения могут быть не только латиницей, для передачи таких данных используется процентная кодировка, сервер её автоматически декодирует
- Порядок параметров в большинстве случаев не должен быть важен
Обработка параметров #
Для всех данных, которые передаёт пользователь необходимо выполнить две базовые проверки:
- Переданы ли данные
- Являются ли они корректными
Подходы к обработке данной ситуации:
- Реализовать собственную подсистему для обработки параметров
- Реализовать самостоятельную обработку
- Интегрировать стороннюю библиотеку
- Использовать механизмы библиотеки http4k
Низкоуровневый доступ к параметрам #
- В объекте запроса (Request) предоставляется поле
uri
, содержащее объект класса Uri - Класс Uri предоставляет следующие методы для работы с параметрами:
- Свойство
query
содержит строку запроса полностью - Функция
fun query(query: String): Uri
позволяет создать новый объект Uri с новым значением запроса - Функция
fun queries(): Parameters
возвращает набор пар ключ-значения - Функция
fun Uri.query(name: String, value: String?): Uri
позволяет добавить новый параметр к новому Uri-объекту - Функция
fun Uri.removeQuery(name: String): Uri
позволяет удалить все параметры с указанным ключом, новое состояние сохраняется в возвращаемом объекте типа Uri
- Свойство
Для установки нового значения для параметра надо сначала удалить старые, а после добавить один новый
val newUri = request.uri.removeQuery("data").query("data", "value")
Формирование параметров на стороне клиента #
- Синтаксис для формирования параметров достаточно простой, можно оформить руками
- Для шаблонизатора можно предоставить функцию-расширения, которая позволит добавлять нужные параметры
- Можно воспользоваться интерактивными элементами - формами https://developer.mozilla.org/ru/docs/Learn/Forms
Отображение HTML-форм #
Формы являются частью языка HTML, для их описания используются следующие элементы:
- Form — базовый контейнер для всех полей ввода, расположенных на форме
- Input — элемент для описания интерактивных элементов
- Textarea — элемент для ввода многострочного текста
<form method="POST">
<input type="text" name="start">
<textarea name="description">
<input type="submit" value="Отправить">
</form>
Сценарий извлечения информации #
- Фильтрация
- Сортировка
Архитектура HTTP-обработчиков #
- Обработчики HTTP-запросов являются контроллерами, соединяющими в себе функции других подсистем
- Подсистема извлечения данных из ввода пользователя (слой валидации) пытается найти в данных от пользователя какой-то смысл.
- Классы предметной области выполняют обработку данных: извлечение данных, добавление данных и т.п.
- Слой представления преобразует данные в формат, удобный для пользователя