Обработка запросов от пользователя

Обработка запросов от пользователя #

Васильев Андрей Михайлович, 2023

Версии презентации


Базовый цикл работы веб-приложения #

Получение запроса, формирование ответа, отправка ответа клиенту

Вход — запрос от клиента, клиентом может выступать:

  • Веб-браузер
  • Клиентские приложения curl, wget, HTTPie
  • Скрипты и собственные приложения
  • Специализированные приложения для тестирования приложения
  • Автоматические тесты приложения

Выход — ответ серверного веб-приложения

  • Статус
  • Заголовки
  • Тело ответа

Клиентское приложение должно всегда получать корректный ответ. Если был составлен некорректный запрос, то серверное приложение должно оповещать об этом


Данные клиентского запроса #

  • Маршрут к документу
  • HTTP-заголовки
    • Наличие: установлен или нет
    • Содержимое: корректно или нет
  • Путь к документу с переменными
    • Наличие: установлен или нет
    • Содержимое: корректно или нет
  • Параметры запроса
    • Наличие: установлен или нет
    • Содержимое: корректно или нет
  • Содержимое полей формы
    • Наличие: установлен или нет
    • Содержимое: корректно или нет

Для каждого случая необходимо подготовить разумную стратегию обработки данных с предоставлением информации об ошибках


Неразумные стратегии обработки ошибок #

  • Обработка только успешного сценария, отказ от обработки некорректных сценариев
  • «Техническая обработка»:
    • возвращение исключительно HTTP-статусов
    • возвращение единого ответа на все некорректные запросы
  • Потеря некорректных данных, переданных пользователем

Обработка некорректного маршрута #

Источники некорректных маршрутов #

  • Логические ошибки внутри приложения
  • Ошибки, вызванные данными, которые ввели пользователи
  • Некорректная ссылка, сформированная вне приложения
  • Корректная ранее ссылка, которая в настоящее время не работает

Подходы к решению проблемы #

  • Показать страницу, с которой он сможет открыть другие части сайта
  • Показать информацию по пришедшему запросу, чтобы пользователь мог сообщить о проблеме
  • Если это возможно, перенаправить запрос на другую страницу с корректным содержимым

Фильтры http4k #

fun interface Filter : (HttpHandler) -> HttpHandler

Фильтры позволяют выполнить действия, общие для ряда запросов от пользователя:

  • Проверить авторизацию пользователя
  • Выполнить обработку внутренних ошибок сервера
  • Выполнить обработку ошибочных запросов от пользователя
  • Выполнить журналирование запросов
  • Настроить общие политики для предоставления содержимого, установка CORS-заголовков
  • Выполнить кеширование ответов на запросы

Процесс обработки запроса с фильтрами #

  • С помощью фильтров определяются действия, которые могут выполняться до и после работы HTTP-обработчика
  • Фильтр может применяться к одиночному HTTP-обработчику
  • Фильтр может применяться ко всему приложению
sequenceDiagram participant browser as Клиент participant filterOne as Фильтр participant handler as HTTP-обработчик browser ->> filterOne: Выполнение
запроса 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 — фильтры, помогающие в отладке работы приложения
  • 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

Особенности параметров #

На первый взгляд параметры очень напоминают структуру данных словарь, т.к. состоят из пар ключ-значение

Однако есть важное отличие: клиент может передать несколько параметров с одинаковыми ключами

  • Параметры представляют собой набор пар
  • Нет никаких ограничений между параметрами нет
  • Ключи и значения могут быть не только латиницей, для передачи таких данных используется процентная кодировка, сервер её автоматически декодирует
  • Порядок параметров в большинстве случаев не должен быть важен

Обработка параметров #

Для всех данных, которые передаёт пользователь необходимо выполнить две базовые проверки:

  1. Переданы ли данные
  2. Являются ли они корректными

Подходы к обработке данной ситуации:

  • Реализовать собственную подсистему для обработки параметров
    • Реализовать самостоятельную обработку
    • Интегрировать стороннюю библиотеку
  • Использовать механизмы библиотеки 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-запросов являются контроллерами, соединяющими в себе функции других подсистем
  • Подсистема извлечения данных из ввода пользователя (слой валидации) пытается найти в данных от пользователя какой-то смысл.
  • Классы предметной области выполняют обработку данных: извлечение данных, добавление данных и т.п.
  • Слой представления преобразует данные в формат, удобный для пользователя
flowchart LR input("Запрос") data_extraction["Извлечение\nданных"] user_data("Данные\nзапроса") domain_processing["Обработка запроса\nв классах\nпредметной области"] result_data("Результат\nобработки") presentation["Преобразование данных\nв ответ"] output("Ответ") input --> data_extraction --> user_data user_data --> domain_processing --> result_data result_data --> presentation --> output

Написание тестов #

© A. M. Васильев, 2023, CC BY-SA 4.0, andrey@crafted.su