Repository: opensrp/fhircore
Issue: [Quest] Fatal crash on Keycloak request timeout

**Describe the bug**

The app crashes during login due to a timeout exception

**To Reproduce**
Steps to reproduce the behavior: 
1. Go to Login
2. Enter the login details
3. Emulate a timeout exception
4. See error in the logs

**Expected behavior**

The app should log the exception and show an error on the login page

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Smartphone (please complete the following information):**
 - Device: BlueG60
 - Android version: 9
 - Build number: 
 - Version [e.g. 24]
 - OpenSRP version - v0.2.4-opensrp

**Additional context**

Stack trace:

```
<-- HTTP FAILED: java.net.SocketTimeoutException: failed to connect to keycloak-stage.smartregister.org/18.170.151.194 (port 443) from /192.168.178.101 (port 55970) after 120000ms: isConnected failed: ETIMEDOUT (Connection timed out)
2023-07-06 10:31:34.593  3044-3120  AndroidRuntime          org.smartregister.opensrp.ecbis      E  FATAL EXCEPTION: DefaultDispatcher-worker-1
                                                                                                    Process: org.smartregister.opensrp.ecbis, PID: 3044
                                                                                                    java.net.SocketTimeoutException: failed to connect to keycloak-stage.smartregister.org/18.170.151.194 (port 443) from /192.168.178.101 (port 55970) after 120000ms: isConnected failed: ETIMEDOUT (Connection timed out)
                                                                                                    	at libcore.io.IoBridge.isConnected(IoBridge.java:276)
                                                                                                    	at libcore.io.IoBridge.connectErrno(IoBridge.java:187)
                                                                                                    	at libcore.io.IoBridge.connect(IoBridge.java:129)
                                                                                                    	at java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:137)
                                                                                                    	at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:390)
                                                                                                    	at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:230)
                                                                                                    	at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:212)
                                                                                                    	at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:436)
                                                                                                    	at java.net.Socket.connect(Socket.java:621)
                                                                                                    	at okhttp3.internal.platform.AndroidPlatform.connectSocket(AndroidPlatform.kt:63)
                                                                                                    	at okhttp3.internal.connection.RealConnection.connectSocket(RealConnection.kt:295)
                                                                                                    	at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:207)
                                                                                                    	at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.kt:226)
                                                                                                    	at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.kt:106)
                                                                                                    	at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.kt:74)
                                                                                                    	at okhttp3.internal.connection.RealCall.initExchange$okhttp(RealCall.kt:255)
                                                                                                    	at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:32)
                                                                                                    	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
                                                                                                    	at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95)
                                                                                                    	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
                                                                                                    	at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83)
                                                                                                    	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
                                                                                                    	at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76)
                                                                                                    	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
                                                                                                    	at io.sentry.android.okhttp.SentryOkHttpInterceptor.intercept(SentryOkHttpInterceptor.kt:91)
                                                                                                    	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
                                                                                                    	at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.kt:221)
                                                                                                    	at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
                                                                                                    	at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201)
                                                                                                    	at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:517)
                                                                                                    	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:764)
                                                                                                    	Suppressed: java.net.SocketTimeoutException: failed to connect to keycloak-stage.smartregister.org/35.176.23.208 (port 443) from /192.168.178.101 (port 59524) after 120000ms: isConnected failed: ETIMEDOUT (Connection timed out)
                                                                                                    		... 33 more
                                                                                                    	Caused by: android.system.ErrnoException: isConnected failed: ETIMEDOUT (Connection timed out)
                                                                                                    		at libcore.io.IoBridge.isConnected(IoBridge.java:267)
                                                                                                    		... 32 more
                                                                                                    	Suppressed: java.net.SocketTimeoutException: failed to connect to keycloak-stage.smartregister.org/13.42.187.60 (port 443) from /192.168.178.101 (port 37774) after 120000ms: isConnected failed: ETIMEDOUT (Connection timed out)
                                                                                                    		... 33 more
                                                                                                    	Caused by: android.system.ErrnoException: isConnected failed: ETIMEDOUT (Connection timed out)
                                                                                                    		at libcore.io.IoBridge.isConnected(IoBridge.java:267)
                                                                                                    		... 32 more
                                                                                                    	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@58679b, Dispatchers.IO]
                                                                                                    Caused by: android.system.ErrnoException: isConnected failed: ETIMEDOUT (Connection timed out)
                                                                                                    	at libcore.io.IoBridge.isConnected(IoBridge.java:267)
                                                                                                    	
                                                                                                    	
Trying to get
```



Changed files:
android/quest/src/test/java/org/smartregister/fhircore/quest/ui/login/LoginViewModelTest.kt
android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt

diff --git a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt
index d58a3ec98..f79e2e585 100644
--- a/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt
+++ b/android/quest/src/main/java/org/smartregister/fhircore/quest/ui/login/LoginViewModel.kt
@@ -29,6 +29,7 @@ import androidx.work.WorkManager
 import dagger.hilt.android.lifecycle.HiltViewModel
 import io.sentry.Sentry
 import io.sentry.protocol.User
+import java.net.SocketTimeoutException
 import java.net.UnknownHostException
 import javax.inject.Inject
 import kotlinx.coroutines.launch
@@ -239,7 +240,7 @@ constructor(
     }
   }
 
-  private suspend fun fetchPractitioner(
+  suspend fun fetchPractitioner(
     onFetchUserInfo: (Result<UserInfo>) -> Unit,
     onFetchPractitioner: (Result<FhirR4ModelBundle>) -> Unit
   ) {
@@ -254,6 +255,15 @@ constructor(
         } catch (httpException: HttpException) {
           onFetchPractitioner(Result.failure(httpException))
           Timber.e(httpException.response()?.errorBody()?.charStream()?.readText())
+        } catch (unknownHostException: UnknownHostException) {
+          onFetchPractitioner(Result.failure(unknownHostException))
+          Timber.e(unknownHostException, "An error occurred fetching the practitioner details")
+        } catch (socketTimeoutException: SocketTimeoutException) {
+          onFetchPractitioner(Result.failure(socketTimeoutException))
+          Timber.e(socketTimeoutException, "An error occurred fetching the practitioner details")
+        } catch (exception: Exception) {
+          onFetchPractitioner(Result.failure(exception))
+          Timber.e(exception, "An error occurred fetching the practitioner details")
         }
       } else {
         onFetchPractitioner(
@@ -262,6 +272,15 @@ constructor(
       }
     } catch (httpException: HttpException) {
       onFetchUserInfo(Result.failure(httpException))
+    } catch (unknownHostException: UnknownHostException) {
+      onFetchUserInfo(Result.failure(unknownHostException))
+      Timber.e(unknownHostException, "An error occurred fetching the practitioner details")
+    } catch (socketTimeoutException: SocketTimeoutException) {
+      onFetchUserInfo(Result.failure(socketTimeoutException))
+      Timber.e(socketTimeoutException, "An error occurred fetching the practitioner details")
+    } catch (exception: Exception) {
+      onFetchUserInfo(Result.failure(exception))
+      Timber.e(exception, "An error occurred fetching the practitioner details")
     }
   }
 
diff --git a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/login/LoginViewModelTest.kt b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/login/LoginViewModelTest.kt
index 1b24ce0a1..6a6e3d17a 100644
--- a/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/login/LoginViewModelTest.kt
+++ b/android/quest/src/test/java/org/smartregister/fhircore/quest/ui/login/LoginViewModelTest.kt
@@ -28,9 +28,11 @@ import io.mockk.mockk
 import io.mockk.slot
 import io.mockk.spyk
 import io.mockk.verify
+import java.net.SocketTimeoutException
 import java.net.UnknownHostException
 import javax.inject.Inject
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
 import okhttp3.internal.http.RealResponseBody
 import org.hl7.fhir.r4.model.Bundle
 import org.hl7.fhir.r4.model.Organization
@@ -361,6 +363,100 @@ internal class LoginViewModelTest : RobolectricTest() {
     Assert.assertEquals(LoginErrorState.INVALID_CREDENTIALS, loginViewModel.loginErrorState.value!!)
   }
 
+  @Test
+  fun `loginViewModel#fetchPractitioner() should call onFetchUserInfo with exception when SocketTimeoutException is thrown`() {
+    updateCredentials()
+    secureSharedPreference.saveCredentials(thisUsername, thisPassword.toCharArray())
+    every { tokenAuthenticator.sessionActive() } returns false
+    coEvery { keycloakService.fetchUserInfo() }.throws(SocketTimeoutException())
+
+    val fetchUserInfoCallback: (Result<UserInfo>) -> Unit = mockk(relaxed = true)
+    val fetchPractitionerCallback: (Result<Bundle>) -> Unit = mockk(relaxed = true)
+    val userInfoSlot = slot<Result<UserInfo>>()
+
+    runBlocking {
+      loginViewModel.fetchPractitioner(fetchUserInfoCallback, fetchPractitionerCallback)
+    }
+
+    verify { fetchUserInfoCallback(capture(userInfoSlot)) }
+    verify(exactly = 0) { fetchPractitionerCallback(any()) }
+
+    Assert.assertTrue(userInfoSlot.captured.exceptionOrNull() is SocketTimeoutException)
+  }
+
+  @Test
+  fun `loginViewModel#fetchPractitioner() should call onFetchUserInfo with exception when UnknownHostException is thrown`() {
+    updateCredentials()
+    secureSharedPreference.saveCredentials(thisUsername, thisPassword.toCharArray())
+    every { tokenAuthenticator.sessionActive() } returns false
+    coEvery { keycloakService.fetchUserInfo() }.throws(UnknownHostException())
+
+    val fetchUserInfoCallback: (Result<UserInfo>) -> Unit = mockk(relaxed = true)
+    val fetchPractitionerCallback: (Result<Bundle>) -> Unit = mockk(relaxed = true)
+    val userInfoSlot = slot<Result<UserInfo>>()
+
+    runBlocking {
+      loginViewModel.fetchPractitioner(fetchUserInfoCallback, fetchPractitionerCallback)
+    }
+
+    verify { fetchUserInfoCallback(capture(userInfoSlot)) }
+    verify(exactly = 0) { fetchPractitionerCallback(any()) }
+
+    Assert.assertTrue(userInfoSlot.captured.exceptionOrNull() is UnknownHostException)
+  }
+
+  @Test
+  fun `loginViewModel#fetchPractitioner() should call onFetchPractitioner with exception when UnknownHostException is thrown`() {
+    updateCredentials()
+    secureSharedPreference.saveCredentials(thisUsername, thisPassword.toCharArray())
+    every { tokenAuthenticator.sessionActive() } returns false
+    coEvery { keycloakService.fetchUserInfo() } returns
+      Response.success(UserInfo(keycloakUuid = "awesome_uuid"))
+    coEvery { fhirResourceService.getResource(any()) }.throws(UnknownHostException())
+
+    val fetchUserInfoCallback: (Result<UserInfo>) -> Unit = mockk(relaxed = true)
+    val fetchPractitionerCallback: (Result<Bundle>) -> Unit = mockk(relaxed = true)
+    val bundleSlot = slot<Result<Bundle>>()
+    val userInfoSlot = slot<Result<UserInfo>>()
+
+    runBlocking {
+      loginViewModel.fetchPractitioner(fetchUserInfoCallback, fetchPractitionerCallback)
+    }
+
+    verify { fetchUserInfoCallback(capture(userInfoSlot)) }
+    verify { fetchPractitionerCallback(capture(bundleSlot)) }
+
+    Assert.assertTrue(userInfoSlot.captured.isSuccess)
+    Assert.assertEquals("awesome_uuid", userInfoSlot.captured.getOrThrow().keycloakUuid)
+    Assert.assertTrue(bundleSlot.captured.exceptionOrNull() is UnknownHostException)
+  }
+
+  @Test
+  fun `loginViewModel#fetchPractitioner() should call onFetchPractitioner with exception when SocketTimeoutException is thrown`() {
+    updateCredentials()
+    secureSharedPreference.saveCredentials(thisUsername, thisPassword.toCharArray())
+    every { tokenAuthenticator.sessionActive() } returns false
+    coEvery { keycloakService.fetchUserInfo() } returns
+      Response.success(UserInfo(keycloakUuid = "awesome_uuid"))
+    coEvery { fhirResourceService.getResource(any()) }.throws(SocketTimeoutException())
+
+    val fetchUserInfoCallback: (Result<UserInfo>) -> Unit = mockk(relaxed = true)
+    val fetchPractitionerCallback: (Result<Bundle>) -> Unit = mockk(relaxed = true)
+    val bundleSlot = slot<Result<Bundle>>()
+    val userInfoSlot = slot<Result<UserInfo>>()
+
+    runBlocking {
+      loginViewModel.fetchPractitioner(fetchUserInfoCallback, fetchPractitionerCallback)
+    }
+
+    verify { fetchUserInfoCallback(capture(userInfoSlot)) }
+    verify { fetchPractitionerCallback(capture(bundleSlot)) }
+
+    Assert.assertTrue(userInfoSlot.captured.isSuccess)
+    Assert.assertEquals("awesome_uuid", userInfoSlot.captured.getOrThrow().keycloakUuid)
+    Assert.assertTrue(bundleSlot.captured.exceptionOrNull() is SocketTimeoutException)
+  }
+
   private fun practitionerDetails(): PractitionerDetails {
     return PractitionerDetails().apply {
       fhirPractitionerDetails =