From da1d7d4873be0d79c4a84a2363a8e7cf5ca3f47d Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Thu, 7 Nov 2024 17:35:45 -0500 Subject: [PATCH 1/9] Accept header only if not already specified --- .../plugins/contentnegotiation/ContentNegotiation.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt index de812bd1ea9..1330678b6fd 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt @@ -141,10 +141,11 @@ public val ContentNegotiation: ClientPlugin = createCl suspend fun convertRequest(request: HttpRequestBuilder, body: Any): OutgoingContent? { registrations.forEach { - LOGGER.trace("Adding Accept=${it.contentTypeToSend.contentType} header for ${request.url}") - - if (request.headers.contains(HttpHeaders.Accept, it.contentTypeToSend.toString())) return@forEach - request.accept(it.contentTypeToSend) + val acceptHeaders = request.headers.getAll(HttpHeaders.Accept).orEmpty() + if (acceptHeaders.none { h -> ContentType.parse(h).match(it.contentTypeToSend) }) { + LOGGER.trace("Adding Accept=${it.contentTypeToSend.contentType} header with q=0.8 for ${request.url}") + request.accept(it.contentTypeToSend) + } } if (body is OutgoingContent || ignoredTypes.any { it.isInstance(body) }) { From aad007252e8e63713487d1f95ce2495617c997d3 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Thu, 7 Nov 2024 17:56:53 -0500 Subject: [PATCH 2/9] Allow specifying default accept header q-value --- .../contentnegotiation/ContentNegotiation.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt index 1330678b6fd..62b4e24a9af 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt @@ -46,6 +46,12 @@ public class ContentNegotiationConfig : Configuration { internal val registrations = mutableListOf() + /** + * By default, `Accept` headers for registered content types will have no q value (implicit 1.0). Set this to + * change that behavior. This is useful to override the preferred `Accept` content types on a per-request basis. + */ + public var defaultAcceptHeaderQValue: String? = null + /** * Registers a [contentType] to a specified [converter] with an optional [configuration] script for a converter. */ @@ -144,7 +150,13 @@ public val ContentNegotiation: ClientPlugin = createCl val acceptHeaders = request.headers.getAll(HttpHeaders.Accept).orEmpty() if (acceptHeaders.none { h -> ContentType.parse(h).match(it.contentTypeToSend) }) { LOGGER.trace("Adding Accept=${it.contentTypeToSend.contentType} header with q=0.8 for ${request.url}") - request.accept(it.contentTypeToSend) + // automatically added headers get a lower content type priority, so user-specified accept headers + // with higher q or implicit q=1 will take precedence + val contentTypeToSend = when (val qValue = pluginConfig.defaultAcceptHeaderQValue) { + null -> it.contentTypeToSend + else -> it.contentTypeToSend.withParameter("q", qValue) + } + request.accept(contentTypeToSend) } } From cf1cc78f67a21176be1a9c46bdb72e41b7c26d22 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Thu, 7 Nov 2024 17:57:21 -0500 Subject: [PATCH 3/9] Client content negotiation: test new features --- .../client/plugins/ContentNegotiationTests.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt index 8812f098753..655b215681d 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt @@ -71,6 +71,58 @@ class ContentNegotiationTests { } } + @Test + fun addAcceptHeadersWithDefaultQValue() { + testWithEngine(MockEngine) { + val registeredTypesToSend = listOf( + ContentType("testing", "a"), + ContentType("testing", "b"), + ContentType("testing", "c") + ) + + setupWithContentNegotiation { + for (typeToSend in registeredTypesToSend) { + register(typeToSend, TestContentConverter()) + defaultAcceptHeaderQValue = "0.8" + } + } + + test { client -> + client.get("https://test.com/").apply { + val sentTypes = assertNotNull(call.request.headers.getAll(HttpHeaders.Accept)) + .map { ContentType.parse(it) } + + // Order NOT tested + for (typeToSend in registeredTypesToSend) { + assertContains(sentTypes, typeToSend.withParameter("q", "0.8")) + } + } + } + } + } + + @Test + fun skipAddAcceptHeadersWithMatchingContentType() { + testWithEngine(MockEngine) { + setupWithContentNegotiation { + register(ContentType("testing", "a"), TestContentConverter()) + } + + test { client -> + client.get("https://test.com/") { + // our explicitly specified lower q-value should take precedence + accept(ContentType("testing", "a", listOf(HeaderValueParam("q", "0.5")))) + }.apply { + val sentTypes = assertNotNull(call.request.headers.getAll(HttpHeaders.Accept)) + .map { ContentType.parse(it) } + + assertContains(sentTypes, ContentType("testing", "a", listOf(HeaderValueParam("q", "0.5")))) + assertEquals(1, sentTypes.size) + } + } + } + } + @Test fun testKeepsContentType() { testWithEngine(MockEngine) { From 40c0095346c4dcf37982e4bee1bc46bd488de565 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Thu, 14 Nov 2024 18:01:14 -0500 Subject: [PATCH 4/9] Client content negotiation: exclude accept types per request Allow accept types to be excluded per request. --- .../contentnegotiation/ContentNegotiation.kt | 26 ++++- .../client/plugins/ContentNegotiationTests.kt | 108 ++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt index 62b4e24a9af..94090369e6d 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt @@ -11,6 +11,7 @@ import io.ktor.client.utils.* import io.ktor.http.* import io.ktor.http.content.* import io.ktor.serialization.* +import io.ktor.util.AttributeKey import io.ktor.util.logging.* import io.ktor.util.reflect.* import io.ktor.utils.io.* @@ -29,6 +30,11 @@ internal val DefaultCommonIgnoredTypes: Set> = setOf( internal expect val DefaultIgnoredTypes: Set> +/** + * Shows that request should skip auth and refresh token procedure. + */ +public val ExcludeContentTypes: AttributeKey> = AttributeKey("exclude-content-types") + /** * A [ContentNegotiation] configuration that is used during installation. */ @@ -146,7 +152,14 @@ public val ContentNegotiation: ClientPlugin = createCl val ignoredTypes: Set> = pluginConfig.ignoredTypes suspend fun convertRequest(request: HttpRequestBuilder, body: Any): OutgoingContent? { - registrations.forEach { + val requestRegistrations = if (request.attributes.contains(ExcludeContentTypes)) { + val excluded = request.attributes[ExcludeContentTypes] + registrations.filter { registration -> excluded.none { registration.contentTypeToSend.match(it) } } + } else { + registrations + } + + requestRegistrations.forEach { val acceptHeaders = request.headers.getAll(HttpHeaders.Accept).orEmpty() if (acceptHeaders.none { h -> ContentType.parse(h).match(it.contentTypeToSend) }) { LOGGER.trace("Adding Accept=${it.contentTypeToSend.contentType} header with q=0.8 for ${request.url}") @@ -264,3 +277,14 @@ public val ContentNegotiation: ClientPlugin = createCl } public class ContentConverterException(message: String) : Exception(message) + +/** + * Excludes the given [ContentType] from the list of types that will be sent in the `Accept` header by + * the [ContentNegotiation] plugin. Can be used to not accept specific types for particular requests. + * This can be called multiple times to exclude multiple content types, or multiple content types can + * be passed in a single call. + */ +public fun HttpRequestBuilder.exclude(vararg contentType: ContentType) { + val excludedContentTypes = attributes.getOrNull(ExcludeContentTypes).orEmpty() + attributes.put(ExcludeContentTypes, excludedContentTypes + contentType) +} diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt index 655b215681d..450ea837f27 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt @@ -71,6 +71,114 @@ class ContentNegotiationTests { } } + @Test + fun addAcceptHeadersWithSingleExclusion() { + testWithEngine(MockEngine) { + val registeredTypesToSend = listOf( + ContentType("testing", "a"), + ContentType("testing", "b"), + ContentType("testing", "c") + ) + + setupWithContentNegotiation { + for (typeToSend in registeredTypesToSend) { + register(typeToSend, TestContentConverter()) + } + } + + test { client -> + client.get("https://test.com/") { + exclude(ContentType("testing", "b")) + }.apply { + val sentTypes = assertNotNull(call.request.headers.getAll(HttpHeaders.Accept)) + .map { ContentType.parse(it) } + + // Order NOT tested + for (typeToSend in registeredTypesToSend.filter { it.contentSubtype != "b" }) { + assertContains(sentTypes, typeToSend) + } + assertNull(sentTypes.firstOrNull { it.contentSubtype == "b" }) + } + } + } + } + + @Test + fun addAcceptHeadersWithExclusionMatchingParameterizedType() { + testWithEngine(MockEngine) { + val registeredTypesToSend = listOf( + ContentType("testing", "a").withParameter("foo", "bar"), + ContentType("testing", "b").withParameter("foo", "bar"), + ContentType("testing", "c").withParameter("foo", "bar") + ) + + setupWithContentNegotiation { + for (typeToSend in registeredTypesToSend) { + register(typeToSend, TestContentConverter()) + } + } + + test { client -> + client.get("https://test.com/") { + exclude(ContentType("testing", "b")) + }.apply { + val sentTypes = assertNotNull(call.request.headers.getAll(HttpHeaders.Accept)) + .map { ContentType.parse(it) } + + // Order NOT tested + for (typeToSend in registeredTypesToSend.filter { it.contentSubtype != "b" }) { + assertContains(sentTypes, typeToSend) + } + assertNull(sentTypes.firstOrNull { it.contentSubtype == "b" }) + } + } + } + } + + @Test + fun addAcceptHeadersWithMultipleExclusions() { + testWithEngine(MockEngine) { + val registeredTypesToSend = listOf( + ContentType("testing", "a"), + ContentType("testing", "b"), + ContentType("testing", "c") + ) + + setupWithContentNegotiation { + for (typeToSend in registeredTypesToSend) { + register(typeToSend, TestContentConverter()) + } + } + + test { client -> + client.get("https://test.com/") { + exclude(ContentType("testing", "b")) + exclude(ContentType("testing", "c")) + }.apply { + val sentTypes = assertNotNull(call.request.headers.getAll(HttpHeaders.Accept)) + .map { ContentType.parse(it) } + + // Order NOT tested + assertTrue(sentTypes.size == 1) + assertContains(sentTypes, ContentType("testing", "a")) + } + } + + test { client -> + client.get("https://test.com/") { + exclude(ContentType("testing", "b"), ContentType("testing", "c")) + }.apply { + val sentTypes = assertNotNull(call.request.headers.getAll(HttpHeaders.Accept)) + .map { ContentType.parse(it) } + + // Order NOT tested + assertTrue(sentTypes.size == 1) + assertContains(sentTypes, ContentType("testing", "a")) + } + } + } + } + @Test fun addAcceptHeadersWithDefaultQValue() { testWithEngine(MockEngine) { From 86f4988adbcef799bb82293d3ba34b1491844355 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Thu, 14 Nov 2024 18:09:06 -0500 Subject: [PATCH 5/9] Update client content negotiation APIs --- .../api/ktor-client-content-negotiation.api | 4 ++++ .../api/ktor-client-content-negotiation.klib.api | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api index ad13c0a333b..7304976a281 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api @@ -5,14 +5,18 @@ public final class io/ktor/client/plugins/contentnegotiation/ContentConverterExc public final class io/ktor/client/plugins/contentnegotiation/ContentNegotiationConfig : io/ktor/serialization/Configuration { public fun ()V public final fun clearIgnoredTypes ()V + public final fun getDefaultAcceptHeaderQValue ()Ljava/lang/String; public final fun ignoreType (Lkotlin/reflect/KClass;)V public final fun register (Lio/ktor/http/ContentType;Lio/ktor/serialization/ContentConverter;Lio/ktor/http/ContentTypeMatcher;Lkotlin/jvm/functions/Function1;)V public fun register (Lio/ktor/http/ContentType;Lio/ktor/serialization/ContentConverter;Lkotlin/jvm/functions/Function1;)V public final fun removeIgnoredType (Lkotlin/reflect/KClass;)V + public final fun setDefaultAcceptHeaderQValue (Ljava/lang/String;)V } public final class io/ktor/client/plugins/contentnegotiation/ContentNegotiationKt { + public static final fun exclude (Lio/ktor/client/request/HttpRequestBuilder;[Lio/ktor/http/ContentType;)V public static final fun getContentNegotiation ()Lio/ktor/client/plugins/api/ClientPlugin; + public static final fun getExcludeContentTypes ()Lio/ktor/util/AttributeKey; } public final class io/ktor/client/plugins/contentnegotiation/JsonContentTypeMatcher : io/ktor/http/ContentTypeMatcher { diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api index 06315ef5525..7eb911d8387 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api @@ -13,6 +13,10 @@ final class io.ktor.client.plugins.contentnegotiation/ContentConverterException final class io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig : io.ktor.serialization/Configuration { // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig|null[0] constructor () // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.|(){}[0] + final var defaultAcceptHeaderQValue // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue|{}defaultAcceptHeaderQValue[0] + final fun (): kotlin/String? // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue.|(){}[0] + final fun (kotlin/String?) // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue.|(kotlin.String?){}[0] + final fun <#A1: io.ktor.serialization/ContentConverter> register(io.ktor.http/ContentType, #A1, io.ktor.http/ContentTypeMatcher, kotlin/Function1<#A1, kotlin/Unit>) // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.register|register(io.ktor.http.ContentType;0:0;io.ktor.http.ContentTypeMatcher;kotlin.Function1<0:0,kotlin.Unit>){0§}[0] final fun <#A1: io.ktor.serialization/ContentConverter> register(io.ktor.http/ContentType, #A1, kotlin/Function1<#A1, kotlin/Unit>) // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.register|register(io.ktor.http.ContentType;0:0;kotlin.Function1<0:0,kotlin.Unit>){0§}[0] final fun clearIgnoredTypes() // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.clearIgnoredTypes|clearIgnoredTypes(){}[0] @@ -28,3 +32,7 @@ final object io.ktor.client.plugins.contentnegotiation/JsonContentTypeMatcher : final val io.ktor.client.plugins.contentnegotiation/ContentNegotiation // io.ktor.client.plugins.contentnegotiation/ContentNegotiation|{}ContentNegotiation[0] final fun (): io.ktor.client.plugins.api/ClientPlugin // io.ktor.client.plugins.contentnegotiation/ContentNegotiation.|(){}[0] +final val io.ktor.client.plugins.contentnegotiation/ExcludeContentTypes // io.ktor.client.plugins.contentnegotiation/ExcludeContentTypes|{}ExcludeContentTypes[0] + final fun (): io.ktor.util/AttributeKey> // io.ktor.client.plugins.contentnegotiation/ExcludeContentTypes.|(){}[0] + +final fun (io.ktor.client.request/HttpRequestBuilder).io.ktor.client.plugins.contentnegotiation/exclude(kotlin/Array...) // io.ktor.client.plugins.contentnegotiation/exclude|exclude@io.ktor.client.request.HttpRequestBuilder(kotlin.Array...){}[0] From a3bdf86528b97091c9378dbcdf1cccd64a78b55e Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Tue, 19 Nov 2024 14:58:15 -0500 Subject: [PATCH 6/9] Improve docs for ContentType.match --- ktor-http/common/src/io/ktor/http/ContentTypes.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ktor-http/common/src/io/ktor/http/ContentTypes.kt b/ktor-http/common/src/io/ktor/http/ContentTypes.kt index c2444bc5c74..117c423d90e 100644 --- a/ktor-http/common/src/io/ktor/http/ContentTypes.kt +++ b/ktor-http/common/src/io/ktor/http/ContentTypes.kt @@ -54,6 +54,14 @@ public class ContentType private constructor( /** * Checks if `this` type matches a [pattern] type taking into account placeholder symbols `*` and parameters. + * The `this` type must be a more specific type than the [pattern] type. In other words: + * + * ```kotlin + * ContentType("a", "b").match(ContentType("a", "b").withParameter("foo", "bar")) === false + * ContentType("a", "b").withParameter("foo", "bar").match(ContentType("a", "b")) === true + * ContentType("a", "*").match(ContentType("a", "b")) === false + * ContentType("a", "b").match(ContentType("a", "*")) === true + * ``` */ public fun match(pattern: ContentType): Boolean { if (pattern.contentType != "*" && !pattern.contentType.equals(contentType, ignoreCase = true)) { From 6b5923c318ba68acd87df09b33fc4a73fd8f58b4 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Tue, 19 Nov 2024 14:58:58 -0500 Subject: [PATCH 7/9] Content type registration accepts more specific json types --- .../client/plugins/contentnegotiation/ContentNegotiation.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt index 94090369e6d..63f5e38bbce 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt @@ -66,8 +66,8 @@ public class ContentNegotiationConfig : Configuration { converter: T, configuration: T.() -> Unit ) { - val matcher = when (contentType) { - ContentType.Application.Json -> JsonContentTypeMatcher + val matcher = when { + contentType.match(ContentType.Application.Json) -> JsonContentTypeMatcher else -> defaultMatcher(contentType) } register(contentType, converter, matcher, configuration) From c317d12c67876d4d116f9c73bbd8d3c0db300477 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Tue, 19 Nov 2024 15:00:18 -0500 Subject: [PATCH 8/9] Minor logging, naming, and kdoc changes base on code review --- .../contentnegotiation/ContentNegotiation.kt | 21 ++++++++++--------- .../client/plugins/ContentNegotiationTests.kt | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt index 63f5e38bbce..c1f547f7c5c 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt @@ -31,9 +31,10 @@ internal val DefaultCommonIgnoredTypes: Set> = setOf( internal expect val DefaultIgnoredTypes: Set> /** - * Shows that request should skip auth and refresh token procedure. + * The content types that are excluded from the `Accept` header for this specific request. Use the + * [exclude] `HttpRequestBuilder` extension to set this attribute on a request. */ -public val ExcludeContentTypes: AttributeKey> = AttributeKey("exclude-content-types") +public val ExcludedContentTypes: AttributeKey> = AttributeKey("ExcludedContentTypesAttr") /** * A [ContentNegotiation] configuration that is used during installation. @@ -56,7 +57,7 @@ public class ContentNegotiationConfig : Configuration { * By default, `Accept` headers for registered content types will have no q value (implicit 1.0). Set this to * change that behavior. This is useful to override the preferred `Accept` content types on a per-request basis. */ - public var defaultAcceptHeaderQValue: String? = null + public var defaultAcceptHeaderQValue: Double? = null /** * Registers a [contentType] to a specified [converter] with an optional [configuration] script for a converter. @@ -152,23 +153,23 @@ public val ContentNegotiation: ClientPlugin = createCl val ignoredTypes: Set> = pluginConfig.ignoredTypes suspend fun convertRequest(request: HttpRequestBuilder, body: Any): OutgoingContent? { - val requestRegistrations = if (request.attributes.contains(ExcludeContentTypes)) { - val excluded = request.attributes[ExcludeContentTypes] + val requestRegistrations = if (request.attributes.contains(ExcludedContentTypes)) { + val excluded = request.attributes[ExcludedContentTypes] registrations.filter { registration -> excluded.none { registration.contentTypeToSend.match(it) } } } else { registrations } + val acceptHeaders = request.headers.getAll(HttpHeaders.Accept).orEmpty() requestRegistrations.forEach { - val acceptHeaders = request.headers.getAll(HttpHeaders.Accept).orEmpty() if (acceptHeaders.none { h -> ContentType.parse(h).match(it.contentTypeToSend) }) { - LOGGER.trace("Adding Accept=${it.contentTypeToSend.contentType} header with q=0.8 for ${request.url}") // automatically added headers get a lower content type priority, so user-specified accept headers // with higher q or implicit q=1 will take precedence val contentTypeToSend = when (val qValue = pluginConfig.defaultAcceptHeaderQValue) { null -> it.contentTypeToSend - else -> it.contentTypeToSend.withParameter("q", qValue) + else -> it.contentTypeToSend.withParameter("q", qValue.toString()) } + LOGGER.trace("Adding Accept=$contentTypeToSend header for ${request.url}") request.accept(contentTypeToSend) } } @@ -285,6 +286,6 @@ public class ContentConverterException(message: String) : Exception(message) * be passed in a single call. */ public fun HttpRequestBuilder.exclude(vararg contentType: ContentType) { - val excludedContentTypes = attributes.getOrNull(ExcludeContentTypes).orEmpty() - attributes.put(ExcludeContentTypes, excludedContentTypes + contentType) + val excludedContentTypes = attributes.getOrNull(ExcludedContentTypes).orEmpty() + attributes.put(ExcludedContentTypes, excludedContentTypes + contentType) } diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt index 450ea837f27..38783d52009 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/test/io/ktor/client/plugins/ContentNegotiationTests.kt @@ -191,7 +191,7 @@ class ContentNegotiationTests { setupWithContentNegotiation { for (typeToSend in registeredTypesToSend) { register(typeToSend, TestContentConverter()) - defaultAcceptHeaderQValue = "0.8" + defaultAcceptHeaderQValue = 0.8 } } From b1444020e29083f97a00dea3d3fd9dcc9986a2f2 Mon Sep 17 00:00:00 2001 From: Mariia Skripchenko <61115099+marychatte@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:23:36 +0100 Subject: [PATCH 9/9] run apiDump, fix codestyle --- .../api/ktor-client-content-negotiation.api | 5 ++--- .../api/ktor-client-content-negotiation.klib.api | 6 ++---- .../client/plugins/contentnegotiation/ContentNegotiation.kt | 6 +++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api index 7304976a281..912d384413f 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.api @@ -5,18 +5,17 @@ public final class io/ktor/client/plugins/contentnegotiation/ContentConverterExc public final class io/ktor/client/plugins/contentnegotiation/ContentNegotiationConfig : io/ktor/serialization/Configuration { public fun ()V public final fun clearIgnoredTypes ()V - public final fun getDefaultAcceptHeaderQValue ()Ljava/lang/String; + public final fun getDefaultAcceptHeaderQValue ()Ljava/lang/Double; public final fun ignoreType (Lkotlin/reflect/KClass;)V public final fun register (Lio/ktor/http/ContentType;Lio/ktor/serialization/ContentConverter;Lio/ktor/http/ContentTypeMatcher;Lkotlin/jvm/functions/Function1;)V public fun register (Lio/ktor/http/ContentType;Lio/ktor/serialization/ContentConverter;Lkotlin/jvm/functions/Function1;)V public final fun removeIgnoredType (Lkotlin/reflect/KClass;)V - public final fun setDefaultAcceptHeaderQValue (Ljava/lang/String;)V + public final fun setDefaultAcceptHeaderQValue (Ljava/lang/Double;)V } public final class io/ktor/client/plugins/contentnegotiation/ContentNegotiationKt { public static final fun exclude (Lio/ktor/client/request/HttpRequestBuilder;[Lio/ktor/http/ContentType;)V public static final fun getContentNegotiation ()Lio/ktor/client/plugins/api/ClientPlugin; - public static final fun getExcludeContentTypes ()Lio/ktor/util/AttributeKey; } public final class io/ktor/client/plugins/contentnegotiation/JsonContentTypeMatcher : io/ktor/http/ContentTypeMatcher { diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api index 7eb911d8387..2638ca4ba91 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/api/ktor-client-content-negotiation.klib.api @@ -14,8 +14,8 @@ final class io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig : constructor () // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.|(){}[0] final var defaultAcceptHeaderQValue // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue|{}defaultAcceptHeaderQValue[0] - final fun (): kotlin/String? // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue.|(){}[0] - final fun (kotlin/String?) // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue.|(kotlin.String?){}[0] + final fun (): kotlin/Double? // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue.|(){}[0] + final fun (kotlin/Double?) // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.defaultAcceptHeaderQValue.|(kotlin.Double?){}[0] final fun <#A1: io.ktor.serialization/ContentConverter> register(io.ktor.http/ContentType, #A1, io.ktor.http/ContentTypeMatcher, kotlin/Function1<#A1, kotlin/Unit>) // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.register|register(io.ktor.http.ContentType;0:0;io.ktor.http.ContentTypeMatcher;kotlin.Function1<0:0,kotlin.Unit>){0§}[0] final fun <#A1: io.ktor.serialization/ContentConverter> register(io.ktor.http/ContentType, #A1, kotlin/Function1<#A1, kotlin/Unit>) // io.ktor.client.plugins.contentnegotiation/ContentNegotiationConfig.register|register(io.ktor.http.ContentType;0:0;kotlin.Function1<0:0,kotlin.Unit>){0§}[0] @@ -32,7 +32,5 @@ final object io.ktor.client.plugins.contentnegotiation/JsonContentTypeMatcher : final val io.ktor.client.plugins.contentnegotiation/ContentNegotiation // io.ktor.client.plugins.contentnegotiation/ContentNegotiation|{}ContentNegotiation[0] final fun (): io.ktor.client.plugins.api/ClientPlugin // io.ktor.client.plugins.contentnegotiation/ContentNegotiation.|(){}[0] -final val io.ktor.client.plugins.contentnegotiation/ExcludeContentTypes // io.ktor.client.plugins.contentnegotiation/ExcludeContentTypes|{}ExcludeContentTypes[0] - final fun (): io.ktor.util/AttributeKey> // io.ktor.client.plugins.contentnegotiation/ExcludeContentTypes.|(){}[0] final fun (io.ktor.client.request/HttpRequestBuilder).io.ktor.client.plugins.contentnegotiation/exclude(kotlin/Array...) // io.ktor.client.plugins.contentnegotiation/exclude|exclude@io.ktor.client.request.HttpRequestBuilder(kotlin.Array...){}[0] diff --git a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt index c1f547f7c5c..21edbe71305 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-content-negotiation/common/src/io/ktor/client/plugins/contentnegotiation/ContentNegotiation.kt @@ -34,7 +34,7 @@ internal expect val DefaultIgnoredTypes: Set> * The content types that are excluded from the `Accept` header for this specific request. Use the * [exclude] `HttpRequestBuilder` extension to set this attribute on a request. */ -public val ExcludedContentTypes: AttributeKey> = AttributeKey("ExcludedContentTypesAttr") +internal val ExcludedContentTypes: AttributeKey> = AttributeKey("ExcludedContentTypesAttr") /** * A [ContentNegotiation] configuration that is used during installation. @@ -166,8 +166,8 @@ public val ContentNegotiation: ClientPlugin = createCl // automatically added headers get a lower content type priority, so user-specified accept headers // with higher q or implicit q=1 will take precedence val contentTypeToSend = when (val qValue = pluginConfig.defaultAcceptHeaderQValue) { - null -> it.contentTypeToSend - else -> it.contentTypeToSend.withParameter("q", qValue.toString()) + null -> it.contentTypeToSend + else -> it.contentTypeToSend.withParameter("q", qValue.toString()) } LOGGER.trace("Adding Accept=$contentTypeToSend header for ${request.url}") request.accept(contentTypeToSend)