Коммит 702364f9 создал по автору Rustam's avatar Rustam Зафиксировано автором Aleksei Tirman
Просмотр файлов

KTOR-4547 Add Apache 5 client engine (#3313)

владелец ff69ac13
......@@ -17,6 +17,7 @@ jetty-alpn-boot-version = "8.1.13.v20181017"
tomcat-version = "9.0.70"
apache-version = "4.1.5"
apache5-version = "5.2"
apacheds-version = "2.0.0-M24"
okhttp-version = "4.10.0"
......@@ -123,6 +124,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo
junit = { module = "junit:junit", version.ref = "junit-version" }
apache-httpasyncclient = { module = "org.apache.httpcomponents:httpasyncclient", version.ref = "apache-version" }
apache-client5 = { module = "org.apache.httpcomponents.client5:httpclient5", version.ref = "apache5-version" }
apacheds-server = { module = "org.apache.directory.server:apacheds-server-integ", version.ref = "apacheds-version" }
apacheds-core = { module = "org.apache.directory.server:apacheds-core-integ", version.ref = "apacheds-version" }
......
public final class io/ktor/client/engine/apache5/Apache5 : io/ktor/client/engine/HttpClientEngineFactory {
public static final field INSTANCE Lio/ktor/client/engine/apache5/Apache5;
public fun create (Lkotlin/jvm/functions/Function1;)Lio/ktor/client/engine/HttpClientEngine;
}
public final class io/ktor/client/engine/apache5/Apache5EngineConfig : io/ktor/client/engine/HttpClientEngineConfig {
public fun <init> ()V
public final fun customizeClient (Lkotlin/jvm/functions/Function1;)V
public final fun customizeRequest (Lkotlin/jvm/functions/Function1;)V
public final fun getConnectTimeout ()J
public final fun getConnectionRequestTimeout ()J
public final fun getFollowRedirects ()Z
public final fun getSocketTimeout ()I
public final fun getSslContext ()Ljavax/net/ssl/SSLContext;
public final fun setConnectTimeout (J)V
public final fun setConnectionRequestTimeout (J)V
public final fun setFollowRedirects (Z)V
public final fun setSocketTimeout (I)V
public final fun setSslContext (Ljavax/net/ssl/SSLContext;)V
}
public final class io/ktor/client/engine/apache5/Apache5EngineContainer : io/ktor/client/HttpClientEngineContainer {
public fun <init> ()V
public fun getFactory ()Lio/ktor/client/engine/HttpClientEngineFactory;
public fun toString ()Ljava/lang/String;
}
description = "Apache backend for ktor http client"
apply<test.server.TestServerPlugin>()
kotlin.sourceSets {
jvmMain {
dependencies {
api(project(":ktor-client:ktor-client-core"))
api(libs.apache.client5)
}
}
jvmTest {
dependencies {
api(project(":ktor-client:ktor-client-tests"))
}
}
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import io.ktor.client.*
import io.ktor.client.engine.*
/**
* A JVM client engine that uses the Apache HTTP client.
*
* To create the client with this engine, pass it to the `HttpClient` constructor:
* ```kotlin
* val client = HttpClient(Apache)
* ```
* To configure the engine, pass settings exposed by [ApacheEngineConfig] to the `engine` method:
* ```kotlin
* val client = HttpClient(Apache) {
* engine {
* // this: ApacheEngineConfig
* }
* }
* ```
*
* You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html).
*/
public object Apache5 : HttpClientEngineFactory<Apache5EngineConfig> {
override fun create(block: Apache5EngineConfig.() -> Unit): HttpClientEngine {
val config = Apache5EngineConfig().apply(block)
return Apache5Engine(config)
}
}
@Suppress("KDocMissingDocumentation")
public class Apache5EngineContainer : HttpClientEngineContainer {
override val factory: HttpClientEngineFactory<*> = Apache5
override fun toString(): String = "Apache5"
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import io.ktor.client.engine.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.utils.*
import io.ktor.util.*
import kotlinx.coroutines.*
import org.apache.hc.client5.http.config.*
import org.apache.hc.client5.http.impl.async.*
import org.apache.hc.client5.http.impl.nio.*
import org.apache.hc.client5.http.ssl.*
import org.apache.hc.core5.http.*
import org.apache.hc.core5.http.ssl.*
import org.apache.hc.core5.reactor.*
import org.apache.hc.core5.ssl.*
import java.net.*
import java.util.concurrent.*
private const val MAX_CONNECTIONS_COUNT = 1000
private const val IO_THREAD_COUNT_DEFAULT = 4
@OptIn(InternalAPI::class)
internal class Apache5Engine(override val config: Apache5EngineConfig) : HttpClientEngineBase("ktor-apache") {
override val dispatcher by lazy {
Dispatchers.clientDispatcher(
config.threadsCount,
"ktor-apache-dispatcher"
)
}
override val supportedCapabilities = setOf(HttpTimeout)
@Volatile
private var engine: CloseableHttpAsyncClient? = null
override suspend fun execute(data: HttpRequestData): HttpResponseData {
val callContext = callContext()
val engine = engine(data)
val apacheRequest = ApacheRequestProducer(data, config, callContext)
return engine.sendRequest(apacheRequest, callContext, data)
}
override fun close() {
super.close()
coroutineContext[Job]!!.invokeOnCompletion {
engine?.close()
}
}
private fun engine(data: HttpRequestData): CloseableHttpAsyncClient {
return engine ?: synchronized(this) {
engine ?: HttpAsyncClients.custom().apply {
val timeout = data.getCapabilityOrNull(HttpTimeout)
setThreadFactory {
Thread(it, "Ktor-client-apache").apply {
isDaemon = true
setUncaughtExceptionHandler { _, _ -> }
}
}
disableAuthCaching()
disableConnectionState()
disableCookieManagement()
setConnectionManager(
PoolingAsyncClientConnectionManagerBuilder.create()
.setMaxConnTotal(MAX_CONNECTIONS_COUNT)
.setMaxConnTotal(MAX_CONNECTIONS_COUNT)
.setTlsStrategy(
ClientTlsStrategyBuilder.create()
.setSslContext(config.sslContext ?: SSLContexts.createSystemDefault())
.setTlsVersions(TLS.V_1_3, TLS.V_1_2)
.build()
)
.setDefaultConnectionConfig(
ConnectionConfig.custom()
.setConnectTimeout(
timeout?.connectTimeoutMillis ?: config.connectTimeout,
TimeUnit.MILLISECONDS
)
.setSocketTimeout(
timeout?.socketTimeoutMillis?.toInt() ?: config.socketTimeout,
TimeUnit.MILLISECONDS
)
.build()
)
.build()
)
setIOReactorConfig(
IOReactorConfig.custom()
.setIoThreadCount(IO_THREAD_COUNT_DEFAULT)
.build()
)
setupProxy()
with(config) { customClient() }
}.build().also {
engine = it
it.start()
}
}
}
private fun HttpAsyncClientBuilder.setupProxy() {
val proxy = config.proxy ?: return
if (proxy.type() == Proxy.Type.DIRECT) {
return
}
val address = proxy.address()
check(proxy.type() == Proxy.Type.HTTP && address is InetSocketAddress) {
"Only http proxy is supported for Apache engine."
}
setProxy(HttpHost.create("http://${address.hostName}:${address.port}"))
}
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import io.ktor.client.engine.*
import org.apache.hc.client5.http.config.*
import org.apache.hc.client5.http.impl.async.*
import javax.net.ssl.*
/**
* A configuration for the [Apache5] client engine.
*/
public class Apache5EngineConfig : HttpClientEngineConfig() {
/**
* Specifies whether to follow redirects automatically.
* Disabled by default.
*
* _Note: By default, the Apache client allows `50` redirects._
*/
public var followRedirects: Boolean = false
/**
* Specifies a maximum time (in milliseconds) of inactivity between two data packets when exchanging data with a server.
*
* Set this value to `0` to use an infinite timeout.
*/
public var socketTimeout: Int = 10_000
/**
* Specifies a time period (in milliseconds) in which a client should establish a connection with a server.
*
* A `0` value represents an infinite timeout, while `-1` represents a system's default value.
*/
public var connectTimeout: Long = 10_000
/**
* Specifies a time period (in milliseconds) in which a client should start a request.
*
* A `0` value represents an infinite timeout, while `-1` represents a system's default value.
*/
public var connectionRequestTimeout: Long = 20_000
/**
* Allows you to configure [SSL](https://ktor.io/docs/client-ssl.html) settings for this engine.
*/
public var sslContext: SSLContext? = null
internal var customRequest: (RequestConfig.Builder.() -> RequestConfig.Builder) = { this }
internal var customClient: (HttpAsyncClientBuilder.() -> HttpAsyncClientBuilder) = { this }
/**
* Customizes a [RequestConfig.Builder] in the specified [block].
*/
public fun customizeRequest(block: RequestConfig.Builder.() -> Unit) {
val current = customRequest
customRequest = { current(); block(); this }
}
/**
* Customizes a [HttpAsyncClientBuilder] in the specified [block].
*/
public fun customizeClient(block: HttpAsyncClientBuilder.() -> Unit) {
val current = customClient
customClient = { current(); block(); this }
}
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.date.*
import kotlinx.coroutines.*
import org.apache.hc.client5.http.impl.async.*
import org.apache.hc.core5.concurrent.*
import org.apache.hc.core5.http.message.*
import org.apache.hc.core5.http.nio.*
import java.net.*
import kotlin.coroutines.*
internal suspend fun CloseableHttpAsyncClient.sendRequest(
request: AsyncRequestProducer,
callContext: CoroutineContext,
requestData: HttpRequestData
): HttpResponseData {
val requestTime = GMTDate()
val bodyConsumer = ApacheResponseConsumer(callContext, requestData)
val responseConsumer = BasicResponseConsumer(bodyConsumer)
val callback = object : FutureCallback<Unit> {
override fun failed(exception: Exception) {}
override fun completed(result: Unit) {}
override fun cancelled() {}
}
val future = execute(request, responseConsumer, callback)!!
try {
val rawResponse = responseConsumer.responseDeferred.await()
val statusLine = StatusLine(rawResponse)
val status = HttpStatusCode(statusLine.statusCode, statusLine.reasonPhrase ?: "")
val version = with(statusLine.protocolVersion) {
HttpProtocolVersion.fromValue(protocol, major, minor)
}
val rawHeaders = rawResponse.headers.filter {
it.name != null || it.name.isNotBlank()
}.groupBy(
{ it.name },
{ it.value ?: "" }
)
val headers = HeadersImpl(rawHeaders)
return HttpResponseData(status, requestTime, headers, version, bodyConsumer.responseChannel, callContext)
} catch (cause: Exception) {
future.cancel(true)
val mappedCause = mapCause(cause, requestData)
callContext.cancel(CancellationException("Failed to execute request.", mappedCause))
throw mappedCause
}
}
internal fun mapCause(exception: Exception, requestData: HttpRequestData): Exception = when {
exception is ConnectException && exception.isTimeoutException() -> ConnectTimeoutException(requestData, exception)
exception is SocketTimeoutException -> SocketTimeoutException(requestData, exception)
else -> exception
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import io.ktor.client.call.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.http.HttpHeaders
import io.ktor.http.content.*
import io.ktor.util.*
import io.ktor.utils.io.*
import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import org.apache.hc.client5.http.async.methods.*
import org.apache.hc.client5.http.classic.methods.*
import org.apache.hc.client5.http.config.*
import org.apache.hc.client5.http.utils.*
import org.apache.hc.core5.http.*
import org.apache.hc.core5.http.HttpRequest
import org.apache.hc.core5.http.io.entity.*
import org.apache.hc.core5.http.message.*
import org.apache.hc.core5.http.nio.*
import org.apache.hc.core5.http.nio.entity.*
import org.apache.hc.core5.http.nio.support.*
import org.apache.hc.core5.http.protocol.*
import java.nio.*
import java.util.concurrent.*
import kotlin.coroutines.*
@OptIn(InternalAPI::class)
internal fun ApacheRequestProducer(
requestData: HttpRequestData,
config: Apache5EngineConfig,
callContext: CoroutineContext
): AsyncRequestProducer {
val content = requestData.body
var length: String? = null
var type: String? = null
mergeHeaders(requestData.headers, content) { key, value ->
when (key) {
HttpHeaders.ContentLength -> length = value
HttpHeaders.ContentType -> type = value
}
}
val isGetOrHead = requestData.method == HttpMethod.Get || requestData.method == HttpMethod.Head
val hasContent = requestData.body !is OutgoingContent.NoContent
val contentLength = length?.toLong() ?: -1
val isChunked = contentLength == -1L && !isGetOrHead && hasContent
return BasicRequestProducer(
setupRequest(requestData, config),
if (!hasContent && isGetOrHead) null
else ApacheRequestEntityProducer(requestData, callContext, contentLength, type, isChunked)
)
}
@OptIn(InternalAPI::class)
private fun setupRequest(requestData: HttpRequestData, config: Apache5EngineConfig): HttpRequest = with(requestData) {
val request = ConfigurableHttpRequest(method.value, url.toURI())
mergeHeaders(headers, body) { key, value ->
when (key) {
HttpHeaders.ContentLength -> {}
HttpHeaders.ContentType -> {}
else -> request.addHeader(key, value)
}
}
with(config) {
request.config = RequestConfig.custom()
.setRedirectsEnabled(followRedirects)
.setConnectionRequestTimeout(connectionRequestTimeout, TimeUnit.MILLISECONDS)
.customRequest()
.build()
}
return request
}
internal class ApacheRequestEntityProducer(
private val requestData: HttpRequestData,
callContext: CoroutineContext,
private val contentLength: Long,
private val contentType: String?,
private val isChunked: Boolean
) : AsyncEntityProducer, CoroutineScope {
private val waitingForContent = atomic(false)
private val producerJob = Job()
override val coroutineContext: CoroutineContext = callContext + producerJob
@OptIn(DelicateCoroutinesApi::class)
private val channel: ByteReadChannel = when (val body = requestData.body) {
is OutgoingContent.ByteArrayContent -> ByteReadChannel(body.bytes())
is OutgoingContent.ProtocolUpgrade -> throw UnsupportedContentTypeException(body)
is OutgoingContent.NoContent -> ByteReadChannel.Empty
is OutgoingContent.ReadChannelContent -> body.readFrom()
is OutgoingContent.WriteChannelContent -> GlobalScope.writer(callContext, autoFlush = true) {
body.writeTo(channel)
}.channel
}
init {
producerJob.invokeOnCompletion { cause ->
channel.cancel(cause)
}
}
override fun releaseResources() {
channel.cancel()
producerJob.complete()
}
override fun available(): Int = channel.availableForRead
override fun produce(channel: DataStreamChannel) {
var result: Int
do {
result = this.channel.readAvailable { buffer: ByteBuffer ->
channel.write(buffer)
}
} while (result > 0)
if (this.channel.isClosedForRead) {
channel.endStream()
return
}
if (result == -1 && !waitingForContent.getAndSet(true)) {
launch(Dispatchers.Unconfined) {
try {
this@ApacheRequestEntityProducer.channel.awaitContent()
} finally {
waitingForContent.value = false
channel.requestOutput()
}
}
}
}
override fun getContentLength(): Long = contentLength
override fun getContentType(): String? = contentType
override fun getContentEncoding(): String? = null
override fun isChunked(): Boolean = isChunked
override fun getTrailerNames(): Set<String> = emptySet()
override fun isRepeatable(): Boolean = false
override fun failed(cause: Exception) {
val mappedCause = mapCause(cause, requestData)
channel.cancel(mappedCause)
producerJob.completeExceptionally(mappedCause)
}
}
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.util.*
import io.ktor.utils.io.*
import io.ktor.utils.io.pool.*
import kotlinx.atomicfu.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import org.apache.hc.client5.http.async.methods.*
import org.apache.hc.core5.concurrent.*
import org.apache.hc.core5.function.*
import org.apache.hc.core5.http.*
import org.apache.hc.core5.http.HttpResponse
import org.apache.hc.core5.http.nio.*
import org.apache.hc.core5.http.nio.entity.*
import org.apache.hc.core5.http.protocol.*
import org.apache.hc.core5.util.*
import java.io.*
import java.nio.*
import java.util.concurrent.atomic.*
import kotlin.coroutines.*
private object CloseChannel
internal class BasicResponseConsumer(private val dataConsumer: ApacheResponseConsumer) :
AsyncResponseConsumer<Unit> {
internal val responseDeferred = CompletableDeferred<HttpResponse>()
override fun consumeResponse(
response: HttpResponse,
entityDetails: EntityDetails?,
httpContext: HttpContext,
resultCallback: FutureCallback<Unit>
) {
responseDeferred.complete(response)
if (entityDetails != null) {
dataConsumer.streamStart(
entityDetails,
object : CallbackContribution<Unit>(resultCallback) {
override fun completed(body: Unit) {
resultCallback.completed(Unit)
}
}
)
} else {
dataConsumer.close()
resultCallback.completed(Unit)
}
}
override fun informationResponse(response: HttpResponse, httpContext: HttpContext) {
}
override fun updateCapacity(capacityChannel: CapacityChannel) {
dataConsumer.updateCapacity(capacityChannel)
}
override fun consume(src: ByteBuffer) {
dataConsumer.consume(src)
}
override fun streamEnd(trailers: List<Header>?) {
dataConsumer.streamEnd(trailers)
}
override fun failed(cause: Exception) {
responseDeferred.completeExceptionally(cause)
dataConsumer.failed(cause)
}
override fun releaseResources() {
dataConsumer.releaseResources()
}
}
@OptIn(InternalCoroutinesApi::class)
internal class ApacheResponseConsumer(
parentContext: CoroutineContext,
private val requestData: HttpRequestData
) : AsyncEntityConsumer<Unit>, CoroutineScope {
private val consumerJob = Job(parentContext[Job])
override val coroutineContext: CoroutineContext = parentContext + consumerJob
private val channel = ByteChannel().also {
it.attachJob(consumerJob)
}
@Volatile
private var capacityChannel: CapacityChannel? = null
private val messagesQueue = Channel<Any>(capacity = UNLIMITED)
internal val responseChannel: ByteReadChannel = channel
init {
coroutineContext[Job]?.invokeOnCompletion(onCancelling = true) { cause ->
if (cause != null) {
responseChannel.cancel(cause)
}
}
launch(coroutineContext) {
for (message in messagesQueue) {
when (message) {
is CloseChannel -> close()
is ByteBuffer -> {
val remaining = message.remaining()
channel.writeFully(message)
capacityChannel?.update(remaining)
}
else -> throw IllegalStateException("Unknown message $message")
}
}
}
}
override fun releaseResources() {
messagesQueue.close()
}
override fun updateCapacity(capacityChannel: CapacityChannel) {
if (this.capacityChannel == null) {
this.capacityChannel = capacityChannel
capacityChannel.update(channel.availableForWrite)
}
}
override fun consume(src: ByteBuffer) {
if (channel.isClosedForWrite) {
channel.closedCause?.let { throw it }
}
messagesQueue.trySend(src.copy())
}
override fun streamEnd(trailers: List<Header>?) {
messagesQueue.trySend(CloseChannel)
}
override fun streamStart(entityDetails: EntityDetails, resultCallback: FutureCallback<Unit>) {}
override fun failed(cause: Exception) {
val mappedCause = mapCause(cause, requestData)
consumerJob.completeExceptionally(mappedCause)
responseChannel.cancel(mappedCause)
}
internal fun close() {
channel.close()
consumerJob.complete()
}
override fun getContent() = Unit
}
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import java.net.*
/**
* Checks the message of the exception and identifies timeout exception by it.
*/
internal fun ConnectException.isTimeoutException() = message?.contains("Timeout connecting") ?: false
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %thread %-5level %logger{36} %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache" level="ERROR"/>
<logger name="httpclient" level="ERROR"/>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
/*
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.client.engine.apache5
import io.ktor.client.tests.*
class Apache5HttpClientTest : HttpClientTest(Apache5)
class Apache5SslOverProxyTest : SslOverProxyTest<Apache5EngineConfig>(Apache5) {
override fun Apache5EngineConfig.disableCertificatePinning() {
this.sslContext = this@Apache5SslOverProxyTest.unsafeSslContext
}
}
......@@ -56,6 +56,7 @@ kotlin.sourceSets {
jvmTest {
dependencies {
api(project(":ktor-client:ktor-client-apache"))
api(project(":ktor-client:ktor-client-apache5"))
runtimeOnly(project(":ktor-client:ktor-client-android"))
runtimeOnly(project(":ktor-client:ktor-client-okhttp"))
if (currentJdk >= 11) {
......
......@@ -459,7 +459,7 @@ class HttpTimeoutTest : ClientLoader() {
}
@Test
fun testSocketTimeoutReadPerRequestAttributes() = clientTests(listOf("Js", "native:CIO", "Java")) {
fun testSocketTimeoutReadPerRequestAttributes() = clientTests(listOf("Js", "native:CIO", "Java", "Apache5")) {
config {
install(HttpTimeout)
}
......@@ -490,7 +490,7 @@ class HttpTimeoutTest : ClientLoader() {
@Test
fun testSocketTimeoutWriteFailOnWritePerRequestAttributes() = clientTests(
listOf("Js", "Android", "native:CIO", "Java")
listOf("Js", "Android", "Apache5", "native:CIO", "Java")
) {
config {
install(HttpTimeout)
......
......@@ -4,10 +4,8 @@
package io.ktor.client.tests
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.client.tests.utils.*
import io.ktor.http.*
import kotlin.test.*
......
......@@ -17,7 +17,7 @@ import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlin.test.*
internal val ENGINES_WITHOUT_WS = listOf("Android", "Apache", "Curl", "DarwinLegacy")
internal val ENGINES_WITHOUT_WS = listOf("Android", "Apache", "Apache5", "Curl", "DarwinLegacy")
private const val TEST_SIZE: Int = 100
......@@ -111,7 +111,7 @@ class WebSocketTest : ClientLoader() {
}
@Test
fun testWebsocketWithDefaultRequest() = clientTests(ENGINES_WITHOUT_WS) {
fun testWebsocketWithDefaultRequest() = clientTests(ENGINES_WITHOUT_WS + "Js") {
config {
install(WebSockets)
defaultRequest {
......@@ -142,7 +142,7 @@ class WebSocketTest : ClientLoader() {
}
@Test
fun testExceptionWss() = clientTests(listOf("Android", "Apache", "Curl", "JS", "DarwinLegacy")) {
fun testExceptionWss() = clientTests(ENGINES_WITHOUT_WS + "Js") {
config {
install(WebSockets)
}
......
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %thread %-5level %logger{36} %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache" level="ERROR"/>
<logger name="httpclient" level="ERROR"/>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
......@@ -14,7 +14,7 @@ private const val TEST_SIZE: Int = 100
class WebSocketJvmTest : ClientLoader(100000) {
@Test
fun testWebSocketDeflateBinary() = clientTests(listOf("Android", "Apache")) {
fun testWebSocketDeflateBinary() = clientTests(listOf("Android", "Apache", "Apache5")) {
config {
WebSockets {
extensions {
......@@ -38,7 +38,7 @@ class WebSocketJvmTest : ClientLoader(100000) {
}
@Test
fun testWebSocketDeflateNoContextTakeover() = clientTests(listOf("Android", "Apache")) {
fun testWebSocketDeflateNoContextTakeover() = clientTests(listOf("Android", "Apache", "Apache5")) {
config {
WebSockets {
extensions {
......
......@@ -62,6 +62,7 @@ include(":ktor-client")
include(":ktor-client:ktor-client-core")
include(":ktor-client:ktor-client-tests")
include(":ktor-client:ktor-client-apache")
include(":ktor-client:ktor-client-apache5")
include(":ktor-client:ktor-client-android")
include(":ktor-client:ktor-client-cio")
if (native_targets_enabled) {
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать