FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

KotlinのWebフレームワークKtorでRoutingに前処理を追加する方法

はじめに

ピクトリンク事業部でSREエンジニアをしている山根です。
この記事では、WebフレームワークKtorでRoutingに対して前処理を追加する方法について説明します。 ktor.io

Ktorとは

Ktorとは、Kotlinで書かれた非同期Webフレームワークです。
Ktorを使用すると、簡単にWebアプリケーションやAPIを構築できます。
以下にKtorの主な特徴を示します。

  • 非同期処理: コルーチンを使用して非同期処理を簡単に実装できます。
  • 軽量: 必要な機能だけを選んで使用できるため、軽量なアプリケーションを構築できます。
  • 柔軟なルーティング: DSLを使用して直感的にルーティングを定義できます。
  • 拡張性: プラグインを使用して機能を簡単に拡張できます。

詳細については、公式ドキュメントを参照してください。

PipelineとPhase

KtorにおけるPipelineとPhaseは、リクエスト処理の流れを管理するための重要な概念です。
また、Pluginを利用することで、Pipelineに処理を追加できます。
まとめると以下のような感じです。

  • Pipeline
    リクエストやレスポンスの処理を段階的に行うための仕組みです。各段階(Phase)で特定の処理を行い、次の段階に進むことができます。
  • Phase
    Pipelineの中の特定の処理段階を表します。KtorにはデフォルトでいくつかのPhaseが定義されていますが、独自のPhaseを追加することも可能です。
  • Plugin
    Pipelineに処理を追加するための仕組みです。Pluginを利用することで、Pipelineに処理を追加できます。

Routingの前処理(Plugin)を実装する

Ktorでは、Pipelineを利用してRoutingの前処理(Plugin)を実装できます。
例えば、リクエストヘッダーのチェック処理や、認証処理などをPipelineに登録することで、Routingの前処理を実装することができます。

以下は、PipelineにRoutingの前処理を実装する例です。

基底インターフェースの定義

まず、Routingの前処理の基底となるインターフェースを定義します。

import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.util.pipeline.*

/**
 * interceptorのPluginの基本設定を定義するインターフェース
 */
internal interface BaseInterceptorPluginConfig {
    // Plugin名
    val name: String

    // Pluginの処理
    suspend fun execute(call: ApplicationCall)
}

/**
 * Callのフェーズ前でHookするための設定
 */
internal object InterceptorHook : Hook<suspend (ApplicationCall) -> Unit> {
    private val InterceptorPhase: PipelinePhase = PipelinePhase("Interceptor")

    override fun install(
        pipeline: ApplicationCallPipeline,
        handler: suspend (ApplicationCall) -> Unit
    ) {
        pipeline.insertPhaseBefore(ApplicationCallPipeline.Call, InterceptorPhase)
        pipeline.intercept(InterceptorPhase) { handler(call) }
    }
}

/**
 * InterceptorHookが発火した時に、
 * 指定の処理を実行するPluginを生成する
 */
internal fun createInterceptorPlugin(pluginConfig: BaseInterceptorPluginConfig) = createRouteScopedPlugin(
    name = pluginConfig.name,
) {
    on(InterceptorHook) { call ->
        pluginConfig.execute(call)
    }
}

/**
 * 複数のinterceptorのPluginを対象のRoute設定に対して登録する
 *
 * @param interceptorPlugins 登録するinterceptorのPluginの可変長引数
 * @param build Route設定
 * @return Pluginの登録が完了したRoute
 */
fun Route.interceptors(vararg interceptorPlugins: RouteScopedPlugin<*>, build: Route.() -> Unit): Route {
    interceptorPlugins.map {
        install(it){}
    }

    this.build()
    return this
}

ヘッダーをチェックするInterceptorの実装

次に、リクエストヘッダーのチェックを行うInterceptorを実装します。

import io.ktor.http.HttpHeaders.UserAgent
import io.ktor.server.application.*

/**
 * ヘッダーのチェックを行うInterceptor
 */
val headerCheckInterceptor = createInterceptorPlugin(HeaderCheckInterceptorPluginConfig())

private class HeaderCheckInterceptorPluginConfig : BaseInterceptorPluginConfig {
    override val name: String = "headerCheckInterceptorPlugin"

    override suspend fun execute(call: ApplicationCall) {
        // User-Agentが存在しない場合は例外を投げる
        call.request.headers.get(UserAgent) ?: throw IllegalArgumentException("User-Agent is not found")
    }
}

RoutingにInterceptorを設定

最後に、実装したInterceptorをRoutingに設定します。

import com.example.plugins.headerCheckInterceptor
import com.example.plugins.interceptors
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.configureRouting() {
    routing {
        get("/") {
            call.respondText("Hello World!")
        }

        // ヘッダーのチェックを行うInterceptorを設定
        interceptors(headerCheckInterceptor) {
            get("/checked") {
                call.respondText("Checked Headers.")
            }
        }
    }
}

上記の例では、/checkedエンドポイントにアクセスする際に、ヘッダーのチェックを行うInterceptorを設定しています。
このようにすることで、Routingの前処理をPipelineを利用して実装できます。

まとめ

KtorのPipelineに処理を追加する実装をご紹介しました。
Pipelineを利用することで、リクエスト処理の流れを柔軟に制御することができたり、認証処理をカスタマイズすることができます。
是非、KtorのPipelineを利用して、アプリケーションの処理をカスタマイズしてみてください。

おまけ: リクエストとレスポンスの処理を行うPluginの例

リクエストを受けた時、レスポンスを返す時に処理をいれるPluginの例をご紹介します。
以下は、nginxやapacheなどのリバースプロキシを経由してリクエストが来た場合に、X-Request-Idヘッダーの値をログに出力するPluginを実装します。

import io.ktor.server.application.*
import org.slf4j.MDC

/**
 * nginx や apache などのリバースプロキシを経由してリクエストが来た場合に、
 * X-Request-Id ヘッダーの値をログに出力するPlugin
 */
val RequestIdPlugin = createApplicationPlugin(name = "RequestIdPlugin") {
    val key = "requestId"

    // リクエストが来たら、X-Request-Id ヘッダーの値を MDC に設定する
    onCallReceive { call ->
        val value = call.request.headers["X-Request-Id"]
        MDC.put(key, value)
    }

    // レスポンスを返す前に MDC から X-Request-Id を削除する
    onCallRespond { _ ->
        MDC.remove(key)
    }
}

このPluginを有効にするには以下のように、installを使って設定します。

import com.example.plugins.RequestIdPlugin
import io.ktor.server.application.*

fun main(args: Array<String>) {
    io.ktor.server.netty.EngineMain.main(args)
}

fun Application.module() {
    // RequestIdPluginを登録
    install(RequestIdPlugin)

    configureRouting()
}

Application.moduleを適用するために、application.yamlに以下のように設定します。

ktor:
    application:
        modules:
            - com.example.ApplicationKt.module

このように、簡単にPipelineを利用してPhaseに対して前処理や後処理を実装することができます。