diff --git a/ktor-client/ktor-client-core/api/ktor-client-core.api b/ktor-client/ktor-client-core/api/ktor-client-core.api index 002ef34f02..80eb3d759a 100644 --- a/ktor-client/ktor-client-core/api/ktor-client-core.api +++ b/ktor-client/ktor-client-core/api/ktor-client-core.api @@ -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 { diff --git a/ktor-client/ktor-client-core/api/ktor-client-core.klib.api b/ktor-client/ktor-client-core/api/ktor-client-core.klib.api index f859311741..abb85dfa08 100644 --- a/ktor-client/ktor-client-core/api/ktor-client-core.klib.api +++ b/ktor-client/ktor-client-core/api/ktor-client-core.klib.api @@ -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.content/OutgoingContent // io.ktor.client.utils/wrapHeaders|wrapHeaders@io.ktor.http.content.OutgoingContent(kotlin.Function1){}[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|fillDefaults@io.ktor.http.Cookie(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|matches@io.ktor.http.Cookie(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|dropCompressionHeaders@io.ktor.http.HeadersBuilder(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|dropCompressionHeaders@io.ktor.http.HeadersBuilder(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|accept@io.ktor.http.HttpMessageBuilder(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|basicAuth@io.ktor.http.HttpMessageBuilder(kotlin.String;kotlin.String){}[0] final fun (io.ktor.http/HttpMessageBuilder).io.ktor.client.request/bearerAuth(kotlin/String) // io.ktor.client.request/bearerAuth|bearerAuth@io.ktor.http.HttpMessageBuilder(kotlin.String){}[0] diff --git a/ktor-client/ktor-client-core/common/src/io/ktor/client/utils/HeadersUtils.kt b/ktor-client/ktor-client-core/common/src/io/ktor/client/utils/HeadersUtils.kt index 1b78fb2e00..b48e3817e6 100644 --- a/ktor-client/ktor-client-core/common/src/io/ktor/client/utils/HeadersUtils.kt +++ b/ktor-client/ktor-client-core/common/src/io/ktor/client/utils/HeadersUtils.kt @@ -15,10 +15,16 @@ private val DecompressionListAttribute: AttributeKey> = 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() }.add(header) + when (val header = get(HttpHeaders.ContentEncoding)) { + null -> if (!alwaysRemove) return + else -> attributes.computeIfAbsent(DecompressionListAttribute) { mutableListOf() }.add(header) + } remove(HttpHeaders.ContentEncoding) remove(HttpHeaders.ContentLength) } diff --git a/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/JsClientEngine.kt b/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/JsClientEngine.kt index cb7c413e06..87b52e43dc 100644 --- a/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/JsClientEngine.kt +++ b/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/JsClientEngine.kt @@ -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 @@ -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( @@ -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] @@ -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) diff --git a/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/compatibility/Utils.kt b/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/compatibility/Utils.kt index 72cc8952d8..0e8afe66f8 100644 --- a/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/compatibility/Utils.kt +++ b/ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/compatibility/Utils.kt @@ -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 @@ -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 = when { PlatformUtils.IS_BROWSER -> fetch(input, init) else -> { @@ -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 diff --git a/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/WasmJsClientEngine.kt b/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/WasmJsClientEngine.kt index 1a1e606777..de007d750b 100644 --- a/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/WasmJsClientEngine.kt +++ b/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/WasmJsClientEngine.kt @@ -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 @@ -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 @@ -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 + ) } /** diff --git a/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/compatibility/Utils.kt b/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/compatibility/Utils.kt index 064a13c0a3..579c5eab7d 100644 --- a/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/compatibility/Utils.kt +++ b/ktor-client/ktor-client-core/wasmJs/src/io/ktor/client/engine/js/compatibility/Utils.kt @@ -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 @@ -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 = when { PlatformUtils.IS_BROWSER -> fetch(input, init) else -> { @@ -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 = diff --git a/ktor-client/ktor-client-test-base/common/src/io/ktor/client/test/base/ClientLoader.kt b/ktor-client/ktor-client-test-base/common/src/io/ktor/client/test/base/ClientLoader.kt index 0cd15f4b92..b8925b6997 100644 --- a/ktor-client/ktor-client-test-base/common/src/io/ktor/client/test/base/ClientLoader.kt +++ b/ktor-client/ktor-client-test-base/common/src/io/ktor/client/test/base/ClientLoader.kt @@ -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()) diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentEncodingIntegrationTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentEncodingIntegrationTest.kt index 29e2f9e3c8..550613bb02 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentEncodingIntegrationTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ContentEncodingIntegrationTest.kt @@ -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") { diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingMockedTests.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingMockedTests.kt index 7a77129982..07cdbbcccb 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingMockedTests.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingMockedTests.kt @@ -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", @@ -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]", @@ -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", @@ -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]", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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") @@ -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: ***") diff --git a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt index d6e3e04b2e..3529e9ce87 100644 --- a/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt +++ b/ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/LoggingTest.kt @@ -56,13 +56,13 @@ class LoggingTest : ClientLoader() { fun testLoggingLevelBody() = clientTests(except("native:CIO")) { val logger = TestLogger( "REQUEST: http://localhost:8080/logging", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "BODY Content-Type: null", "BODY START", "", "BODY END", "RESPONSE: 200 OK", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "FROM: http://localhost:8080/logging", "BODY Content-Type: text/plain; charset=UTF-8", "BODY START", @@ -73,10 +73,10 @@ class LoggingTest : ClientLoader() { } @Test - fun testLogLevelAll() = clientTests(except("native:CIO")) { + fun testLogLevelAll() = clientTests(except("native:CIO", "web:Js")) { val logger = TestLogger( "REQUEST: http://localhost:8080/logging", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "COMMON HEADERS", "-> Accept: */*", "-> Accept-Charset: UTF-8", @@ -87,7 +87,7 @@ class LoggingTest : ClientLoader() { "", "BODY END", "RESPONSE: 200 OK", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "FROM: http://localhost:8080/logging", "COMMON HEADERS", "???-> Connection: keep-alive", @@ -103,17 +103,17 @@ class LoggingTest : ClientLoader() { } @Test - fun testLogLevelHeaders() = clientTests { + fun testLogLevelHeaders() = clientTests(except("web:Js")) { val logger = TestLogger { line("REQUEST: http://localhost:8080/logging") - line("METHOD: HttpMethod(value=GET)") + line("METHOD: GET") line("COMMON HEADERS") line("-> Accept: */*") line("-> Accept-Charset: UTF-8") line("CONTENT HEADERS") line("-> Content-Length: 0") line("RESPONSE: 200 OK") - line("METHOD: HttpMethod(value=GET)") + line("METHOD: GET") line("FROM: http://localhost:8080/logging") line("COMMON HEADERS") optional("-> Connection: close") @@ -128,9 +128,9 @@ class LoggingTest : ClientLoader() { fun testLogLevelInfo() = clientTests { val logger = TestLogger( "REQUEST: http://localhost:8080/logging", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "RESPONSE: 200 OK", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "FROM: http://localhost:8080/logging" ) checkLog(logger, HttpMethod.Get, "", null, LogLevel.INFO) @@ -143,10 +143,10 @@ class LoggingTest : ClientLoader() { } @Test - fun testLogPostBody() = clientTests(except("native:CIO")) { + fun testLogPostBody() = clientTests(except("native:CIO", "web:Js")) { val testLogger = TestLogger( "REQUEST: http://localhost:8080/logging", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "COMMON HEADERS", "-> Accept: */*", "-> Accept-Charset: UTF-8", @@ -158,7 +158,7 @@ class LoggingTest : ClientLoader() { content, "BODY END", "RESPONSE: 201 Created", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "FROM: http://localhost:8080/logging", "COMMON HEADERS", "???-> Connection: close", @@ -199,10 +199,10 @@ class LoggingTest : ClientLoader() { } @Test - fun testLogPostMalformedUtf8Body() = clientTests(except("native:CIO")) { + fun testLogPostMalformedUtf8Body() = clientTests(except("native:CIO", "web:Js")) { val testLogger = TestLogger( "REQUEST: http://localhost:8080/logging/non-utf", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "COMMON HEADERS", "-> Accept: */*", "-> Accept-Charset: UTF-8", @@ -214,7 +214,7 @@ class LoggingTest : ClientLoader() { "�o", "BODY END", "RESPONSE: 201 Created", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "FROM: http://localhost:8080/logging/non-utf", "COMMON HEADERS", "???-> Connection: close", @@ -255,10 +255,10 @@ class LoggingTest : ClientLoader() { } @Test - fun testRequestAndResponseBody() = clientTests(except("native:CIO")) { + fun testRequestAndResponseBody() = clientTests(except("native:CIO", "web:Js")) { val testLogger = TestLogger( "REQUEST: http://127.0.0.1:8080/content/echo", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "COMMON HEADERS", "-> Accept: */*", "-> Accept-Charset: UTF-8", @@ -270,7 +270,7 @@ class LoggingTest : ClientLoader() { "test", "BODY END", "RESPONSE: 200 OK", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "FROM: http://127.0.0.1:8080/content/echo", "COMMON HEADERS", "???-> Connection: close", @@ -305,10 +305,10 @@ class LoggingTest : ClientLoader() { } @Test - fun testRequestContentTypeInLog() = clientTests(except("Darwin", "native:CIO", "DarwinLegacy")) { + fun testRequestContentTypeInLog() = clientTests(except("Darwin", "native:CIO", "DarwinLegacy", "web:Js")) { val testLogger = TestLogger( "REQUEST: http://127.0.0.1:8080/content/echo", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "COMMON HEADERS", "-> Accept: */*", "-> Accept-Charset: UTF-8", @@ -320,7 +320,7 @@ class LoggingTest : ClientLoader() { "test", "BODY END", "RESPONSE: 200 OK", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "FROM: http://127.0.0.1:8080/content/echo", "COMMON HEADERS", "???-> Connection: keep-alive", @@ -357,10 +357,10 @@ class LoggingTest : ClientLoader() { } @Test - fun testLoggingWithCompression() = clientTests(except("Darwin", "DarwinLegacy", "native:CIO", "web:CIO")) { + fun testLoggingWithCompression() = clientTests(except("Darwin", "DarwinLegacy", "native:CIO", "web:*")) { val testLogger = TestLogger( "REQUEST: http://127.0.0.1:8080/compression/deflate", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "COMMON HEADERS", "-> Accept: */*", "-> Accept-Charset: UTF-8", @@ -372,7 +372,7 @@ class LoggingTest : ClientLoader() { "", "BODY END", "RESPONSE: 200 OK", - "METHOD: HttpMethod(value=GET)", + "METHOD: GET", "FROM: http://127.0.0.1:8080/compression/deflate", "COMMON HEADERS", "???-> Connection: keep-alive", @@ -409,7 +409,7 @@ class LoggingTest : ClientLoader() { fun testLoggingWithStreaming() = clientTests { val testLogger = TestLogger( "REQUEST: http://127.0.0.1:8080/content/echo", - "METHOD: HttpMethod(value=POST)" + "METHOD: POST" ) config { Logging { @@ -504,10 +504,10 @@ class LoggingTest : ClientLoader() { data class User(val name: String) @Test - fun testLogPostBodyWithJson() = clientTests(retries = 5) { + fun testLogPostBodyWithJson() = clientTests(except("web:Js"), retries = 5) { val testLogger = TestLogger( "REQUEST: http://127.0.0.1:8080/content/echo", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "COMMON HEADERS", "-> Accept: application/json", "-> Accept-Charset: UTF-8", @@ -519,7 +519,7 @@ class LoggingTest : ClientLoader() { "{\"name\":\"Ktor\"}", "BODY END", "RESPONSE: 200 OK", - "METHOD: HttpMethod(value=POST)", + "METHOD: POST", "FROM: http://127.0.0.1:8080/content/echo", "COMMON HEADERS", "???-> connection: keep-alive", diff --git a/ktor-http/common/src/io/ktor/http/HttpMethod.kt b/ktor-http/common/src/io/ktor/http/HttpMethod.kt index c6a7eca9f3..9e5cdf0b1b 100644 --- a/ktor-http/common/src/io/ktor/http/HttpMethod.kt +++ b/ktor-http/common/src/io/ktor/http/HttpMethod.kt @@ -9,6 +9,8 @@ package io.ktor.http * @property value contains method name */ public data class HttpMethod(val value: String) { + override fun toString(): String = value + @Suppress("KDocMissingDocumentation", "PublicApiImplicitType") public companion object { public val Get: HttpMethod = HttpMethod("GET") diff --git a/ktor-http/common/src/io/ktor/http/URLParser.kt b/ktor-http/common/src/io/ktor/http/URLParser.kt index 68d7c2085b..3b03cadaff 100644 --- a/ktor-http/common/src/io/ktor/http/URLParser.kt +++ b/ktor-http/common/src/io/ktor/http/URLParser.kt @@ -136,6 +136,10 @@ internal fun URLBuilder.takeFromUnsafe(urlString: String): URLBuilder { private fun URLBuilder.parseFile(urlString: String, startIndex: Int, endIndex: Int, slashCount: Int) { when (slashCount) { + 1 -> { + host = "" + encodedPath = urlString.substring(startIndex, endIndex) + } 2 -> { val nextSlash = urlString.indexOf('/', startIndex) if (nextSlash == -1 || nextSlash == endIndex) { diff --git a/ktor-http/common/test/io/ktor/tests/http/UrlTest.kt b/ktor-http/common/test/io/ktor/tests/http/UrlTest.kt index bbd31fae2d..068225ea4a 100644 --- a/ktor-http/common/test/io/ktor/tests/http/UrlTest.kt +++ b/ktor-http/common/test/io/ktor/tests/http/UrlTest.kt @@ -264,6 +264,15 @@ class UrlTest { assertEquals(expectedUrl, result.toString()) } + @Test + fun testForFileProtocolMinimalRepresentation() { + val result = Url("file:/var/www") + assertEquals("file", result.protocol.name) + assertEquals("", result.host) + assertEquals(listOf("var", "www"), result.rawSegments) + assertEquals("file:///var/www", result.toString()) + } + @Test fun testForMailProtocol() { val expectedUrl = "mailto:abc@xyz.io"