Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KTOR-4849 Routing: Wrong content-type should result in 415 #3161

Merged
merged 1 commit into from Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand Down
Expand Up @@ -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)
}

/**
Expand Down
Expand Up @@ -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 {
Expand Down