Repository: auth0/auth0.android
Issue: JsonIOException crash caused by SocketTimeoutException

<!--
**Please do not report security vulnerabilities here**. The Responsible Disclosure Program (https://auth0.com/whitehat) details the procedure for disclosing security issues.

Thank you in advance for helping us to improve this library! Please read through the template below and answer all relevant questions. Your additional work here is greatly appreciated and will help us respond as quickly as possible. For general support or usage questions, use the Auth0 Community (https://community.auth0.com/) or Auth0 Support (https://support.auth0.com/). Finally, to avoid duplicates, please search existing Issues before submitting one here.

By submitting an Issue to this repository, you agree to the terms within the Auth0 Code of Conduct (https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).
-->

### Describe the problem

<!--
> Provide a clear and concise description of the issue
-->

We have a crash log from a user in production:

```
Fatal Exception: com.google.gson.JsonIOException: java.net.SocketTimeoutException: timeout
       at com.google.gson.internal.Streams.parse(Streams.java:62)
       at com.google.gson.internal.bind.TreeTypeAdapter.read(TreeTypeAdapter.java:65)
       at com.google.gson.TypeAdapter.fromJson(TypeAdapter.java:260)
       at com.auth0.android.request.internal.GsonAdapter.fromJson(GsonAdapter.kt:58)
       at com.auth0.android.request.internal.BaseRequest.execute(BaseRequest.kt:99)
       at com.auth0.android.request.internal.BaseRequest$start$1.run(BaseRequest.kt:67)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
       at java.lang.Thread.run(Thread.java:923)
Caused by java.net.SocketTimeoutException: timeout
       at okhttp3.internal.http2.Http2Stream$StreamTimeout.newTimeoutException(Http2Stream.kt:677)
       at okhttp3.internal.http2.Http2Stream$StreamTimeout.exitAndThrowIfTimedOut(Http2Stream.kt:686)
       at okhttp3.internal.http2.Http2Stream$FramingSource.read(Http2Stream.kt:382)
       at okhttp3.internal.connection.Exchange$ResponseBodySource.read(Exchange.kt:276)
       at okio.RealBufferedSource.read(RealBufferedSource.kt:189)
       at okio.RealBufferedSource.exhausted(RealBufferedSource.kt:197)
       at okio.InflaterSource.refill(InflaterSource.kt:112)
       at okio.InflaterSource.readOrInflate(InflaterSource.kt:76)
       at okio.InflaterSource.read(InflaterSource.kt:49)
       at okio.GzipSource.read(GzipSource.kt:69)
       at okio.RealBufferedSource$inputStream$1.read(RealBufferedSource.kt:158)
       at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:291)
       at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:355)
       at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:181)
       at java.io.InputStreamReader.read(InputStreamReader.java:184)
       at com.google.gson.stream.JsonReader.fillBuffer(JsonReader.java:1289)
       at com.google.gson.stream.JsonReader.nextQuotedValue(JsonReader.java:1029)
       at com.google.gson.stream.JsonReader.nextString(JsonReader.java:814)
       at com.google.gson.internal.bind.TypeAdapters$29.read(TypeAdapters.java:702)
       at com.google.gson.internal.bind.TypeAdapters$29.read(TypeAdapters.java:715)
       at com.google.gson.internal.bind.TypeAdapters$29.read(TypeAdapters.java:723)
       at com.google.gson.internal.bind.TypeAdapters$29.read(TypeAdapters.java:715)
       at com.google.gson.internal.bind.TypeAdapters$29.read(TypeAdapters.java:723)
       at com.google.gson.internal.bind.TypeAdapters$29.read(TypeAdapters.java:698)
       at com.google.gson.internal.Streams.parse(Streams.java:48)
       at com.google.gson.internal.bind.TreeTypeAdapter.read(TreeTypeAdapter.java:65)
       at com.google.gson.TypeAdapter.fromJson(TypeAdapter.java:260)
       at com.auth0.android.request.internal.GsonAdapter.fromJson(GsonAdapter.kt:58)
       at com.auth0.android.request.internal.BaseRequest.execute(BaseRequest.kt:99)
       at com.auth0.android.request.internal.BaseRequest$start$1.run(BaseRequest.kt:67)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
       at java.lang.Thread.run(Thread.java:923)
```

### What was the expected behavior?

<!--
> Tell us about the behavior you expected to see
-->

The app shouldn't crash from this network error.

### Reproduction
<!--
> Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent.
> **Note**: If clear, reproducable steps or the smallest sample app demonstrating misbehavior cannot be provided, we may not be able to follow up on this bug report.

> Where possible, please include:
>
> - The smallest possible sample app that reproduces the undesirable behavior
> - Log files (redact/remove sensitive information)
> - Application settings (redact/remove sensitive information)
> - Screenshots
-->

Seems this would happen when the login network response is successful, but times out streaming the response body to decode the JSON.

Gson internally, in `Streams.parse()` catches `IOException` and propagates as `JsonIOException`, which is not an `IOException`, but a `RuntimeException`. There's no try/catch logic to handle errors in the call to `resultAdapter.fromJson(reader)` and propagate as `Auth0Exception`:

```
...
val reader = AwareInputStreamReader(response.body, Charset.defaultCharset())
if (response.isSuccess()) {
    //2. Successful scenario. Response of type T
    val result: T = resultAdapter.fromJson(reader)
    reader.close()
    return result
}
...
```

### Environment

<!--
> Please provide the following:
-->

- **Version of this library used:** 2.2.0
- **Which framework are you using, if applicable:**
- **Other modules/plugins/libraries that might be involved:**
- **Any other relevant information you think would be useful:**

Changed files:
auth0/src/main/java/com/auth0/android/request/internal/BaseRequest.kt, auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt

diff --git a/auth0/src/main/java/com/auth0/android/request/internal/BaseRequest.kt b/auth0/src/main/java/com/auth0/android/request/internal/BaseRequest.kt
index 4b2eba7..db56869 100755
--- a/auth0/src/main/java/com/auth0/android/request/internal/BaseRequest.kt
+++ b/auth0/src/main/java/com/auth0/android/request/internal/BaseRequest.kt
@@ -96,22 +96,37 @@ internal open class BaseRequest<T, U : Auth0Exception>(
         val reader = AwareInputStreamReader(response.body, StandardCharsets.UTF_8)
         if (response.isSuccess()) {
             //2. Successful scenario. Response of type T
-            val result: T = resultAdapter.fromJson(reader)
-            reader.close()
+            val result: T = try {
+                resultAdapter.fromJson(reader)
+            } catch (exception: Exception) {
+                //multi catch IOException and JsonParseException (including JsonIOException)
+                //3. Network exceptions, timeouts, etc reading response body
+                val error: U = errorAdapter.fromException(exception)
+                throw error
+            } finally {
+                reader.close()
+            }
             return result
         }
 
-        //3. Error scenario. Response of type U
-        val error: U = if (response.isJson()) {
-            errorAdapter.fromJsonResponse(response.statusCode, reader)
-        } else {
-            errorAdapter.fromRawResponse(
-                response.statusCode,
-                reader.readText(),
-                response.headers
-            )
+        //4. Error scenario. Response of type U
+        val error: U = try {
+            if (response.isJson()) {
+                errorAdapter.fromJsonResponse(response.statusCode, reader)
+            } else {
+                errorAdapter.fromRawResponse(
+                    response.statusCode,
+                    reader.readText(),
+                    response.headers
+                )
+            }
+        } catch (exception: Exception) {
+            //multi catch IOException and JsonParseException (including JsonIOException)
+            //5. Network exceptions, timeouts, etc reading response body
+            errorAdapter.fromException(exception)
+        } finally {
+            reader.close()
         }
-        reader.close()
         throw error
     }
 
diff --git a/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt
index c65d53f..fc49bde 100755
--- a/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt
+++ b/auth0/src/test/java/com/auth0/android/request/internal/BaseRequestTest.kt
@@ -4,6 +4,7 @@ import com.auth0.android.Auth0Exception
 import com.auth0.android.callback.Callback
 import com.auth0.android.request.*
 import com.google.gson.Gson
+import com.google.gson.JsonIOException
 import com.nhaarman.mockitokotlin2.*
 import org.hamcrest.MatcherAssert
 import org.hamcrest.Matchers
@@ -174,6 +175,41 @@ public class BaseRequestTest {
         verify(errorAdapter).fromException(eq(networkError))
     }
 
+    @Test
+    @Throws(Exception::class)
+    public fun shouldBuildErrorFromResponseParseException() {
+        mockSuccessfulServerResponse()
+        val resultAdapter = Mockito.mock(GsonAdapter::class.java) as JsonAdapter<SimplePojo>
+        val baseRequest = BaseRequest(
+            HttpMethod.POST,
+            BASE_URL,
+            client,
+            resultAdapter,
+            errorAdapter
+        )
+        val networkError = mock<JsonIOException>()
+        Mockito.`when`(
+            resultAdapter.fromJson(
+                any()
+            )
+        ).thenThrow(networkError)
+        Mockito.`when`(
+            errorAdapter.fromException(
+                any()
+            )
+        ).thenReturn(auth0Exception)
+        var exception: Exception? = null
+        var result: SimplePojo? = null
+        try {
+            result = baseRequest.execute()
+        } catch (e: Exception) {
+            exception = e
+        }
+        MatcherAssert.assertThat(exception, Matchers.`is`(auth0Exception))
+        MatcherAssert.assertThat(result, Matchers.`is`(Matchers.nullValue()))
+        verify(errorAdapter).fromException(eq(networkError))
+    }
+
     @Test
     @Throws(Exception::class)
     public fun shouldBuildErrorFromUnsuccessfulJsonResponse() {
@@ -420,4 +456,4 @@ public class BaseRequestTest {
     private companion object {
         private const val BASE_URL = "https://auth0.com"
     }
-}
\\ No newline at end of file
+}