🤪

Java/Kotlin の HTTP 系ライブラリで「?&key=value」がどう扱われるか検証してみた

に公開

TLDR:

// クエリのサイズが・・・
// => 2
Android Uri, OkHttp HttpUrl

// => 1
Apache HttpComponents URIBuilder, Spring UrlComponents

// ?
Ktor

Why?

ある日、適当に Ktor で遊んでいたところ、以下のような問題に遭遇しました。

@Test
fun testEmptyQuery() {
    val baseUrl = URLBuilder().apply {
        encodedPath = "/"
        parameters.appendAll("", emptyList())
        parameters.append("key", "value")
    }.build()
    val url = URLBuilder(baseUrl).build()

    // fail
    // expect: /?&key=value
    // actual: /?key=value
    assertEquals(baseUrl.fullPath, url.fullPath)
}

URL クラスの作成方法によってクエリーパラメータの扱いが変わるというものです。
これはプルリクエストを作るチャンスですね。さっそく修正してテストを走らせてみました。
・・・テストで落ちるようになりました。おっと?
厄介なことに、「空のクエリーパラメータを維持する」ことを期待するテストと「空のクエリーパラメータを破棄する」ことを期待するテストの2つが存在していたのです。

https://github.com/ktorio/ktor/blob/ec97064e584016623fc95a8351f235b9af7c776a/ktor-http/common/test/io/ktor/tests/http/QueryParametersTest.kt#L112-L116

https://github.com/ktorio/ktor/blob/ec97064e584016623fc95a8351f235b9af7c776a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/QueryTest.kt#L15-L28

両方維持することも出来なくはなさそうですが、片方に統一するのが健全でしょう。
というわけで「他のライブラリではどうなっているのか?」を検証してみました。

検証

検証コードはすごくシンプルです。

val url = TODO("それぞれのやり方で URL クラスを作成")
val params = TODO("〃クエリパラメータ作成")

println(params.size)

では検証していきます。

Android Uri

まず最初に思い付いたのがこれでした。

val url = Uri.parse("https://example.com?&key=value")
val params = url.queryParameterNames

println(params.size)
// => 2

はい、2です。
では次。

OkHttp HttpUrl

val url = HttpUrl.parse("https://example.com?&key=value")
val params = url.queryParameterNames

println(params.size)
// => 2

同じですね。現時点では残す側が優勢のようです。

Apache HttpComponents URIBuilder

val url = URIBuilder("https://example.com?&key=value")
val params = url.getQueryParams()

println(params.size)
// => 1

おっと、ここでついに1派が登場。

Spring UrlComponents

val url = UriComponentsBuilder.fromUriString("http://example.com?&key=value").build()
val params = url.queryParams

println(params.size)
// => 1

また1です。これで割れました。

結論

// クエリのサイズが・・・
// => 2
Android Uri, OkHttp HttpUrl

// => 1
Apache HttpComponents URIBuilder, Spring UrlComponents

// ?
Ktor

です。
どうしよう。どっちに合わせるべきなんだ。

おまけ

JavaScript の URLSearchParams を Chromium と Firefox で試してみました。

const params = new URLSearchParams("?&key=value")

console.log(params.size)
// => Chromium / Firefox のどちらも 1

どうしましょうねこれ。

Discussion