diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt index 52fa222b113..5c6319b84eb 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/HttpClientConfig.kt @@ -7,12 +7,47 @@ package io.ktor.client import io.ktor.client.engine.* import io.ktor.client.plugins.* import io.ktor.util.* -import io.ktor.util.collections.* import io.ktor.utils.io.* -import kotlin.collections.set /** - * A mutable [HttpClient] configuration. + * A mutable [HttpClient] configuration used to adjust settings, install plugins and interceptors. + * + * This class is available as block in [HttpClient] constructor or [HttpClient.config] builder: + * ```kotlin + * val client = HttpClient { // HttpClientConfig() + * // Configure engine settings + * engine { // HttpClientEngineConfig + * threadsCount = 4 + * pipelining = true + * } + * + * // Install and configure plugins + * install(ContentNegotiation) { + * json() + * } + * + * // Configure default request parameters + * defaultRequest { + * url("https://api.example.com") + * header("X-Custom-Header", "value") + * } + * + * // Configure client-wide settings + * expectSuccess = true + * followRedirects = true + * } + * ``` + * ## Configuring [HttpClientEngine] + * + * If the engine is specified explicitly, engine-specific properties will be available in the engine block: + * ```kotlin + * val client = HttpClient(CIO) { // HttpClientConfig.() -> Unit + * engine { // CIOEngineConfig.() -> Unit + * // engine specific properties + * } + * } + * ``` + * * Learn more about the client's configuration from * [Creating and configuring a client](https://ktor.io/docs/create-client.html). */ @@ -25,7 +60,15 @@ public class HttpClientConfig { internal var engineConfig: T.() -> Unit = {} /** - * Allows you to configure engine parameters. + * Builder for configuring engine-specific settings in [HttpClientEngineConfig] + * (like dispatcher, threads count, proxy, etc.) + * + * ```kotlin + * val client = HttpClient(CIO) { // HttpClientConfig + * engine { // CIOEngineConfig.() -> Unit + * proxy = ProxyBuilder.http("proxy.example.com", 8080) + * } + * ``` * * You can learn more from [Engines](https://ktor.io/docs/http-client-engines.html). */ @@ -40,17 +83,34 @@ public class HttpClientConfig { /** * Specifies whether the client redirects to URLs provided in the `Location` header. * You can disable redirections by setting this property to `false`. + * + * For the advanced redirection configuration, use [HttpRedirect] plugin. */ public var followRedirects: Boolean = true /** - * Uses [defaultTransformers] to automatically handle simple [ContentType]. + * Enable body transformations for many common types like [String], [ByteArray], [ByteReadChannel], etc. + * These transformations are applied to the request and response bodies. + * + * The transformers will be used when the response body is received with a type: + * ```kotlin + * val client = HttpClient() + * val bytes = client.get("https://ktor.io") + * .body() + * ``` + * + * The flag is enabled by default. + * You might want to disable it if you want to write your own transformers or handle body manually. + * + * Check [defaultTransformers] documentation for more details. */ public var useDefaultTransformers: Boolean = true /** * Terminates [HttpClient.receivePipeline] if the status code is not successful (>=300). * Learn more from [Response validation](https://ktor.io/docs/response-validation.html). + * + * Please check [HttpCallValidator] documentation to learn more details. */ public var expectSuccess: Boolean = false @@ -66,6 +126,18 @@ public class HttpClientConfig { /** * Installs the specified [plugin] and optionally configures it using the [configure] block. + * + * ```kotlin + * val client = HttpClient { + * install(ContentNegotiation) { + * // configuration block + * json() + * } + * } + * ``` + * + * If the plugin is already installed, the Configuration block will be applied to the existing configuration class. + * * Learn more from [Plugins](https://ktor.io/docs/http-client-plugins.html). */ public fun install( @@ -95,6 +167,8 @@ public class HttpClientConfig { /** * Installs an interceptor defined by [block]. * The [key] parameter is used as a unique name, that also prevents installing duplicated interceptors. + * + * If the [key] is already used, the new interceptor will replace the old one. */ public fun install(key: String, block: HttpClient.() -> Unit) { customInterceptors[key] = block diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/call/SavedCall.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/call/SavedCall.kt index 7ea85c2e6b1..83cef024b49 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/call/SavedCall.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/call/SavedCall.kt @@ -10,8 +10,30 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.util.date.* import io.ktor.utils.io.* -import kotlinx.io.* -import kotlin.coroutines.* +import kotlinx.io.readByteArray +import kotlin.coroutines.CoroutineContext + + +/** + * Saves the entire content of this [HttpClientCall] to memory and returns a new [HttpClientCall] + * with the content cached in memory. + * This can be particularly useful for caching, debugging, + * or processing responses without relying on the original network stream. + * + * By caching the content, this function simplifies the management of the [HttpResponse] lifecycle. + * It releases the network connection and other resources associated with the original [HttpResponse], + * ensuring they are no longer required to be explicitly closed. + * + * This behavior is automatically applied to non-streaming [HttpResponse] instances. + * For streaming responses, this function allows you to convert them into a memory-based representation. + * + * @return A new [HttpClientCall] instance with all its content stored in memory. + */ +@OptIn(InternalAPI::class) +public suspend fun HttpClientCall.save(): HttpClientCall { + val responseBody = response.rawContent.readRemaining().readByteArray() + return SavedHttpCall(client, request, response, responseBody) +} internal class SavedHttpCall( client: HttpClient, @@ -60,14 +82,3 @@ internal class SavedHttpResponse( @OptIn(InternalAPI::class) override val rawContent: ByteReadChannel get() = ByteReadChannel(body) } - -/** - * Fetch data for [HttpClientCall] and close the origin. - */ - -@OptIn(InternalAPI::class) -public suspend fun HttpClientCall.save(): HttpClientCall { - val responseBody = response.rawContent.readRemaining().readByteArray() - - return SavedHttpCall(client, request, response, responseBody) -} diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/call/utils.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/call/utils.kt index e72044c4095..fe9c9341b2a 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/call/utils.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/call/utils.kt @@ -7,10 +7,19 @@ package io.ktor.client.call import io.ktor.http.* import io.ktor.http.content.* +/** + * Exception thrown when the engine does not support the content type of the HTTP request body. + * For instance, some engines do not support upgrade requests. + */ public class UnsupportedContentTypeException(content: OutgoingContent) : IllegalStateException("Failed to write body: ${content::class}") @Suppress("KDocMissingDocumentation", "UNUSED") +@Deprecated( + "This exception is deprecated, use UnsupportedContentTypeException instead.", + replaceWith = ReplaceWith("UnsupportedContentTypeException(content)"), + level = DeprecationLevel.WARNING +) public class UnsupportedUpgradeProtocolException( url: Url ) : IllegalArgumentException("Unsupported upgrade protocol exception: $url") diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/content/ObservableContent.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/content/ObservableContent.kt index 4a8cc205fdb..f8ca3315787 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/content/ObservableContent.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/content/ObservableContent.kt @@ -5,22 +5,28 @@ package io.ktor.client.content import io.ktor.client.call.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* import io.ktor.client.utils.* import io.ktor.http.* import io.ktor.http.content.* import io.ktor.util.* import io.ktor.utils.io.* -import kotlinx.coroutines.* -import kotlin.coroutines.* +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlin.coroutines.CoroutineContext /** * Callback that can be registered to listen for upload/download progress. - * @param bytesSentTotal number of transmitted bytes. - * @param contentLength body size. Can be null if the size is unknown. + * + * This class is used for callbacks in [HttpRequestBuilder.onDownload] and [HttpRequestBuilder.onUpload]. */ public fun interface ProgressListener { /** * Invokes every time some data is flushed through the [ByteReadChannel]. + * + * @param bytesSentTotal number of transmitted bytes. + * @param contentLength body size. Can be null if the size is unknown. */ public suspend fun onProgress(bytesSentTotal: Long, contentLength: Long?) } diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngine.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngine.kt index a79e707d586..acf53eb0d9c 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngine.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngine.kt @@ -13,27 +13,85 @@ import io.ktor.util.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* import kotlinx.coroutines.* -import kotlin.coroutines.* +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.coroutineContext internal val CALL_COROUTINE = CoroutineName("call-context") internal val CLIENT_CONFIG = AttributeKey>("client-config") /** * Serves as the base interface for an [HttpClient]'s engine. + * + * An `HttpClientEngine` represents the underlying network implementation that + * performs HTTP requests and handles responses. + * Developers can implement this interface to create custom engines for use with [HttpClient]. + * + * This interface provides a set of properties and methods that define the + * contract for configuring, executing, and managing HTTP requests within the engine. + * + * See also [HttpClientEngineBase] for a base implementation that handles common engine functionality. */ public interface HttpClientEngine : CoroutineScope, Closeable { /** - * Specifies [CoroutineDispatcher] for I/O operations. + * Specifies the [CoroutineDispatcher] for I/O operations in the engine. + * + * This dispatcher is used for all network-related operations, such as + * sending requests and receiving responses. + * By default, it should be optimized for I/O tasks. + * + * Example: + * ```kotlin + * override val dispatcher: CoroutineDispatcher = Dispatchers.IO + * ``` */ public val dispatcher: CoroutineDispatcher /** - * Provides access to an engine's configuration. + * Provides access to the engine's configuration via [HttpClientEngineConfig]. + * + * The [config] object stores user-defined parameters or settings that control + * how the engine operates. When creating a custom engine, this property + * should return the specific configuration implementation. + * + * Example: + * ```kotlin + * override val config: HttpClientEngineConfig = CustomEngineConfig() + * ``` */ public val config: HttpClientEngineConfig /** - * Set of supported engine extensions. + * Specifies the set of capabilities supported by this HTTP client engine. + * + * Capabilities provide a mechanism for plugins and other components to + * determine whether the engine supports specific features such as timeouts, + * WebSocket communication, HTTP/2, HTTP/3, or other advanced networking + * capabilities. This allows seamless integration of features based on the + * engine's functionality. + * + * Each capability is represented as an instance of [HttpClientEngineCapability], + * which can carry additional metadata or configurations for the capability. + * + * Example: + * ```kotlin + * override val supportedCapabilities: Set> = setOf( + * WebSocketCapability, + * Http2Capability, + * TimeoutCapability + * ) + * ``` + * + * **Usage in Plugins**: + * Plugins can check if the engine supports a specific capability before + * applying behavior: + * ```kotlin + * if (engine.supportedCapabilities.contains(WebSocketCapability)) { + * // Configure WebSocket-specific settings + * } + * ``` + * + * When implementing a custom engine, ensure this property accurately reflects + * the engine's abilities to avoid unexpected plugin behavior or runtime errors. */ public val supportedCapabilities: Set> get() = emptySet() @@ -42,13 +100,26 @@ public interface HttpClientEngine : CoroutineScope, Closeable { get() = !(coroutineContext[Job]?.isActive ?: false) /** - * Creates a new [HttpClientCall] specific for this engine, using a request [data]. + * Executes an HTTP request and produces an HTTP response. + * + * This function is responsible for converting [HttpRequestData], which + * contains all the request details, into [HttpResponseData], which encapsulates + * the response information such as headers, status code, and body. + * + * @param data The [HttpRequestData] representing the request to be executed. + * @return A [HttpResponseData] object containing the server's response. */ @InternalAPI public suspend fun execute(data: HttpRequestData): HttpResponseData /** - * Installs the engine to [HttpClient]. + * Installs the engine into an [HttpClient]. + * + * This method is called when the engine is being set up within an `HttpClient`. + * Use it to register interceptors, validate configuration, or prepare the engine + * for use with the client. + * + * @param client The [HttpClient] instance to which the engine is being installed. */ @InternalAPI public fun install(client: HttpClient) { @@ -107,16 +178,6 @@ public interface HttpClientEngine : CoroutineScope, Closeable { } } -/** - * A factory of [HttpClientEngine] with a specific [T] of [HttpClientEngineConfig]. - */ -public interface HttpClientEngineFactory { - /** - * Creates a new [HttpClientEngine] optionally specifying a [block] configuring [T]. - */ - public fun create(block: T.() -> Unit = {}): HttpClientEngine -} - /** * Creates a new [HttpClientEngineFactory] based on this one * with further configurations from the [nested] block. diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineBase.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineBase.kt index 0ac6ebc76fb..7bfadfaa3fd 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineBase.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineBase.kt @@ -11,9 +11,28 @@ import kotlinx.coroutines.* import kotlin.coroutines.* /** - * Abstract implementation of [HttpClientEngine] responsible for lifecycle control of [dispatcher] and - * [coroutineContext] as well as proper call context management. Should be considered as the best parent class for - * custom [HttpClientEngine] implementations. + * Abstract base implementation of [HttpClientEngine], providing lifecycle management for the [dispatcher] + * and [coroutineContext], as well as proper handling of call contexts. + * + * This class is designed to simplify the creation of custom [HttpClientEngine] implementations by + * handling common functionality such as: + * - Managing the [dispatcher] for I/O operations. + * - Setting up a structured [coroutineContext] with a custom name for easier debugging. + * - Ensuring proper resource cleanup when the engine is closed. + * + * Developers creating custom HTTP client engines are encouraged to use this class as their parent, + * as it handles much of the boilerplate related to engine lifecycle and coroutine management. + * + * @param engineName The name of the engine, used for debugging and context naming. + * + * Example: + * ```kotlin + * class MyCustomHttpClientEngine : HttpClientEngineBase("MyCustomEngine") { + * override suspend fun execute(data: HttpRequestData): HttpResponseData { + * // Implementation of request execution + * } + * } + * ``` */ public abstract class HttpClientEngineBase(private val engineName: String) : HttpClientEngine { private val closed = atomic(false) diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineCapability.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineCapability.kt index b5e767444a6..61fcf979cf7 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineCapability.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineCapability.kt @@ -19,7 +19,54 @@ internal val ENGINE_CAPABILITIES_KEY = public val DEFAULT_CAPABILITIES: Set> = setOf(HttpTimeoutCapability) /** - * Capability required by request to be supported by [HttpClientEngine] with [T] representing type of the capability - * configuration. + * Represents a capability that an [HttpClientEngine] can support, with [T] representing the type + * of configuration or metadata associated with the capability. + * + * Capabilities are used to declare optional features or behaviors that an engine may support, + * such as WebSocket communication, HTTP/2, or custom timeouts. They enable plugins and request + * builders to configure engine-specific functionality by associating a capability with a + * specific configuration. + * + * Capabilities can be set on a per-request basis using the `HttpRequestBuilder.setCapability` method, + * allowing users to configure engine-specific behavior for individual requests. + * + * @param T The type of the configuration or metadata associated with this capability. + * + * Example: + * Suppose you have a custom capability for WebSocket support that requires a specific configuration: + * ```kotlin + * object WebSocketCapability : HttpClientEngineCapability + * + * data class WebSocketConfig(val maxFrameSize: Int, val pingIntervalMillis: Long) + * ``` + * + * Setting a capability in a request: + * ```kotlin + * client.request { + * setCapability(WebSocketCapability, WebSocketConfig( + * maxFrameSize = 65536, + * pingIntervalMillis = 30000 + * )) + * } + * ``` + * + * Engine Example: + * A custom engine implementation can declare support for specific capabilities in its `supportedCapabilities` property: + * ```kotlin + * override val supportedCapabilities: Set> = setOf(WebSocketCapability) + * ``` + * + * Plugin Integration Example: + * Plugins use capabilities to interact with engine-specific features. For example: + * ```kotlin + * if (engine.supportedCapabilities.contains(WebSocketCapability)) { + * // Configure WebSocket behavior if supported by the engine + * } + * ``` + * + * When creating a custom capability: + * - Define a singleton object implementing `HttpClientEngineCapability`. + * - Use the type parameter [T] to provide the associated configuration type or metadata. + * - Ensure that engines supporting the capability handle the associated configuration properly. */ public interface HttpClientEngineCapability diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineFactory.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineFactory.kt new file mode 100644 index 00000000000..e3f93d2090b --- /dev/null +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineFactory.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.engine + +/** + * A factory for creating instances of [HttpClientEngine] with a specific configuration type [T]. + * + * This interface defines how to produce custom HTTP client engines that implement [HttpClientEngine]. + * Each engine is initialized with a configuration object of type [T], which extends [HttpClientEngineConfig]. + * + * Factories implementing this interface are commonly passed to the [io.ktor.client.HttpClient] constructor to specify the + * underlying engine that will handle HTTP requests. This allows users to seamlessly plug in different + * engines based on their requirements, such as for performance, platform compatibility, or protocol support. + * + * @param T The type of [HttpClientEngineConfig] used to configure the engine. + * + * Example: + * ```kotlin + * object MyCustomEngine: HttpClientEngineFactory { + * // ... + * } + * + * val client = HttpClient(MyCustomEngine) { + * engine { + * timeout = 10_000 + * customSetting = "example" + * } + * } + * ``` + */ +public interface HttpClientEngineFactory { + /** + * Creates or retrieves an instance of [HttpClientEngine], applying optional configurations. + * + * This method is responsible for deciding whether to create a new engine instance or reuse + * an existing one, based on the factory's internal logic and the configuration provided in + * the [block]. This allows for efficient resource management by avoiding unnecessary engine + * instantiation when possible. + * + * The [block] parameter enables users to customize the engine's [T] configuration during creation. + * If no block is provided, the factory should apply default configurations or reuse an engine + * with the default settings. + * + * Typically, this method is invoked internally by the [io.ktor.client.HttpClient] constructor when the factory + * is passed to it. Users can, however, call it directly to explicitly control engine instantiation. + * + * @param block A lambda that applies additional configurations to the engine's [T] object. + * @return An [HttpClientEngine] instance, which may be newly created or reused. + * + * Example with explicit engine creation: + * ```kotlin + * val engine = MyCustomEngineFactory.create { + * timeout = 5_000 + * customHeader = "example" + * } + * ``` + * + * Example when used with `HttpClient`: + * ```kotlin + * val client = HttpClient(MyCustomEngineFactory) { + * engine { + * timeout = 5_000 + * customHeader = "example" + * } + * } + * ``` + * + * **Contracts for Implementors**: + * - If reusing an existing engine, ensure its configuration matches the one provided in [block]. + * - Ensure thread safety when managing a shared engine instance across multiple [create] calls. + * - Provide meaningful default configurations when no block is provided. + * - Properly release resources of reused engines when the [io.ktor.client.HttpClient]] or engine is closed. + */ + public fun create(block: T.() -> Unit = {}): HttpClientEngine +} diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/Utils.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/Utils.kt index f0d2be53168..1257629f507 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/Utils.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/engine/Utils.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.* import kotlin.coroutines.* /** - * Default user agent to use in ktor client. + * Default user agent to use in a ktor client. */ @InternalAPI public val KTOR_DEFAULT_USER_AGENT: String = "ktor-client" diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/DefaultTransform.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/DefaultTransform.kt index 6d35d2a0f6e..29b295283a8 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/DefaultTransform.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/DefaultTransform.kt @@ -23,7 +23,6 @@ private val LOGGER = KtorSimpleLogger("io.ktor.client.plugins.defaultTransformer * Usually installed by default so there is no need to use it * unless you have disabled it via [HttpClientConfig.useDefaultTransformers]. */ -@OptIn(InternalAPI::class) public fun HttpClient.defaultTransformers() { requestPipeline.intercept(HttpRequestPipeline.Render) { body -> if (context.headers[HttpHeaders.Accept] == null) { diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/HttpCallValidator.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/HttpCallValidator.kt index 168e9ec5edd..55be0c0af15 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/HttpCallValidator.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/plugins/HttpCallValidator.kt @@ -75,9 +75,9 @@ public typealias CallExceptionHandler = suspend (cause: Throwable) -> Unit public typealias CallRequestExceptionHandler = suspend (cause: Throwable, request: HttpRequest) -> Unit /** - * Response validator plugin is used for validate response and handle response exceptions. + * Response validator plugin is used for validating [HttpClient] response and handle response exceptions. * - * See also [Config] for additional details. + * See also [HttpCallValidatorConfig] for additional details. */ public val HttpCallValidator: ClientPlugin = createClientPlugin( "HttpResponseValidator", diff --git a/ktor-client/ktor-client-core/common/test/HttpClientConfigTest.kt b/ktor-client/ktor-client-core/common/test/HttpClientConfigTest.kt new file mode 100644 index 00000000000..514696ecd13 --- /dev/null +++ b/ktor-client/ktor-client-core/common/test/HttpClientConfigTest.kt @@ -0,0 +1,44 @@ +import io.ktor.client.* +import io.ktor.client.plugins.api.* +import kotlin.test.Test +import kotlin.test.assertEquals + +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +class HttpClientConfigTest { + + @Test + fun testPluginInstalledTwice() { + var configuration = 0 + var installation = 0 + var first = 0 + var second = 0 + + class Config { + init { + configuration++ + } + } + + val plugin = createClientPlugin("hey", ::Config) { + installation++ + } + + HttpClient { + install(plugin) { + first += 1 + } + + install(plugin) { + second += 1 + } + } + + assertEquals(1, configuration) + assertEquals(1, installation) + assertEquals(1, first) + assertEquals(1, second) + } +} diff --git a/ktor-client/ktor-client-darwin-legacy/darwin/src/io/ktor/client/engine/darwin/DarwinLegacy.kt b/ktor-client/ktor-client-darwin-legacy/darwin/src/io/ktor/client/engine/darwin/DarwinLegacy.kt index 91e3766fedd..21303503fe7 100644 --- a/ktor-client/ktor-client-darwin-legacy/darwin/src/io/ktor/client/engine/darwin/DarwinLegacy.kt +++ b/ktor-client/ktor-client-darwin-legacy/darwin/src/io/ktor/client/engine/darwin/DarwinLegacy.kt @@ -5,7 +5,6 @@ package io.ktor.client.engine.darwin import io.ktor.client.engine.* -import io.ktor.util.* import io.ktor.utils.io.* @Suppress("DEPRECATION") diff --git a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpsTest.kt b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpsTest.kt index fe6b7b2d74a..952078b8288 100644 --- a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpsTest.kt +++ b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/HttpsTest.kt @@ -10,7 +10,6 @@ import io.ktor.client.request.* import io.ktor.client.tests.utils.* import io.ktor.server.engine.* import io.ktor.server.netty.* -import io.ktor.utils.io.errors.* import kotlinx.io.IOException import java.security.* import java.security.cert.*