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

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

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

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


Архитектура приложения и её проектирование #

Процесс проектирования архитектуры приложения может быть

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

Архитектура приложения — это комбинация выбранных подходов к формированию приложения, а также результирующая структура исходного кода

Обычно архитектура проявляется в подходе к разделению зон ответственности исходного кода приложения на части


Подходы к разделению #

Деление по функциям предметной области #

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

  • Управление комнатами и списками постояльцев
  • Управление персоналом гостиницы
  • Управление автопарком гостиницы

Для каждой из этих областей можно выделить класс или набор классов в выделенном пакете


Деление по техническим аспектам #

Выделение кода, описывающего предметную область #

Любое достаточно сложное приложение содержит нетривиальную логику по модификации данных, эту логику удобно отделить от технической части приложения

  • Минимизация зависимостей позволяет легче тестировать классы предметной области
  • Каждая часть приложения будет достаточно сложной
  • В каждой части можно будет решать одновременно меньше задач

Выделение различных технических частей приложения #

Относительно веб-приложения можно рассмотреть следующие части

  • Обработчики HTTP-запросов
  • Фильтры
  • Слой валидации данных
  • Слой визуализации данных в HTML-документы или JSON-документы

Вариант разделения частей для веб-приложения #

Можно предложить следующий подход к разделению небольшого приложения на части:

  • Описание предментной области, пакет .domain
    • Подсистема хранения данных, пакет .domain.storage или .domain.database
    • Подсистема изменения данных, пакет .domain.operations
  • Статические данные приложения, пакет .public
  • Логика обработки HTTP-запросов, пакет .web
    • Фильтры по обработке HTTP-запросов, пакет .web.filters
    • Обработчики HTTP-запросов, пакет .web.handlers
    • Шаблоны для отображения информации, пакет .web.models
    • Подсистема валидации входящих запросов от пользователя .web.validation

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

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

  • Веб-браузер
  • Клиентские приложения 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 — фильтры, помогающие в отладке работы приложения
  • 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 {
            response.body(renderer(ErrorMessage(request)))
        }
    }
}
val application = errorFilter.then(router)

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

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

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

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

  • Вручную обработать ситуацию наличия или отсутствия данных
  • Использовать специализированный механизм основной библиотеки
  • Реализовать собственную подсистему для обработки запросов

Ручная обработка переменных внутри обработчика #

Рассмотрим порядок обработки маршрута с переменной внутри. Стратегия решения проблемы — отображение сообщения, что искомый элемент не был найден

fun handler(
    renderer: TemplateRenderer,
    getEntityByData: (Int) -> Entity?,
): HttpHandler = handler@{ request ->
    val paramemter: String? = request.path("parameter")?.toIntOrNull()
    if (parameter == null) {
        return@handler Response(UNSATISFIABLE_PARAMETERS).body(
            renderer(EmptyParameterRequestFoundVM())
        )
    }
    val entity: Entity? = getEntityByData(parameter)
    if (entity == null) {
        return@handler Response(NOT_FOUND).body(
            renderer(EntityNotFoundVM())
        )
    }
    Response(OK).body(renderer(ShowEntityVM(entity)))
}

Или с помощью составления цепочки по обработке данных, которые могут стать null:

fun handler(
    renderer: Templaterenderer,
    getEntityByData: (Int) -> Entity?,
): HttpHandler = { request ->
    request
        .path("parameter")
        ?.let { parameter ->
            getEntityByParameter(parameter)
        }?.let { entity ->
            Response(OK).body(renderer(ShowEntityVM(entity)))
        } ?: Response(NOT_FOUND).body(renderer(EntityNotFoundVM()))
}

Или путём обработки данной ситуации на уровне шаблона Pebble

Разрешаем null-состояние для модели

data class ShowEntityVM(entity: Entity?)

Передаём неопределённое состояние в модель

fun handler(
    renderer: Templaterenderer,
    getEntityByData: (Int) -> Entity?,
): HttpHandler = { request ->
    val entity request
        .path("parameter")
        ?.let { parameter ->
           parameter.toIntOrNull()
        }?.let { number ->
            getEntityByParameter(parameter)
        }
    Response(OK).body(renderer(ShowEntityVM(entity)))
}

Проверка на null в шаблоне Pebble:

{% if model.entity is null %}
   ...
{% else %}
   ...
{% endif %}

Однако такой подход не следует активно использовать:

  • Шаблонизатор исполняет код медленнее компилированного кода приложения
  • Шаблонизатору приходится работать в рамках враждебного окружения, что значительно повышает сложность восприятия, следовательно повышается риск ошибок

В рамках курса такой подход запрещён

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