Обработка запросов от пользователя #
Васильев Андрей Михайлович, 2022
Версии презентации
Базовый цикл работы веб-приложения #
Вход — запрос от клиента, клиентом может выступать:
- Веб-браузер
 - Клиентские приложения curl, wget, HTTPie
 - Скрипты и собственные приложения
 - Специализированные приложения для тестирования приложения
 - Автоматические тесты приложения
 
Выход — ответ сервера
- Статус
 - Заголовки
 - Тело ответа
 
Пользователь должен всегда получать корректный ответ, даже если составил некорректный запрос
Какие данные передаёт пользователь #
- Маршрут к документу
 - HTTP-заголовки
- Наличие: установлен или нет
 - Содержимое: корректно или нет
 
 - Путь к документу с переменными
- Наличие: установлен или нет
 - Содержимое: корректно или нет
 
 - Параметры запроса
- Наличие: установлен или нет
 - Содержимое: корректно или нет
 
 - Содержимое полей формы
- Наличие: установлен или нет
 - Содержимое: корректно или нет
 
 
Для каждого случая необходимо подготовить разумную стратегию обработки ошибок
Неразумные стратегии обработки ошибок #
- Обработка только успешного сценария, отказ от обработки некорректных сценариев
 - «Техническая обработка» — возвращение исключительно HTTP-статусов
 - Потеря данных, переданных пользователем
 
Обработка некорректного маршрута #
Источники некорректных маршрутов #
- Логические ошибки внутри приложения
 - Ошибки, вызванные данными, которые ввели пользователи
 - Некорректная ссылка, сформированная вне приложения
 - Корректная ранее ссылка
 
Подход к обработке #
- Необходимо показать страницу, с которой он сможет открыть другие части сайта
 - Необходимо показать информацию по пришедшему запросу, чтобы пользователь мог сообщить о проблеме
 - Если это возможно, перенаправить запрос на другую страницу с корректным содержимым
 
Фильтры http4k #
fun interface Filter : (HttpHandler) -> HttpHandlerФильтры позволяют выполнить действия, независимые от конкретного запроса:
- Проверить авторизацию пользователя
 - Выполнить обработку внутренних ошибок сервера
 - Выполнить обработку ошибочных запросов от пользователя
 - Выполнить журналирование запросов
 - Настроить общие политики для предоставления содержимого, CORS
 - Выполнить кеширование ответов на запросы
 
Реализация собственного фильтра #
Фильтр принимает в качестве аргумента HttpHandler и должен вернуть HttpHandler
val handler = { _: Request -> Response(OK) }
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 при обращении к несуществующему маршруту
 - Стандартные обработчики тоже могут возвращать неуспешные ответы
 
Подход к реализации:
- Надо выбрать уровень обработки сообщений. Либо только динамические маршруты, либо все маршруты приложения
 - Необходимо реализовать фильтр для обработки неуспешного запроса, отслеживать состояние ответа можно с помощью Status.successful
 
Принципиальный подход к реализации
val errorFilter: Filter = { next: HttpHandler ->
    { request: Request ->
        val response = next(request)
        if (response.successfull) {
            response
        } else {
            renderer(ErrorMessage(request))
        }
    }
}
val system = errorFilter.then(app)Обработка параметров #
Для всех данных, которые передаёт пользователь необходимо выполнить две базовые проверки:
- Переданы ли данные
 - Являются ли они корректными
 
Подходы к обработке данной ситуации:
- Вручную обработать ситуацию наличия или отсутствия данных
 - Использовать специализированный механизм основной библиотеки
 - Реализовать собственную подсистему для обработки запросов
 
Ручная обработка переменных внутри обработчика #
Рассмотрим порядок обработки маршрута с переменной внутри. Стратегия решения проблемы — отображение сообщения, что искомый элемент не был найден
fun handler(renderer: TemplateRenderer): HttpHandler = { request ->
    val paramemter: String? = request.path("parameter")
    if (parameter == null) {
        return@handler Response(UNSATISFIABLE_PARAMETERS).body(
            renderer(EmptyParameterRequestFoundViewModel())
        )
    }
    val entity: Entity? = getEntityByData(data)
    if (entity == null) {
        return@handler Response(NOT_FOUND).body(
            renderer(EntityNotFoundViewModel())
        )
    }
    Response(OK).body(renderer(ShowEntityViewModel(entity)))
}Работа со сложными структурами данных #
При работе со сложными вложенными структурами приходится решать вопрос по получению и изменению свойств вложенных элементов
val structure = listOf(
    mapOf(
        "lang" to "kotlin",
        "name" to "Веб-программирование",
    )
)Для редактирования структуры необходимо сначала получить доступ к элементу массива, а затем уже к свойству словаря
Для сильно вложенной структуры данных цепочка может быть гораздо длиннее. Это несёт следующие проблемы:
- Надо корректно указать путь к данным несколько раз
- Возможны технические ошибки
 - Необходимо написать много кода
 
 - Код начинает зависеть от всех промежуточных объектов
 
Механизм линз #
Линза — специальный объект, который позволяет осуществлять операции чтения и записи для конкретного вложенного свойства
Предположим, что мы создали линзу для получения переменной из пути:
val parameterLens = Path.of("parameter")Для получения параметра:
val parameter:String = parameterLens(request)Потенциально для установки значения можно было бы написать:
val newRequest = parameterLens("newData", request)Но смысла в операции записи на запросе нет
Поддерживаемые линзы #
| Объект | Начальный тип данных | Применимо для объектов | Несколько | Ограничитель требований | 
|---|---|---|---|---|
| Параметры запроса | String | Request | Один или много | Обязательный или необязательный | 
| Заголовок | String | Request / Response | Один или много | Обязательный или необязательный | 
| Переменная пути | String | Request | Один | Обязательный | 
| Поле формы | String | WebForm | Один или много | Обязательный или необязательный | 
| Тело | String | Request / Response | Один | Обязательный | 
Настройка линзы #
При создании линзы необходимо указать:
- Объект, для которого формируется линза
 - Список настроек для линзы
 - Терминатор (если есть обязательные или необязательные параметры)
 
Указание объекта #
Указание типа данных для преобразования #
Данный этап можно пропустить, если необходима строка, однако лучше всегда явно указывать тип для преобразования
http4k предлагает большой набор функций расширения, с помощью которых можно настраивать преобразование данных. Рассмотрим список для типа Query
val request = Request(GET, Uri.of("http://server.ru"))
val requestWithQuery = request.query("data", "42")
val queryDataLens = Query.int().defaulted("data", 15)
val data = queryDataLens(requestWithQuery) // 42Указание необходимости параметра #
Уровни необходимости параметра описываются в рамках класса LensSpec
- defaulted() — нужно указать название элемента, значение по умолчанию и описание
 - optional() — параметр является необязательным, необходимо указать название
 - required() — параметр является обязательным, необходимо указать название
 
Ключевое отличие — поведение при отсутствии целевого значения
- defaulted() возвращает значение по умолчанию
 - optional() возвращает null-тип
 - required() выбрасывает исключение LensFailure
 
Обработка HTML-форм #
Предыдущий рассмотренный сценарий обработки форм не удовлетворяет требованиям по обработке данных:
- Работает только в случае передачи корректных данных
 - Падает с сообщением внутренних проблем при получении данных
 - Теряет данные, которые были введены пользователем
 
Корректный подход к обработке форм #
sequenceDiagram autonumber actor Пользователь participant Браузер participant Сервер Пользователь ->> Браузер: Нажимает на ссылку к странице формы Браузер ->> Сервер: Отправляет GET-запрос по адресу документа Сервер ->> Браузер: HTML-документ, содержащий форму Браузер ->> Пользователь: Показывает документ с формой Пользователь ->> Браузер: Заполняет интерактивные элементы на форме Пользователь ->> Браузер: Нажимает на кнопку отправки запроса Браузер ->> Сервер: POST-запрос с содержимым формы alt Форма не содержит ошибок Сервер ->> Браузер: Ответ со статусом 302, FOUND else Сервер ->> Браузер: HTML-документ, содержащий форму с данными пользователя и сообщениями об ошибках, 3 end
Описание линз для формы #
HTML форма представляет собой набор полей, которые пользователь заполняет
В рамках http4k используются следующие типы данных:
- FormField — описание требований к конкретному полю формы
 - Body.webForm — описание спецификации линзы для всей формы
 - WebForm — структура для хранения результатов проверки формы
 
val ageField = FormField.int().required("age")
val nameField = FormField.map(::Name, Name::value).optional("name")
val feedbackFormBody = Body.webForm(Validator.Feedback, nameField, ageField).toLens()
val invalidRequest = Request(GET, "/")
        .with(Header.CONTENT_TYPE of ContentType.APPLICATION_FORM_URLENCODED)
val invalidForm = feedbackFormBody(invalidRequest)
println(invalidForm.errors)
val validForm = strictFormBody(validRequest)
val age = ageField(validForm)Отображение результатов проверки в форме #
- При первом показе форма должна отображать пустые поля ввода
 - После неудачи форма должна отображать данные, введённые пользователем
 
Удобно использовать объекты класса WebForm в обоих случаях
data class WebForm constructor(
    val fields: Map<String, List<String>> = emptyMap(),
    val errors: List<Failure> = emptyList()
)Для проверки наличия данных в форме можно использовать следующие конструкции:
{% if model.form.errors is not empty %}
    Присутствуют ошибки
{% endif %}
{% if model.form.fields contains "field" %}
    Поле присутствует: {{ model.form.fields["field"] | first }}
{% endif %}Использование макросов Pebble #
Для уменьшения количества повторений внутри шаблонов Pebble можно воспользоваться макросами, которые можно несколько раз вызывать внутри шаблона
{% macro input(type="text", name, placeholder="", form) %}
    <input
        type="{{ type }}"
        name="{{ name }}"
        placeholder="{{ placeholder }}"
        {% if form.fields contains name  %}
            value="{{ form.fields[name] | first }}"
        {% endif %}
    >
{% endmacro %}
{{
input(name="count", type="number", placeholder="Число работников",
    form=model.form)
}}Указание типа документа #
Согласно протоколу HTTP серверу желательно сообщать клиенту тип документа с помощью MIME типов. Также поступает и клиента
Для указания типа документа используется заголовок Content-Type
В http4k для решения этой задачи можно либо воспользоваться фильтрами:
val SetHtmlContentType = Filter { next ->
    { next(it).with(CONTENT_TYPE of TEXT_HTML) }
}
val app = routes(
    return "/create" bind POST to SetHtmlContentType.then someHandler
)Либо использовать линзы:
val renderer = PebbleTemplates().HotReload("src/main/resources")
val htmlView = Body.viewModel(renderer, TEXT_HTML).toLens()
val handler: HttpHandler {
    Response(OK).with(htmlView of SomeViewModel(42))
}