@@ -9,14 +9,24 @@ import io.ktor.client.plugins.*
99import io.ktor.client.request.*
1010import io.ktor.client.statement.*
1111import io.ktor.client.utils.*
12+ import io.ktor.client.utils.EmptyContent.contentType
1213import io.ktor.http.*
1314import io.ktor.http.content.*
1415import io.ktor.serialization.*
1516import io.ktor.util.*
17+ import io.ktor.util.reflect.*
1618import io.ktor.utils.io.*
1719import io.ktor.utils.io.charsets.*
1820import kotlin.reflect.*
1921
22+ internal val DefaultCommonIgnoredTypes : Set <KClass <* >> = setOf (
23+ ByteArray ::class ,
24+ String ::class ,
25+ HttpStatusCode ::class ,
26+ ByteReadChannel ::class ,
27+ OutgoingContent ::class
28+ )
29+
2030internal expect val DefaultIgnoredTypes : Set <KClass <* >>
2131
2232/* *
@@ -28,7 +38,8 @@ internal expect val DefaultIgnoredTypes: Set<KClass<*>>
2838 * You can learn more from [Content negotiation and serialization](https://ktor.io/docs/serialization-client.html).
2939 */
3040public class ContentNegotiation internal constructor(
31- internal val registrations : List <Config .ConverterRegistration >
41+ internal val registrations : List <Config .ConverterRegistration >,
42+ internal val ignoredTypes : Set <KClass <* >>
3243) {
3344
3445 /* *
@@ -39,9 +50,13 @@ public class ContentNegotiation internal constructor(
3950 internal class ConverterRegistration (
4051 val converter : ContentConverter ,
4152 val contentTypeToSend : ContentType ,
42- val contentTypeMatcher : ContentTypeMatcher ,
53+ val contentTypeMatcher : ContentTypeMatcher
4354 )
4455
56+ @PublishedApi
57+ internal val ignoredTypes: MutableSet <KClass <* >> =
58+ (DefaultIgnoredTypes + DefaultCommonIgnoredTypes ).toMutableSet()
59+
4560 internal val registrations = mutableListOf<ConverterRegistration >()
4661
4762 /* *
@@ -77,11 +92,84 @@ public class ContentNegotiation internal constructor(
7792 registrations.add(registration)
7893 }
7994
95+ /* *
96+ * Adds a type to the list of types that should be ignored by [ContentNegotiation].
97+ *
98+ * The list contains the [HttpStatusCode], [ByteArray], [String] and streaming types by default.
99+ */
100+ public inline fun <reified T > ignoreType () {
101+ ignoredTypes.add(T ::class )
102+ }
103+
104+ /* *
105+ * Remove [T] from the list of types that should be ignored by [ContentNegotiation].
106+ */
107+ public inline fun <reified T > removeIgnoredType () {
108+ ignoredTypes.remove(T ::class )
109+ }
110+
111+ /* *
112+ * Clear all configured ignored types including defaults.
113+ */
114+ public fun clearIgnoredTypes () {
115+ ignoredTypes.clear()
116+ }
117+
80118 private fun defaultMatcher (pattern : ContentType ): ContentTypeMatcher = object : ContentTypeMatcher {
81119 override fun contains (contentType : ContentType ): Boolean = contentType.match(pattern)
82120 }
83121 }
84122
123+ internal suspend fun convertRequest (request : HttpRequestBuilder , body : Any ): Any? {
124+ registrations.forEach { request.accept(it.contentTypeToSend) }
125+
126+ if (body is OutgoingContent || ignoredTypes.any { it.isInstance(body) }) return null
127+ val contentType = request.contentType() ? : return null
128+
129+ if (body is Unit ) {
130+ request.headers.remove(HttpHeaders .ContentType )
131+ return EmptyContent
132+ }
133+
134+ val matchingRegistrations = registrations.filter { it.contentTypeMatcher.contains(contentType) }
135+ .takeIf { it.isNotEmpty() } ? : return null
136+ if (request.bodyType == null ) return null
137+ request.headers.remove(HttpHeaders .ContentType )
138+
139+ // Pick the first one that can convert the subject successfully
140+ val serializedContent = matchingRegistrations.firstNotNullOfOrNull { registration ->
141+ registration.converter.serializeNullable(
142+ contentType,
143+ contentType.charset() ? : Charsets .UTF_8 ,
144+ request.bodyType!! ,
145+ body.takeIf { it != NullBody }
146+ )
147+ } ? : throw ContentConverterException (
148+ " Can't convert $body with contentType $contentType using converters " +
149+ matchingRegistrations.joinToString { it.converter.toString() }
150+ )
151+
152+ return serializedContent
153+ }
154+
155+ @OptIn(InternalAPI ::class )
156+ internal suspend fun convertResponse (
157+ info : TypeInfo ,
158+ body : Any ,
159+ responseContentType : ContentType ,
160+ charset : Charset = Charsets .UTF_8
161+ ): Any? {
162+ if (body !is ByteReadChannel ) return null
163+ if (info.type in ignoredTypes) return null
164+
165+ val suitableConverters = registrations
166+ .filter { it.contentTypeMatcher.contains(responseContentType) }
167+ .map { it.converter }
168+ .takeIf { it.isNotEmpty() } ? : return null
169+
170+ return suitableConverters.deserialize(body, info, charset)
171+ }
172+
85173 /* *
86174 * A companion object used to install a plugin.
87175 */
@@ -91,61 +179,22 @@ public class ContentNegotiation internal constructor(
91179
92180 override fun prepare (block : Config .() -> Unit ): ContentNegotiation {
93181 val config = Config ().apply (block)
94- return ContentNegotiation (config.registrations)
182+ return ContentNegotiation (config.registrations, config.ignoredTypes )
95183 }
96184
97185 override fun install (plugin : ContentNegotiation , scope : HttpClient ) {
98- scope.requestPipeline.intercept(HttpRequestPipeline .Transform ) { payload ->
99- val registrations = plugin.registrations
100- registrations.forEach { context.accept(it.contentTypeToSend) }
101-
102- if (subject is OutgoingContent || DefaultIgnoredTypes .any { it.isInstance(payload) }) {
103- return @intercept
104- }
105- val contentType = context.contentType() ? : return @intercept
106-
107- if (payload is Unit ) {
108- context.headers.remove(HttpHeaders .ContentType )
109- proceedWith(EmptyContent )
110- return @intercept
111- }
112-
113- val matchingRegistrations = registrations.filter { it.contentTypeMatcher.contains(contentType) }
114- .takeIf { it.isNotEmpty() } ? : return @intercept
115- if (context.bodyType == null ) return @intercept
116- context.headers.remove(HttpHeaders .ContentType )
117-
118- // Pick the first one that can convert the subject successfully
119- val serializedContent = matchingRegistrations.firstNotNullOfOrNull { registration ->
120- registration.converter.serializeNullable(
121- contentType,
122- contentType.charset() ? : Charsets .UTF_8 ,
123- context.bodyType!! ,
124- payload.takeIf { it != NullBody }
125- )
126- } ? : throw ContentConverterException (
127- " Can't convert $payload with contentType $contentType using converters " +
128- matchingRegistrations.joinToString { it.converter.toString() }
129- )
130-
131- proceedWith(serializedContent)
186+ scope.requestPipeline.intercept(HttpRequestPipeline .Transform ) {
187+ val result = plugin.convertRequest(context, subject) ? : return @intercept
188+ proceedWith(result)
132189 }
133190
134191 scope.responsePipeline.intercept(HttpResponsePipeline .Transform ) { (info, body) ->
135- if (body !is ByteReadChannel ) return @intercept
136- if (info.type == ByteReadChannel ::class ) return @intercept
137-
138192 val contentType = context.response.contentType() ? : return @intercept
139- val registrations = plugin.registrations
140- val suitableConverters = registrations
141- .filter { it.contentTypeMatcher.contains(contentType) }
142- .map { it.converter }
143- .takeIf { it.isNotEmpty() } ? : return @intercept
144-
145- @OptIn(InternalAPI ::class )
146- val parsedBody = suitableConverters.deserialize(body, info, context.request.headers.suitableCharset())
147- val response = HttpResponseContainer (info, parsedBody)
148- proceedWith(response)
193+ val charset = context.request.headers.suitableCharset()
194+
195+ val deserializedBody = plugin.convertResponse(info, body, contentType, charset) ? : return @intercept
196+ val result = HttpResponseContainer (info, deserializedBody)
197+ proceedWith(result)
149198 }
150199 }
151200 }
0 commit comments