Фильтры в http4k #
Васильев Андрей Михайлович, 2024
Версии презентации
Фильтры в http4k #
fun interface Filter : (HttpHandler) -> HttpHandler
Фильтры позволяют выполнить действия, общие для ряда запросов от пользователя:
- Проверить аутентификацию пользователя
- Выполнить авторизацию действия пользователя
- Выполнить обработку внутренних ошибок сервера
- Выполнить обработку ошибочных запросов от пользователя
- Выполнить журналирование запросов
- Настроить общие политики для предоставления содержимого, установка CORS-заголовков, Content-Type-заголовков
- Выполнить кеширование ответов на запросы
Процесс обработки запроса с фильтрами #
- С помощью фильтров определяются действия, которые могут выполняться до и после работы HTTP-обработчика
- В любой стадии фильтр может самостоятельно сформировать ответ пользователю
- Фильтр может применяться к одиночному HTTP-обработчику
- Фильтр может применяться ко всему приложению
Реализация собственного фильтра #
Интерфейс фильтра (Filter) принимает в качестве аргумента объект HttpHandler и
должен вернуть объект HttpHandler: interface Filter : (HttpHandler) -> HttpHandler
val appHandler: HttpHandler = ... // HTTP-обработчик, который надо обернуть
val timingFilter = Filter { // Создаём фильтр
next: HttpHandler -> { // HTTP-обработчик, который оборачиваем
request: Request -> // Код нового HTTP-обработчика
val start = System.currentTimeMillis() // Код до
val response = next(request) // Вызов обработчика
val latency = System.currentTimeMillis() - start // Код после
println("Задержка составила $latency мс")
response // Возврат ответа клиенту
}
}
// Создаём цепочку из стандартного фильтра
val latencyAndBasicAuth: Filter = ServerFilters.BasicAuth(
"Моё окружение", "user", "password")
.then(timingFilter) // А после добавляем наш фильтр
val app: HttpHandler = latencyAndBasicAuth.then(appHandler)
Фильтр в примере выполняет измерение скорости выполнения кода HTTP-обработчиков
Реализация фильтров с помощью классов #
При реализации функциональных интерфейсов с помощью классов потребуется создать два класса:
- Один необходимо унаследовать от интерфейса
org.http4k.core.Filter
- Второй класс будет реализовывать интерфейс HttpHandler, его конструктор будет принимать ссылку на оборачиваемый HTTP-обработчик
class CounterFilter : Filter {
override fun invoke(next: HttpHandler): HttpHandler =
CounterHandler(next)
}
class CounterHandler(private val next: HttpHandler) : HttpHandler {
override fun invoke(request: Request): Response {
val start = System.currentTimeMillis()
val response = next(request)
val latency = System.currentTimeMillis() - start
println("I took $latency ms")
return response
}
}
Стандартные фильтры http4k #
Пакет org.http4k.filter содержит описание фильтров, поставляемых с помощью http4k
- DebuggingFilters — фильтры, помогающие в отладке работы приложения
- PrintRequest — отображать запрос
- PrintRequest — отображать ответ
- PrintRequestAndResponse — отображать и запрос и ответ
- ServerFilters — основные серверные фильтры
- BasicAuth — реализация базовой HTTP-авторизации
- CatchAll — перехватывание всех внутренних исключений, возникающих при обработке сообщений
- Cors — установка CORS-заголовков
- HandleRemoteRequestFailed — обработка ошибок от HTTP-запросов к внешним ресурсам
- RequestTracing — структурированное отслеживание запросов в формате Zipkin
Добавление заголовка content-type #
Если в каждом обработчике HTTP-запроса в качестве ответа высылается JSON-документ, то в каждом ответе необходимо установить заголовок в ответе
Можно уменьшить дублирование, перенеся данную логику в фильтр
val jsonContentTypeFilter = Filter { next: HttpHandler ->
{ request ->
val response = next(request)
if (response.body.hasContentToRead()) { // Если есть содержимое
response.contentType(ContentType.APPLICATION_JSON)
} else {
response
}
}
}
- При наличии содержимого в ответе добавляется заголовок с указанием типа данных
- Данный фильтр можно применить ко всему приложению
Обработка ошибок линз #
При работе с приложением пользователь может передать некорректные данные в качестве параметров запроса или в его теле
Если обработка ситуации должна быть единой для всех обработчиков HTTP-запроса, то логично делегировать эту задачу единому фильтру
При получении данных с помощью линзы в случае ошибки будет выброшено исключение LensFailure, обработку которого можно делегировать фильтру
fun lensFailureFilter(): Filter =
Filter { next: HttpHandler ->
{ request: Request ->
try {
next(request)
} catch (lensFailure: LensFailure) {
Response(Status.BAD_REQUEST)
.body("{\"error\":\"${lensFailure.message}\"}")
}
}
}
Теперь в HTTP-обработчиках можно применять линзу для извлечения данных, в случае возникновения ошибки будет выброшено исключение
Цепочка из фильтров #
Фильтры поддерживают выстраивание их в цепочку, для связывания фильтров
используется функция then
Рассмотрим следующую функцию по созданию именованных фильтров
fun namedFilter(name: String): Filter = Filter { next: HttpHandler -> {
println(name)
next(it)
}
}
Свяжем данные фильтры с HTTP-обработчиком и обратимся к нему:
namedFilter("первый").then(namedFilter("второй")).then(pingHandler())
Вывод приложения:
первый
второй
Функция then
также связывает фильтры с HTTP-обработчиком