From 1027b9707b6c2ca7b860b9c3793d576697178495 Mon Sep 17 00:00:00 2001 From: "Aleksei.Tirman" Date: Fri, 20 Dec 2024 13:22:11 +0200 Subject: [PATCH] Add Gzip response tests --- .../io/ktor/client/plugins/logging/Logging.kt | 37 ++++- .../client/plugins/logging/NewFormatTest.kt | 127 ++++++++++++------ 2 files changed, 119 insertions(+), 45 deletions(-) diff --git a/ktor-client/ktor-client-plugins/ktor-client-logging/common/src/io/ktor/client/plugins/logging/Logging.kt b/ktor-client/ktor-client-plugins/ktor-client-logging/common/src/io/ktor/client/plugins/logging/Logging.kt index d4ef1bbcdb..767ce78f9d 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-logging/common/src/io/ktor/client/plugins/logging/Logging.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-logging/common/src/io/ktor/client/plugins/logging/Logging.kt @@ -211,11 +211,11 @@ public val Logging: ClientPlugin = createClientPlugin("Logging", response.headers[HttpHeaders.TransferEncoding] == "chunked" && (isInfo() || isHeaders()) -> "<-- ${response.status} ${request.url.pathQuery()} (${duration}ms, unknown-byte body)" + isInfo() && contentLength != null -> "<-- ${response.status} ${request.url.pathQuery()} (${duration}ms, $contentLength-byte body)" + isBody() || (isInfo() && contentLength == null) || (isHeaders() && contentLength != null) || (response.headers[HttpHeaders.ContentEncoding] == "gzip") -> "<-- ${response.status} ${request.url.pathQuery()} (${duration}ms)" - isInfo() && contentLength != null -> "<-- ${response.status} ${request.url.pathQuery()} (${duration}ms, $contentLength-byte body)" - else -> "<-- ${response.status} ${request.url.pathQuery()} (${duration}ms, unknown-byte body)" } @@ -241,9 +241,36 @@ public val Logging: ClientPlugin = createClientPlugin("Logging", val (origChannel, newChannel) = response.rawContent.split(response) logger.log("") - val text = newChannel.readRemaining().readText() - logger.log(text) - logger.log("<-- END HTTP (${duration}ms, ${text.length}-byte body)") + + val contentType = response.contentType() + + val charset = if (contentType != null) { + contentType.charset() ?: Charsets.UTF_8 + } else { + Charsets.UTF_8 + } + + var isBinary = false + val text = try { + charset.newDecoder().decode(newChannel.readRemaining()) + } catch (_: MalformedInputException) { + isBinary = true + "" + } + + for (ch in text) { + if (ch == '\ufffd') { + isBinary = true + break + } + } + + if (!isBinary) { + logger.log(text) + logger.log("<-- END HTTP (${duration}ms, ${text.length}-byte body)") + } else { + logger.log("<-- END HTTP (${duration}ms, binary $contentLength-byte body omitted)") + } return object : HttpResponse() { override val call: HttpClientCall diff --git a/ktor-client/ktor-client-plugins/ktor-client-logging/jvm/test/io/ktor/client/plugins/logging/NewFormatTest.kt b/ktor-client/ktor-client-plugins/ktor-client-logging/jvm/test/io/ktor/client/plugins/logging/NewFormatTest.kt index 329c37506f..3a563f917e 100644 --- a/ktor-client/ktor-client-plugins/ktor-client-logging/jvm/test/io/ktor/client/plugins/logging/NewFormatTest.kt +++ b/ktor-client/ktor-client-plugins/ktor-client-logging/jvm/test/io/ktor/client/plugins/logging/NewFormatTest.kt @@ -246,6 +246,21 @@ class NewFormatTest { } } + @Test + fun basicGzippedBody() = testWithLevel(LogLevel.INFO, handle = { + val channel = GZipEncoder.encode(ByteReadChannel("a".repeat(1024))) + respond(channel, headers = Headers.build { + append(HttpHeaders.ContentEncoding, "gzip") + append(HttpHeaders.ContentLength, "29") + }) + }) { client -> + client.prepareGet("/").execute { response -> + log.assertLogEqual("--> GET /") + .assertLogMatch(Regex("""<-- 200 OK / \(\d+ms, 29-byte body\)""")) + .assertNoMoreLogs() + } + } + @Test fun basicGzippedBodyContentEncoding() = runTest { HttpClient(MockEngine) { @@ -389,6 +404,28 @@ class NewFormatTest { .assertNoMoreLogs() } + @Test + fun headersGzippedResponseBody() = testWithLevel(LogLevel.HEADERS, handle = { + val content = "a".repeat(1024) + val channel = GZipEncoder.encode(ByteReadChannel(content)) + respond(channel, headers = Headers.build { + append(HttpHeaders.ContentEncoding, "gzip") + append(HttpHeaders.ContentLength, "29") + }) + }) { client -> + client.get("/") + + log.assertLogEqual("--> GET /") + .assertLogEqual("Accept-Charset: UTF-8") + .assertLogEqual("Accept: */*") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK / \(\d+ms\)""")) + .assertLogEqual("Content-Encoding: gzip") + .assertLogEqual("Content-Length: 29") + .assertLogEqual("<-- END HTTP") + .assertNoMoreLogs() + } + @Test fun headersGzippedResponseBodyContentEncoding() = runTest { HttpClient(MockEngine) { @@ -419,48 +456,58 @@ class NewFormatTest { } } -// @Test -// fun bodyWithGzip() = runTest { -// HttpClient(MockEngine) { -// install(Logging) { -// level = LogLevel.BODY -// logger = log -// standardFormat = true -// } -// install(ContentEncoding) { gzip() } -// -// engine { -// addHandler { -// val channel = GZipEncoder.encode(ByteReadChannel("response".repeat(1024))) -// -// respond(channel, headers = Headers.build { append(HttpHeaders.ContentEncoding, "gzip") }) -// } -// } -// }.use { client -> -// client.post("/") { -// val channel = GZipEncoder.encode(ByteReadChannel("request".repeat(1024))) -// header(HttpHeaders.ContentEncoding, "gzip") -// setBody(channel) -// } -// -// log.assertLogEqual("--> POST /") -// .assertLogEqual("Content-Type: application/octet-stream") -// .assertLogEqual("Content-Encoding: gzip") -// .assertLogEqual("Accept-Encoding: gzip") -// .assertLogEqual("Accept-Charset: UTF-8") -// .assertLogEqual("Accept: */*") -// .assertLogEqual("") -// .assertLogEqual("request".repeat(1024)) -// .assertLogEqual("--> END POST (7168-byte, gzipped)") -// .assertLogMatch(Regex("""<-- 200 OK / \(\d+ms\)""")) -// .assertLogEqual("") -// .assertLogEqual("response".repeat(1024)) -// .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 111-byte body, gzipped\)""")) -// .assertNoMoreLogs() -// } -// } + @Test + fun bodyGzippedResponseBody() = testWithLevel(LogLevel.BODY, handle = { + val channel = GZipEncoder.encode(ByteReadChannel("response".repeat(1024))) + respond(channel, headers = Headers.build { + append(HttpHeaders.ContentEncoding, "gzip") + append(HttpHeaders.ContentLength, "55") + }) + }) { client -> + client.get("/") + log.assertLogEqual("--> GET /") + .assertLogEqual("Accept-Charset: UTF-8") + .assertLogEqual("Accept: */*") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK / \(\d+ms\)""")) + .assertLogEqual("Content-Encoding: gzip") + .assertLogEqual("Content-Length: 55") + .assertLogEqual("") + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, binary 55-byte body omitted\)""")) + .assertNoMoreLogs() + } + + @Test + fun bodyGzippedResponseBodyContentEncoding() = runTest { + HttpClient(MockEngine) { + install(Logging) { + level = LogLevel.BODY + logger = log + standardFormat = true + } + install(ContentEncoding) { gzip() } + engine { + addHandler { + val channel = GZipEncoder.encode(ByteReadChannel("response".repeat(1024))) + respond(channel, headers = Headers.build { append(HttpHeaders.ContentEncoding, "gzip") }) + } + } + }.use { client -> + client.get("/") + log.assertLogEqual("--> GET /") + .assertLogEqual("Accept-Encoding: gzip") + .assertLogEqual("Accept-Charset: UTF-8") + .assertLogEqual("Accept: */*") + .assertLogEqual("--> END GET") + .assertLogMatch(Regex("""<-- 200 OK / \(\d+ms\)""")) + .assertLogEqual("") + .assertLogEqual("response".repeat(1024)) + .assertLogMatch(Regex("""<-- END HTTP \(\d+ms, 8192-byte body\)""")) + .assertNoMoreLogs() + } + } @Test fun bodyGet() = testWithLevel(LogLevel.BODY, handle = { respondWithLength() }) { client ->