From 4450eac56084112b3f7cd2ca5af7af4eb4e2f5ec Mon Sep 17 00:00:00 2001 From: rsinukov Date: Mon, 12 Sep 2022 13:57:11 +0200 Subject: [PATCH] KTOR-4849 Routing: Wrong content-type should result in 415 --- .../io/ktor/server/routing/RouteSelector.kt | 31 ++++++++++++-- .../io/ktor/server/routing/RoutingBuilder.kt | 3 +- .../server/routing/RoutingProcessingTest.kt | 42 +++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RouteSelector.kt b/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RouteSelector.kt index a07240d623..77d7b963f4 100644 --- a/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RouteSelector.kt +++ b/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RouteSelector.kt @@ -7,7 +7,6 @@ package io.ktor.server.routing import io.ktor.http.* import io.ktor.server.plugins.* import io.ktor.server.request.* -import io.ktor.util.* /** * A result of a route evaluation against a call. @@ -552,8 +551,8 @@ public data class HttpMethodRouteSelector( /** * Evaluates a route against a header in the request. - * @param name is a name of the header - * @param value is a value of the header + * @param name is the name of the header + * @param value is the value of the header */ public data class HttpHeaderRouteSelector( val name: String, @@ -572,6 +571,32 @@ public data class HttpHeaderRouteSelector( override fun toString(): String = "(header:$name = $value)" } +/** + * Evaluates a route against a `Content-Type` in the [HttpHeaders.ContentType] request header. + * @param contentType is an instance of [ContentType] + */ +internal data class ContentTypeHeaderRouteSelector( + val contentType: ContentType +) : RouteSelector() { + + private val failedEvaluation = RouteSelectorEvaluation.Failure( + RouteSelectorEvaluation.qualityFailedParameter, + HttpStatusCode.UnsupportedMediaType + ) + + override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation { + val headers = context.call.request.header(HttpHeaders.ContentType) + val parsedHeaders = parseAndSortContentTypeHeader(headers) + + val header = parsedHeaders.firstOrNull { ContentType.parse(it.value).match(contentType) } + ?: return failedEvaluation + + return RouteSelectorEvaluation.Success(header.quality) + } + + override fun toString(): String = "(contentType = $contentType)" +} + /** * Evaluates a route against a `Content-Type` in the [HttpHeaders.Accept] request header. * @param contentType is an instance of [ContentType] diff --git a/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RoutingBuilder.kt b/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RoutingBuilder.kt index a5bcda2e02..ea0e1fb584 100644 --- a/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RoutingBuilder.kt +++ b/ktor-server/ktor-server-core/jvmAndNix/src/io/ktor/server/routing/RoutingBuilder.kt @@ -96,7 +96,8 @@ public fun Route.accept(contentType: ContentType, build: Route.() -> Unit): Rout */ @KtorDsl public fun Route.contentType(contentType: ContentType, build: Route.() -> Unit): Route { - return header(HttpHeaders.ContentType, "${contentType.contentType}/${contentType.contentSubtype}", build) + val selector = ContentTypeHeaderRouteSelector(contentType) + return createChild(selector).apply(build) } /** diff --git a/ktor-server/ktor-server-tests/jvmAndNix/test/io/ktor/tests/server/routing/RoutingProcessingTest.kt b/ktor-server/ktor-server-tests/jvmAndNix/test/io/ktor/tests/server/routing/RoutingProcessingTest.kt index 01edfd1289..c6199a13e0 100644 --- a/ktor-server/ktor-server-tests/jvmAndNix/test/io/ktor/tests/server/routing/RoutingProcessingTest.kt +++ b/ktor-server/ktor-server-tests/jvmAndNix/test/io/ktor/tests/server/routing/RoutingProcessingTest.kt @@ -510,6 +510,48 @@ class RoutingProcessingTest { } } + @Test + fun testContentTypeHeaderProcessing() = testApplication { + routing { + route("/") { + contentType(ContentType.Text.Plain) { + handle { + call.respond("OK") + } + } + contentType(ContentType.Application.Any) { + handle { + call.respondText("{\"status\": \"OK\"}", ContentType.Application.Json) + } + } + } + } + + client.get("/") { + header(HttpHeaders.ContentType, "text/plain") + }.let { + assertEquals("OK", it.bodyAsText()) + } + + client.get("/") { + header(HttpHeaders.ContentType, "application/json") + }.let { + assertEquals("{\"status\": \"OK\"}", it.bodyAsText()) + } + + client.get("/") { + header(HttpHeaders.ContentType, "application/pdf") + }.let { + assertEquals("{\"status\": \"OK\"}", it.bodyAsText()) + } + + client.get("/") { + header(HttpHeaders.ContentType, "text/html") + }.let { + assertEquals(HttpStatusCode.UnsupportedMediaType, it.status) + } + } + @Test fun testTransparentSelectorWithHandler() = withTestApplication { application.routing {