From d0cca85f5cc293ec6255cd80919694e879c27867 Mon Sep 17 00:00:00 2001 From: Nicola Corti Date: Sat, 25 Jan 2020 18:35:38 +0100 Subject: [PATCH 1/2] Fix for Empty Response Body and 0 Content Lenght (#206) * Rewrite the processResponseBody method as it's broken I've rewrote the processResponseBody following the body of HttpLoggingInterceptor. * Make sure ChuckerInterceptor is after the LoggingInterceptor * Make check for image content type case-insensitive Co-authored-by: Volodymyr Buberenko --- .../chucker/api/ChuckerInterceptor.kt | 77 ++++++++----------- .../chucker/internal/support/IOUtils.kt | 6 +- .../chucker/sample/HttpBinClient.kt | 2 +- 3 files changed, 36 insertions(+), 49 deletions(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt index b33b2fc54..61ec4ff48 100755 --- a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt @@ -1,21 +1,18 @@ package com.chuckerteam.chucker.api import android.content.Context -import android.util.Log -import com.chuckerteam.chucker.api.Chucker.LOG_TAG import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.chuckerteam.chucker.internal.support.IOUtils import com.chuckerteam.chucker.internal.support.hasBody import java.io.IOException import java.nio.charset.Charset -import java.nio.charset.UnsupportedCharsetException import okhttp3.Headers import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody import okio.Buffer -import okio.BufferedSource +import okio.GzipSource /** * An OkHttp Interceptor which persists and displays HTTP activity @@ -108,7 +105,7 @@ class ChuckerInterceptor @JvmOverloads constructor( * Processes a [Response] and populates corresponding fields of a [HttpTransaction]. */ private fun processResponse(response: Response, transaction: HttpTransaction) { - val responseBody = response.body() + val responseBody = response.body()!! val responseEncodingIsSupported = io.bodyHasSupportedEncoding(response.headers().get(CONTENT_ENCODING)) transaction.apply { @@ -123,8 +120,8 @@ class ChuckerInterceptor @JvmOverloads constructor( responseCode = response.code() responseMessage = response.message() - responseContentType = responseBody?.contentType()?.toString() - responseContentLength = responseBody?.contentLength() ?: 0L + responseContentType = responseBody.contentType()?.toString() + responseContentLength = responseBody.contentLength() tookMs = (response.receivedResponseAtMillis() - response.sentRequestAtMillis()) } @@ -137,32 +134,36 @@ class ChuckerInterceptor @JvmOverloads constructor( /** * Processes a [ResponseBody] and populates corresponding fields of a [HttpTransaction]. */ - private fun processResponseBody(response: Response, responseBody: ResponseBody?, transaction: HttpTransaction) { - getNativeSource(response)?.use { source -> - source.request(java.lang.Long.MAX_VALUE) - val buffer = source.buffer() - var charset: Charset = UTF8 - val contentType = responseBody?.contentType() - if (contentType != null) { - try { - charset = contentType.charset(UTF8) ?: UTF8 - } catch (e: UnsupportedCharsetException) { - return - } + private fun processResponseBody(response: Response, responseBody: ResponseBody, transaction: HttpTransaction) { + val contentType = responseBody.contentType() + val charset: Charset = contentType?.charset(UTF8) ?: UTF8 + val contentLength = responseBody.contentLength() + + val source = responseBody.source() + source.request(Long.MAX_VALUE) // Buffer the entire body. + var buffer = source.buffer() + + if (io.bodyIsGzipped(response.headers()[CONTENT_ENCODING])) { + GzipSource(buffer.clone()).use { gzippedResponseBody -> + buffer = Buffer() + buffer.writeAll(gzippedResponseBody) } - if (io.isPlaintext(buffer)) { - val content = io.readFromBuffer(buffer, charset, maxContentLength) - transaction.responseBody = content - } else { - transaction.isResponseBodyPlainText = false + } + + if (io.isPlaintext(buffer)) { + transaction.isResponseBodyPlainText = true + if (contentLength != 0L) { + transaction.responseBody = buffer.clone().readString(charset) + } + } else { + transaction.isResponseBodyPlainText = false - val isImageContentType = (transaction.responseContentType?.contains(CONTENT_TYPE_IMAGE) == true) + val isImageContentType = + (contentType?.toString()?.contains(CONTENT_TYPE_IMAGE, ignoreCase = true) == true) - if (isImageContentType && buffer.size() < MAX_BLOB_SIZE) { - transaction.responseImageData = buffer.readByteArray() - } + if (isImageContentType && buffer.size() < MAX_BLOB_SIZE) { + transaction.responseImageData = buffer.readByteArray() } - transaction.responseContentLength = buffer.size() } } @@ -177,24 +178,6 @@ class ChuckerInterceptor @JvmOverloads constructor( return builder.build() } - /** - * Returns a [BufferedSource] of a [Response] and UnGzip it if necessary. - */ - @Throws(IOException::class) - private fun getNativeSource(response: Response): BufferedSource? { - if (io.bodyIsGzipped(response.headers().get(CONTENT_ENCODING))) { - val source = response.peekBody(maxContentLength).source() - if (source.buffer().size() < maxContentLength) { - return io.getNativeSource(source, true) - } else { - Log.w(LOG_TAG, "gzip encoded response was too long") - } - } - // Let's clone the response Buffer in order to don't cause an IllegalStateException: closed - // if others interceptors are manipulating the body (see #192). - return response.body()?.source()?.buffer()?.clone() - } - companion object { private val UTF8 = Charset.forName("UTF-8") diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/IOUtils.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/IOUtils.kt index 50d947b88..b5422d3b8 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/IOUtils.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/IOUtils.kt @@ -67,5 +67,9 @@ internal class IOUtils(private val context: Context) { contentEncoding.equals("identity", ignoreCase = true) || contentEncoding.equals("gzip", ignoreCase = true) - fun bodyIsGzipped(contentEncoding: String?) = contentEncoding?.equals("gzip", ignoreCase = true) ?: false + fun bodyIsGzipped(contentEncoding: String?) = CONTENT_ENCODING_GZIP.equals(contentEncoding, ignoreCase = true) + + private companion object { + const val CONTENT_ENCODING_GZIP = "gzip" + } } diff --git a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt index e727dd226..63928e81e 100644 --- a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt +++ b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt @@ -36,8 +36,8 @@ class HttpBinClient( private val httpClient = OkHttpClient.Builder() // Add a ChuckerInterceptor instance to your OkHttp client - .addInterceptor(chuckerInterceptor) .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) + .addInterceptor(chuckerInterceptor) .build() private val api: HttpBinApi by lazy { From 37edfd1ca3283535f007063477a6656bda6289d1 Mon Sep 17 00:00:00 2001 From: Volodymyr Buberenko Date: Sat, 25 Jan 2020 20:12:32 +0200 Subject: [PATCH 2/2] Prepare 3.1.1 (#207) * Update README and CHANGELOG * Addressing @cortinico feedback --- CHANGELOG.md | 10 ++++++++++ README.md | 32 +++++++++++--------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e045f05..10e90d9ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,17 @@ # Change Log +## Version 3.1.1 *(2020-01-25)* + +This is hot-fix release to fix issue introduced in `3.1.0`. + +### Summary of Changes + +- Fixed an [issue](https://github.com/ChuckerTeam/chucker/issues/203) introduced in 3.1.0 where some of response bodies were shown as `null` and their sizes were 0 bytes. + ## Version 3.1.0 *(2020-01-24)* +### This version shouldn't be used as dependency due to [#203](https://github.com/ChuckerTeam/chucker/issues/203). Use 3.1.1 instead. + This is a new minor release of Chucker. Please note that this minor release contains multiple new features (see below) as well as multiple bugfixes. ### Summary of Changes diff --git a/README.md b/README.md index 8a26d46de..d4de8fd91 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ repositories { ```groovy dependencies { - debugImplementation "com.github.ChuckerTeam.Chucker:library:3.0.1" - releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:3.0.1" + debugImplementation "com.github.ChuckerTeam.Chucker:library:3.1.1" + releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:3.1.1" } ``` @@ -63,7 +63,7 @@ Don't forget to check the [changelog](CHANGELOG.md) to have a look at all the ch * Compatible with **OkHTTP 4** * **API >= 16** compatible -* Easy to integrate (just a 2 gradle implementation line). +* Easy to integrate (just 2 gradle `implementation` lines). * Works **out of the box**, no customization needed. * **Empty release artifact** 🧼 (no traces of Chucker in your final APK). * Support for body text search with **highlighting** 🕵️‍♂️ @@ -94,9 +94,9 @@ val chuckerInterceptor = ChuckerInterceptor( context = this, // The previously created Collector collector = chuckerCollector, - // The max body content length, after this responses will be truncated. + // The max body content length in bytes, after this responses will be truncated. maxContentLength = 250000L, - // List of headers to obfuscate in the Chucker UI + // List of headers to replace with ** in the Chucker UI headersToRedact = setOf("Auth-Token")) // Don't forget to plug the ChuckerInterceptor inside the OkHttpClient @@ -107,7 +107,7 @@ val client = OkHttpClient.Builder() ### Throwables ☄️ -Chucker supports also collecting and displaying **Throwables** of your application. To inform Chucker that a `Throwable` was fired you need to call the `onError` method of the `ChuckerCollector` (you need to retain an instance of your collector): +Chucker can also collect and display **Throwables** of your application. To inform Chucker that a `Throwable` was fired you need to call the `onError` method of the `ChuckerCollector` (you need to retain an instance of your collector): ```kotlin try { @@ -176,9 +176,8 @@ We're offering support for Chucker on the [#chucker](https://kotlinlang.slack.co Short `TODO` List for new contributors: -- [ ] Kotlinize classes inside the `.internal` package. -- [x] Have a empty state graphics/message for requests with no headers. -- [ ] Increment the test coverage. +- Increment the test coverage. +- [Issues marked as `Help wanted`](https://github.com/ChuckerTeam/chucker/labels/help%20wanted) ## Acknowledgments 🌸 @@ -191,18 +190,9 @@ Chucker is currently developed and maintained by the [ChuckerTeam](https://githu - [@redwarp](https://github.com/redwarp) - [@vbuberen](https://github.com/vbuberen) -### Contributors +### Thanks -Big thanks to our contributors ❤️ Please add your name here once you submit a PR. - -- [Ashok Varma](https://github.com/Ashok-Varma) Clean up -- [Bernat Borrás Paronella](https://github.com/alorma) Redact headers + HTTP methods in notifications -- [Hafiz Waleed Hussain](https://github.com/Hafiz-Waleed-Hussain) Search highlight in response tab -- [Karol Wrótniak](https://github.com/koral--) Fix race condition + Application name retrieval -- [OlliZi](https://github.com/OlliZi) Support for images -- [Paul Hawke](https://github.com/psh) API Clean -- [Paul Woitaschek](https://github.com/PaulWoitaschek) Fix typo in API -- [SeungHun Choe](https://github.com/uOOOO) Fix memory leak +Big thanks to our contributors ❤️ ### Libraries @@ -215,7 +205,7 @@ Chucker uses the following open source libraries: ## License 📄 ``` - Copyright (C) 2018 Chucker Team. + Copyright (C) 2018-2020 Chucker Team. Copyright (C) 2017 Jeff Gilfelt. Licensed under the Apache License, Version 2.0 (the "License");