KtorのCORSがchromeのPreflight Requestに対してうまく働かなくてハマった問題
経緯
Kotlinを書いているうち - 適度にシュッとしている点 - Nullableをハンドリングできる点 - Collection系の関数があるところ - IntelliJとの相性抜群 などに惹かれたのでAPIでも書いてみようと軽そうなKtorを触ることに。
発見した問題
ChromeからのCORSのリクエスト、特にPreflight Requestが通らない。
Chromeのインスペクタから Copy as cURLしてやると、下記のリクエストで 403
が返ってくる
curl 'http://localhost:8080/sample' -X OPTIONS -H 'Access-Control-Request-Method: GET' -H 'Origin: http://localhost:8878' -H 'Accept-Encoding: gzip, deflate, br' -H 'Accept-Language: en,ja-JP;q=0.9,ja;q=0.8,en-GB;q=0.7,en-US;q=0.6' -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' -H 'Accept: */*' -H 'Connection: keep-alive' -H 'Access-Control-Request-Headers: authorization,crossdomain,x-csrf-token' --compressed
解決法
公式ドキュメントを読んでもそれらしき内容はないし、ISSUEもない、ということでなぜ 403
なのかをコードを追って確認することに。
CORS - Servers - Ktor
private suspend fun ApplicationCall.respondPreflight(origin: String) { if (!corsCheckRequestMethod() || !corsCheckRequestHeaders()) { respond(HttpStatusCode.Forbidden) // ここで 403が返ってるっぽい return } accessControlAllowOrigin(origin) accessControlAllowCredentials() response.header(HttpHeaders.AccessControlAllowMethods, methodsListHeaderValue) response.header(HttpHeaders.AccessControlAllowHeaders, headersListHeaderValue) accessControlMaxAge() respond(HttpStatusCode.OK) }
ので corsCheckRequestHeader
を追うことに
private fun ApplicationCall.corsCheckRequestHeaders(): Boolean { val requestHeaders = request.headers.getAll(HttpHeaders.AccessControlRequestHeaders)?.flatMap { it.split(",") }?.map { it.trim().toLowerCase() } ?: emptyList() return !requestHeaders.any { it !in headers } }
Access-Control-Request-Headers
で指定されたヘッダに許可していないものが入っていないか調べているらしい。Chromeから送られるリクエストは
'Access-Control-Request-Headers: authorization,crossdomain,x-csrf-token'
の3つのヘッダーを含んでいるが、これらはKtorのデフォルト設定だと許可されていない
class Configuration { companion object { val CorsDefaultMethods = setOf(HttpMethod.Get, HttpMethod.Post, HttpMethod.Head) // https://www.w3.org/TR/cors/#simple-header val CorsDefaultHeaders: Set<String> = TreeSet(String.CASE_INSENSITIVE_ORDER).apply { addAll(listOf( HttpHeaders.CacheControl, HttpHeaders.ContentLanguage, HttpHeaders.ContentType, HttpHeaders.Expires, HttpHeaders.LastModified, HttpHeaders.Pragma )) }
見た感じデフォルトで許可すべきヘッダはw3cの仕様で決まっている?(ここは追っていない とにかく、許可ヘッダを足す必要があるため、若干ウォークアラウンド感があるが下記のように設定を追加し事なきをえた
header("CrossDomain") header("X-CSRF-Token")
これがブラウザごとに異なる、とかだと結構設定が面倒なので、Ktor側でうまく吸収して欲しい...