in ktor-server/ktor-server-core/jvm/src/io/ktor/features/ContentNegotiation.kt [103:181]
override fun install(
pipeline: ApplicationCallPipeline,
configure: Configuration.() -> Unit
): ContentNegotiation {
val configuration = Configuration().apply(configure)
val feature = ContentNegotiation(configuration.registrations, configuration.acceptContributors)
// Respond with "415 Unsupported Media Type" if content cannot be transformed on receive
pipeline.intercept(ApplicationCallPipeline.Features) {
try {
proceed()
} catch (e: UnsupportedMediaTypeException) {
call.respond(HttpStatusCode.UnsupportedMediaType)
}
}
pipeline.sendPipeline.intercept(ApplicationSendPipeline.Render) { subject ->
if (subject is OutgoingContent) return@intercept
val acceptHeaderContent = call.request.header(HttpHeaders.Accept)
val acceptHeader = try {
parseHeaderValue(acceptHeaderContent)
.map { ContentTypeWithQuality(ContentType.parse(it.value), it.quality) }
} catch (parseFailure: BadContentTypeFormatException) {
throw BadRequestException(
"Illegal Accept header format: $acceptHeaderContent",
parseFailure
)
}
val acceptItems = feature.acceptContributors.fold(acceptHeader) { acc, e ->
e(call, acc)
}.distinct().sortedByQuality()
val suitableConverters = if (acceptItems.isEmpty()) {
// all converters are suitable since client didn't indicate what it wants
feature.registrations
} else {
// select converters that match specified Accept header, in order of quality
acceptItems.flatMap { (contentType, _) ->
feature.registrations.filter { it.contentType.match(contentType) }
}.distinct()
}
// Pick the first one that can convert the subject successfully
val converted = suitableConverters.mapFirstNotNull {
it.converter.convertForSend(this, it.contentType, subject)
}
val rendered = converted?.let { transformDefaultContent(it) }
?: HttpStatusCodeContent(HttpStatusCode.NotAcceptable)
proceedWith(rendered)
}
pipeline.receivePipeline.intercept(ApplicationReceivePipeline.Transform) { receive ->
// skip if already transformed
if (subject.value !is ByteReadChannel) return@intercept
// skip if a byte channel has been requested so there is nothing to negotiate
if (subject.type == ByteReadChannel::class) return@intercept
val requestContentType = try {
call.request.contentType().withoutParameters()
} catch (parseFailure: BadContentTypeFormatException) {
throw BadRequestException(
"Illegal Content-Type header format: ${call.request.headers[HttpHeaders.ContentType]}",
parseFailure
)
}
val suitableConverter =
feature.registrations.firstOrNull { converter -> requestContentType.match(converter.contentType) }
?: throw UnsupportedMediaTypeException(requestContentType)
val converted = suitableConverter.converter.convertForReceive(this)
?: throw UnsupportedMediaTypeException(requestContentType)
proceedWith(ApplicationReceiveRequest(receive.typeInfo, converted, reusableValue = true))
}
return feature
}