Kotlin/NativeでHTTPサーバーを作るアドベントカレンダー(19日目:リクエストヘッダー)

この記事はkdnaktの1人 Advent Calendar 2020の19日目の記事です(1日遅れ)。

 

2020年は会社でKotlin dojoを主催して週1回30分Kotlinと戯れていました。12月はその集大成ということで、Kotlin/NativeでHTTPサーバーを作ってみたいと思います。どこまでできるか、お楽しみ……。

 

 

[POSTメソッドの実装に着手]

2日目にスコープを考えたときに、POSTメソッドも実装する計画でした。

kdnakt.hatenablog.com

 

POSTメソッドはHTTPリクエストに含まれるContent-Typeヘッダーを元に、リクエストボディの種類を決定する必要があります。

developer.mozilla.org

 

また、リクエストボディのサイズを知るためには、Content-Lengthヘッダーも取得できなくてはいけません。

 

[リクエストヘッダーをパースする]

厳密にリクエストヘッダーをパースしようとすると、RFC7230の以下の定義を思い出す必要があります。

header-field   = field-name ":" OWS field-value OWS

 

OWSはOptional whitespaceなので、半角スペースまたは水平タブが入る場合もあれば、省略されることもあります。

 

以上の内容をテストコードに書き出します。

@Test
fun shouldParseHeaderWithRightSize() {
    val reqByteArray = ("GET /index.html HTTP/1.1" + crlf
            + "Host: localhost:8080" + crlf
            + crlf).encodeToByteArray()
    val context = parser.parse(reqByteArray)
    assertEquals(1, context.headers.size)
}

@Test
fun shouldParseHeaderContentWithoutOWS() {
    val reqByteArray = ("GET /index.html HTTP/1.1" + crlf
            + "Host:localhost:8080" + crlf
            + crlf).encodeToByteArray()
    val context = parser.parse(reqByteArray)
    assertEquals("localhost:8080", context.headers["Host"])
}

@Test
fun shouldParseHeaderContentWithOWSAfterFieldValue() {
    val reqByteArray = ("GET /index.html HTTP/1.1" + crlf
            + "Host: localhost:8080\t" + crlf
            + crlf).encodeToByteArray()
    val context = parser.parse(reqByteArray)
    assertEquals("localhost:8080", context.headers["Host"])
}

 

まず、request.headersフィールドがないためコンパイルエラーになるので、RequestContext.ktにフィールドを追加します。

data class RequestContext(
        val method: HttpMethod,
        val requestTarget: String,
        val httpVersion: HttpVersion)
+ {
+     val headers = mutableMapOf<String, String>()
+ }

 

これでコンパイルエラーはなくなりました。テストを実行すると、追加したテストが失敗しています。

以下のように、リクエストヘッダーをパースする実装を追加します。

- return RequestContext(httpMethod,
+ val req = RequestContext(httpMethod,
        startLineElements[1],
        httpVersion
)
+ reqString.split("\r\n\r\n")[0]
+         .split("\r\n")
+         .let { it.slice(1 until it.size) }
+         .map { headerString ->
+             val indexOfFirstColon = headerString.indexOfFirst { it == ':' }
+             req.headers.put(
+                     headerString.substring(0 until indexOfFirstColon),
+                     headerString.substring(indexOfFirstColon + 1).trim())
+         }
+ return req

 

これでテストが無事通過しました。

本当はリクエストのサイズとかもうすこし気にして処理する必要がありそうですが、今日はこの辺で……。

 

[まとめ]

  • POSTメソッドを実装するために、まずはリクエストヘッダーをKotlinで扱えるようにした
  • 実装中のコードは以下のリポジトリにまとめてある

github.com