HTTPリクエストのハンドリング

Estimated reading time: 4 minutes

ルーティングを行うときや直接パイプラインをインターセプトするときには、ApplicationCallから実行時のコンテキスト情報を取得します。 callrequestというプロパティを持っており、これはリクエストに関する情報を保持しています。

また、call自体もリクエストに関係するいくつかの有益なプロパティやメソッドを持っています。

目次:

イントロダクション

Routing機能を利用するときや、リクエストのインターセプトを行うときには、 ルートハンドラ内部でcallプロパティにアクセスできます。 callはリクエストに関係する情報をもったrequestプロパティを保持します。

routing {
    get("/") {
        val uri = call.request.uri
        call.respondText("Request uri: $uri")
    } 
}

intercept(ApplicationCallPipeline.Call) { 
    if (call.request.uri == "/") {
        call.respondText("Test String")
    }
}

リクエスト情報

requestの一部として、内部のコンテキスト情報にアクセスできます。

val call: ApplicationCall = request.call
val pipeline: ApplicationReceivePipeline = request.pipeline

URL, メソッド, スキーマ, プロトコル, ホスト, パス, HTTPバージョン, remoteホスト, クライアントIP

val version: String = request.httpVersion // "HTTP/1.1"
val httpMethod: HttpMethod = request.httpMethod // GET, POST... 
val uri: String = request.uri // Short cut for `origin.uri`
val scheme: String = request.origin.scheme // "http" or "https"
val host: String? = request.host() // The host part without the port 
val port: Int = request.port() // Port of request
val path: String = request.path() // The uri without the query string
val document: String = request.document() // The last component after '/' of the uri
val remoteHost: String = request.origin.remoteHost // The IP address of the client doing the request

リバースプロキシのサポート: originlocal

(例えばNginxやロードバランサなどがあることで)リバースプロキシの背後にあるとき、受け取ったリクエストはエンドユーザからでなくリバースプロキシからのものになります。 つまり、接続のクライアントIPアドレスがクライアントの代わりにプロキシのものになるということです。 また、リバースプロキシはHTTPSでレスポンスを返す一方、あなたのサーバへはHTTP経由でリクエストします。 多くのリバースプロキシはX-Forwarded-ヘッダーをこの情報にアクセスするために送信します。

リバースプロキシ配下で配下で動作するためには、XForwardedHeaderSupport Featureをインストールする必要がある点にご注意ください。

リクエストオブジェクトの一部として、localoriginという2つのプロパティがあり、それを使うことでオリジナルのリクエストやlocal・プロキシのリクエストの情報を取得できます。

val local : RequestConnectionPoint = request.local // Local information 
val origin: RequestConnectionPoint = request.origin // Local / Origin if XForwardedHeaderSupport feature is installed.

以下があなたが取得可能なlocal/origin情報です。

interface RequestConnectionPoint {
    val scheme: String // "http" or "https": The provided protocol (local) or `X-Forwarded-Proto`
    val version: String // "HTTP/1.1"
    val port: Int
    val host: String // The provided host (local) or `X-Forwarded-Host`
    val uri: String
    val method: HttpMethod
    val remoteHost: String // The client IP (the direct ip for `local`, or the redirected one `X-Forwarded-For`)
}

GET / クエリパラメータ

クエリパラメータ?param1=value&param2=valueをコレクションとしてアクセスする必要が出たときには、queryParametersが利用できます。 StringValuesインターフェースを実装しており、各キーがそれに紐づくStringのリストを保持する構造になっています。

val queryParameters: Parameters = request.queryParameters
val param1: String? = request.queryParameters["param1"] // To access a single parameter (first one if repeated)
val repeatedParam: List<String>? = request.queryParameters.getAll("repeatedParam") // Multiple values

加工されていない生のqueryString(param1=value&param2=value)にもアクセス可能です。

val queryString: String = request.queryString()

POST, PUT, PATCH

POST, PUT, PATCH リクエストはリクエストボディ(payload)を持ちます。 payloadは通常エンコードされています。

これらのメソッドはクライアントから送信されたpayload全体を読み込み、リクエストボディを2度読み込むとRequestAlreadyConsumedExceptionエラーが発生します。 (DoubleReceive機能がインストールされていない限り)

Raw payload

payloadの生のビット列にアクセスするためには、receiveChannelが使えます。 call.requestではなくcallの一部として直接アクセスすることになります。

val channel: ByteReadChannel = call.receiveChannel()

他にも汎用的な型のための便利なメソッドが提供されています。

val channel: ByteReadChannel = call.receiveChannel()
val text: String = call.receiveText()
val inputStream: InputStream = call.receiveStream() // NOTE: InputStream is synchronous and blocks the thread
val multipart: MultiPartData = call.receiveMultipart()

これらreceiveメソッド群はcall.receive<T>メソッドに対する特定の型の場合のエイリアスです。 ByteReadChannel, ByteArray, InputStream, MultiPartData, String,Parametersの型はデフォルトでインストールされているApplicationReceivePipeline.installDefaultTransformationsによってハンドルされます。

Formパラメータ (urlencodedまたはmultipart)

Form URLencodeまたはmultipartをパースするためにはreceiveParametersreceive<Parameters>が使えます。

val postParameters: Parameters = call.receiveParameters()

型オブジェクト、Content-Type、JSONの受け取り

callはジェネリクスの受け取りもサポートしています。

val obj: T = call.receive<T>()
val obj: T? = call.receiveOrNull<T>()

payloadからカスタムのオブジェクトを受け取るためには、 ContentNegotiation機能を使う必要があります。 以下はREST APIにおけるJSON payloadの受信、送信の良い例です。

install(ContentNegotiation) {
    gson {
        setDateFormat(DateFormat.LONG)
        setPrettyPrinting()
    }
}

ContentNegotiationをgsonを使うように設定した場合、ktor-gsonアーティファクトをインクルードする必要があります。

compile("io.ktor:ktor-gson:$ktor_version")

そして以下の例のように受信ができます。

data class HelloWorld(val hello: String)

routing {
    post("/route") {
        val helloWorld = call.receive<HelloWorld>()
    }
}

Gsonに認識されるためクラスはトップレベル(任意のclassや関数の外側で)で定義されている必要があることを忘れてはいけません。

Multipart, ファイル, アップロード

アップロードセクションをご覧ください。

カスタムreceiveトランスフォーマー

application.receivePipeline.intercept(ApplicationReceivePipeline.Transform) { query ->を呼ぶことで、 カスタムのトランスフォーマーを作成することができ、 proceedWith(ApplicationReceiveRequest(query.type, transformed))を呼び出すことで ContentNegotiation機能が実行されます

Cookie

クライアントから送信されたCookieヘッダーにアクセスするためのcookiesプロパティというものがあり、コレクションのように動作します。

val cookies: RequestCookies = request.cookies
val mycookie: String? = request.cookies["mycookie"]

cookieを使うことでセッションのハンドルをする場合は、セッション機能をご覧ください。

ヘッダー

ヘッダーにアクセスするため、リクエストオブジェクトはheaders: Headersプロパティを持っています。 StringValuesインターフェースを実装しており、各キーは対応する値としてStringのリストを保持しています。

val headers: Headers = request.headers
val header: String? = request.header("HeaderName") // To access a single header (first one if repeated)
val repeatedHeader: List<String>? = request.headers.getAll("HeaderName") // Multiple values

共通的なヘッダーにアクセスするためにいくつかの便利なメソッドがあります。

val contentType: ContentType = request.contentType() // Parsed Content-Tpe 
val contentCharset: Charset? = request.contentCharset() // Content-Type JVM charset
val authorization: String? = request.authorization() // Authorization header
val location: String? = request.location() // Location header
val accept: String? = request.accept() // Accept header
val acceptItems: List<HeaderValue> = request.acceptItems() // Parsed items of Accept header
val acceptEncoding: String? = request.acceptEncoding() // Accept-Encoding header
val acceptEncodingItems: List<HeaderValue> = request.acceptEncodingItems() // Parsed Accept-Encoding items 
val acceptLanguage: String? = request.acceptLanguage() // Accept-Language header
val acceptLanguageItems: List<HeaderValue> = request.acceptLanguageItems() // Parsed Accept-Language items
val acceptCharset: String? = request.acceptCharset() // Accept-Charset header
val acceptCharsetItems: List<HeaderValue> = request.acceptCharsetItems() // Parsed Accept-Charset items
val userAgent: String? = request.userAgent() // User-Agent header
val cacheControl: String? = request.cacheControl() // Cache-Control header
val ranges: RangesSpecifier? = request.ranges() // Parsed Ranges header

val isChunked: Boolean = request.isChunked() // Transfer-Encoding: chunked
val isMultipart: Boolean = request.isMultipart() // Content-Type matches Multipart