Skip to content

Commit

Permalink
Merge branch 'main' into bjhham/eof-exception
Browse files Browse the repository at this point in the history
  • Loading branch information
bjhham authored Jan 29, 2025
2 parents 112c920 + 15f0921 commit 9add2a6
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 79 deletions.
3 changes: 2 additions & 1 deletion ktor-client/ktor-client-core/api/ktor-client-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1557,7 +1557,8 @@ public final class io/ktor/client/utils/HeadersKt {
}

public final class io/ktor/client/utils/HeadersUtilsKt {
public static final fun dropCompressionHeaders (Lio/ktor/http/HeadersBuilder;Lio/ktor/http/HttpMethod;Lio/ktor/util/Attributes;)V
public static final fun dropCompressionHeaders (Lio/ktor/http/HeadersBuilder;Lio/ktor/http/HttpMethod;Lio/ktor/util/Attributes;Z)V
public static synthetic fun dropCompressionHeaders$default (Lio/ktor/http/HeadersBuilder;Lio/ktor/http/HttpMethod;Lio/ktor/util/Attributes;ZILjava/lang/Object;)V
}

public final class io/ktor/client/utils/HttpResponseReceiveFail {
Expand Down
2 changes: 1 addition & 1 deletion ktor-client/ktor-client-core/api/ktor-client-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,7 @@ final fun (io.ktor.client/HttpClientConfig<*>).io.ktor.client.plugins/defaultReq
final fun (io.ktor.http.content/OutgoingContent).io.ktor.client.utils/wrapHeaders(kotlin/Function1<io.ktor.http/Headers, io.ktor.http/Headers>): io.ktor.http.content/OutgoingContent // io.ktor.client.utils/wrapHeaders|[email protected](kotlin.Function1<io.ktor.http.Headers,io.ktor.http.Headers>){}[0]
final fun (io.ktor.http/Cookie).io.ktor.client.plugins.cookies/fillDefaults(io.ktor.http/Url): io.ktor.http/Cookie // io.ktor.client.plugins.cookies/fillDefaults|[email protected](io.ktor.http.Url){}[0]
final fun (io.ktor.http/Cookie).io.ktor.client.plugins.cookies/matches(io.ktor.http/Url): kotlin/Boolean // io.ktor.client.plugins.cookies/matches|[email protected](io.ktor.http.Url){}[0]
final fun (io.ktor.http/HeadersBuilder).io.ktor.client.utils/dropCompressionHeaders(io.ktor.http/HttpMethod, io.ktor.util/Attributes) // io.ktor.client.utils/dropCompressionHeaders|[email protected](io.ktor.http.HttpMethod;io.ktor.util.Attributes){}[0]
final fun (io.ktor.http/HeadersBuilder).io.ktor.client.utils/dropCompressionHeaders(io.ktor.http/HttpMethod, io.ktor.util/Attributes, kotlin/Boolean = ...) // io.ktor.client.utils/dropCompressionHeaders|[email protected](io.ktor.http.HttpMethod;io.ktor.util.Attributes;kotlin.Boolean){}[0]
final fun (io.ktor.http/HttpMessageBuilder).io.ktor.client.request/accept(io.ktor.http/ContentType) // io.ktor.client.request/accept|[email protected](io.ktor.http.ContentType){}[0]
final fun (io.ktor.http/HttpMessageBuilder).io.ktor.client.request/basicAuth(kotlin/String, kotlin/String) // io.ktor.client.request/basicAuth|[email protected](kotlin.String;kotlin.String){}[0]
final fun (io.ktor.http/HttpMessageBuilder).io.ktor.client.request/bearerAuth(kotlin/String) // io.ktor.client.request/bearerAuth|[email protected](kotlin.String){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ private val DecompressionListAttribute: AttributeKey<MutableList<String>> = Attr
* (like js and Curl) to make sure all the plugins and checks work with the correct content length and encoding.
*/
@InternalAPI
public fun HeadersBuilder.dropCompressionHeaders(method: HttpMethod, attributes: Attributes) {
public fun HeadersBuilder.dropCompressionHeaders(
method: HttpMethod,
attributes: Attributes,
alwaysRemove: Boolean = false,
) {
if (method == HttpMethod.Head || method == HttpMethod.Options) return
val header = get(HttpHeaders.ContentEncoding) ?: return
attributes.computeIfAbsent(DecompressionListAttribute) { mutableListOf<String>() }.add(header)
when (val header = get(HttpHeaders.ContentEncoding)) {
null -> if (!alwaysRemove) return
else -> attributes.computeIfAbsent(DecompressionListAttribute) { mutableListOf<String>() }.add(header)
}
remove(HttpHeaders.ContentEncoding)
remove(HttpHeaders.ContentLength)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.js
Expand All @@ -16,9 +16,11 @@ import io.ktor.util.*
import io.ktor.util.date.*
import io.ktor.utils.io.*
import kotlinx.coroutines.*
import org.w3c.dom.*
import org.w3c.dom.events.*
import kotlin.coroutines.*
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.js.Promise

internal class JsClientEngine(
Expand All @@ -31,7 +33,7 @@ internal class JsClientEngine(
check(config.proxy == null) { "Proxy unsupported in Js engine." }
}

@OptIn(InternalAPI::class, InternalCoroutinesApi::class)
@OptIn(InternalAPI::class)
override suspend fun execute(data: HttpRequestData): HttpResponseData {
val callContext = callContext()
val clientConfig = data.attributes[CLIENT_CONFIG]
Expand All @@ -42,14 +44,7 @@ internal class JsClientEngine(

val requestTime = GMTDate()
val rawRequest = data.toRaw(clientConfig, callContext)

val controller = AbortController()
rawRequest.signal = controller.signal
callContext.job.invokeOnCompletion(onCancelling = true) {
controller.abort()
}

val rawResponse = commonFetch(data.url.toString(), rawRequest, config)
val rawResponse = commonFetch(data.url.toString(), rawRequest, config, callContext.job)

val status = HttpStatusCode(rawResponse.status.toInt(), rawResponse.statusText)
val headers = rawResponse.headers.mapToKtor(data.method, data.attributes)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.js.compatibility
Expand All @@ -9,14 +9,26 @@ import io.ktor.client.engine.js.browser.*
import io.ktor.client.fetch.*
import io.ktor.util.*
import io.ktor.utils.io.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.js.Promise

@OptIn(InternalCoroutinesApi::class)
internal suspend fun commonFetch(
input: String,
init: RequestInit,
config: JsClientEngineConfig,
callJob: Job,
): org.w3c.fetch.Response = suspendCancellableCoroutine { continuation ->
val controller = AbortController()
init.signal = controller.signal

val abortOnCallCompletion = callJob.invokeOnCompletion(onCancelling = true) {
controller.abort()
}

val promise: Promise<org.w3c.fetch.Response> = when {
PlatformUtils.IS_BROWSER -> fetch(input, init)
else -> {
Expand All @@ -32,12 +44,10 @@ internal suspend fun commonFetch(
onRejected = {
continuation.resumeWith(Result.failure(Error("Fail to fetch", it)))
}
)
).finally { abortOnCallCompletion.dispose() }
}

internal fun AbortController(): AbortController {
return js("new AbortController()")
}
private fun AbortController(): AbortController = js("new AbortController()")

internal fun CoroutineScope.readBody(
response: org.w3c.fetch.Response
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.js
Expand Down Expand Up @@ -56,13 +56,8 @@ internal class JsClientEngine(

val requestTime = GMTDate()
val rawRequest = data.toRaw(clientConfig, callContext)
val controller = AbortController()
rawRequest.signal = controller.signal
callContext.job.invokeOnCompletion(onCancelling = true) {
controller.abort()
}
val rawResponse = commonFetch(data.url.toString(), rawRequest, config, callContext.job)

val rawResponse = commonFetch(data.url.toString(), rawRequest, config)
val status = HttpStatusCode(rawResponse.status.toInt(), rawResponse.statusText)
val headers = rawResponse.headers.mapToKtor(data.method, data.attributes)
val version = HttpProtocolVersion.HTTP_1_1
Expand Down Expand Up @@ -173,7 +168,13 @@ internal fun org.w3c.fetch.Headers.mapToKtor(method: HttpMethod, attributes: Att
append(key, value)
}

dropCompressionHeaders(method, attributes)
// Content-Encoding is hidden for cross-origin calls,
// so browser requests should always ignore Content-Length
dropCompressionHeaders(
method,
attributes,
alwaysRemove = PlatformUtils.IS_BROWSER
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.js.compatibility
Expand All @@ -10,14 +10,26 @@ import io.ktor.client.fetch.*
import io.ktor.client.utils.*
import io.ktor.util.*
import io.ktor.utils.io.*
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.js.Promise

@OptIn(InternalCoroutinesApi::class)
internal suspend fun commonFetch(
input: String,
init: RequestInit,
config: JsClientEngineConfig,
callJob: Job,
): org.w3c.fetch.Response = suspendCancellableCoroutine { continuation ->
val controller = AbortController()
init.signal = controller.signal

val abortOnCallCompletion = callJob.invokeOnCompletion(onCancelling = true) {
controller.abort()
}

val promise: Promise<org.w3c.fetch.Response> = when {
PlatformUtils.IS_BROWSER -> fetch(input, init)
else -> {
Expand All @@ -40,17 +52,16 @@ internal suspend fun commonFetch(
continuation.resumeWith(Result.failure(Error("Fail to fetch", JsError(it))))
null
}
)
).finally { abortOnCallCompletion.dispose() }
}

private fun abortControllerCtorBrowser(): AbortController =
js("AbortController")

internal fun AbortController(): AbortController {
private fun AbortController(): AbortController {
val ctor = abortControllerCtorBrowser()
return makeJsNew(ctor)
}

private fun abortControllerCtorBrowser(): AbortController = js("AbortController")

internal fun CoroutineScope.readBody(
response: org.w3c.fetch.Response
): ByteReadChannel =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ abstract class ClientLoader(private val timeout: Duration = 1.minutes) {
return EngineSelectionRule { pattern.matches(it) }
}

/** Includes the set of [engines] for the test */
fun only(vararg engines: String): EngineSelectionRule {
val includePatterns = engines.map(EnginePattern::parse)
return EngineSelectionRule { engineName -> includePatterns.any { it.matches(engineName) } }
}

/** Excludes the specified [engines] from test execution. */
fun except(vararg engines: String): EngineSelectionRule = except(engines.asList())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ContentEncodingIntegrationTest : ClientLoader() {

// GZipEncoder is implemented only on JVM.
@Test
fun testGzipWithContentLengthWithoutPlugin() = clientTests(only("jvm:*")) {
fun testGzipWithContentLengthWithoutPlugin() = clientTests(only("jvm:*", "web:Js")) {
test { client ->
val response = client.get("$TEST_URL/gzip-with-content-length")
val content = if (response.headers[HttpHeaders.ContentEncoding] == "gzip") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class LoggingMockedTests {
fun testLogResponseWithException() = testWithEngine(MockEngine, retries = 5) {
val testLogger = TestLogger(
"REQUEST: ${URLBuilder.origin}",
"METHOD: HttpMethod(value=GET)",
"METHOD: GET",
"COMMON HEADERS",
"-> Accept: */*",
"-> Accept-Charset: UTF-8",
Expand All @@ -40,7 +40,7 @@ class LoggingMockedTests {
"",
"BODY END",
"RESPONSE: 200 OK",
"METHOD: HttpMethod(value=GET)",
"METHOD: GET",
"FROM: ${URLBuilder.origin}",
"COMMON HEADERS",
"+++RESPONSE ${URLBuilder.origin} failed with exception: CustomError[PARSE ERROR]",
Expand Down Expand Up @@ -94,7 +94,7 @@ class LoggingMockedTests {
fun testLogResponseWithExceptionSingle() = testWithEngine(MockEngine) {
val testLogger = TestLogger(
"REQUEST: ${URLBuilder.origin}",
"METHOD: HttpMethod(value=GET)",
"METHOD: GET",
"COMMON HEADERS",
"-> Accept: */*",
"-> Accept-Charset: UTF-8",
Expand All @@ -105,7 +105,7 @@ class LoggingMockedTests {
"",
"BODY END",
"RESPONSE: 200 OK",
"METHOD: HttpMethod(value=GET)",
"METHOD: GET",
"FROM: ${URLBuilder.origin}",
"COMMON HEADERS",
"RESPONSE ${URLBuilder.origin} failed with exception: CustomError[PARSE ERROR]",
Expand Down Expand Up @@ -150,7 +150,7 @@ class LoggingMockedTests {
fun testLoggingWithForm() = testWithEngine(MockEngine) {
val testLogger = TestLogger(
"REQUEST: http://localhost/",
"METHOD: HttpMethod(value=POST)",
"METHOD: POST",
"COMMON HEADERS",
"-> Accept: */*",
"-> Accept-Charset: UTF-8",
Expand All @@ -168,7 +168,7 @@ class LoggingMockedTests {
"",
"BODY END",
"RESPONSE: 200 OK",
"METHOD: HttpMethod(value=POST)",
"METHOD: POST",
"FROM: http://localhost/",
"COMMON HEADERS",
"BODY Content-Type: null",
Expand Down Expand Up @@ -219,7 +219,7 @@ class LoggingMockedTests {
fun testFilterRequest() = testWithEngine(MockEngine) {
val testLogger = TestLogger(
"REQUEST: http://somewhere/filtered_path",
"METHOD: HttpMethod(value=GET)",
"METHOD: GET",
"COMMON HEADERS",
"-> Accept: */*",
"-> Accept-Charset: UTF-8",
Expand All @@ -230,7 +230,7 @@ class LoggingMockedTests {
"",
"BODY END",
"RESPONSE: 200 OK",
"METHOD: HttpMethod(value=GET)",
"METHOD: GET",
"FROM: http://somewhere/filtered_path",
"COMMON HEADERS",
"BODY Content-Type: null",
Expand Down Expand Up @@ -264,7 +264,7 @@ class LoggingMockedTests {
fun testSanitizeHeaders() = testWithEngine(MockEngine) {
val testLogger = TestLogger {
line("REQUEST: http://localhost/")
line("METHOD: HttpMethod(value=GET)")
line("METHOD: GET")
line("COMMON HEADERS")
line("-> Accept: */*")
line("-> Accept-Charset: UTF-8")
Expand All @@ -273,7 +273,7 @@ class LoggingMockedTests {
line("CONTENT HEADERS")
line("-> Content-Length: 0")
line("RESPONSE: 200 OK")
line("METHOD: HttpMethod(value=GET)")
line("METHOD: GET")
line("FROM: http://localhost/")
line("COMMON HEADERS")
line("-> Sanitized: ***")
Expand Down
Loading

0 comments on commit 9add2a6

Please sign in to comment.