From 567dae4110a826e6240181f47885db7fcfa6c916 Mon Sep 17 00:00:00 2001 From: Eric Denman <1063557+edenman@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:03:53 -0500 Subject: [PATCH] [android] Don't crash the app if native client fails to init In our app we are wrapping the calls to the juicebox SDK in a try/catch but we are still getting occasional crashes (stacktrace below). This PR adds a try/catch around both of the launched threads to ensure that the app does not crash if Client init fails, and adds an optional callback so callers get notified if the init failed. ``` Fatal Exception: java.net.NoRouteToHostException: Host unreachable at libcore.io.IoBridge.connect(IoBridge.java:182) at java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:142) 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:646) at com.android.okhttp.internal.Platform.connectSocket(Platform.java:182) at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:145) at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116) at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186) at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128) at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97) at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289) at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:131) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getOutputStream(HttpURLConnectionImpl.java:262) at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getOutputStream(DelegatingHttpsURLConnection.java:219) at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:30) at com.google.firebase.perf.network.InstrURLConnectionBase.getOutputStream(InstrURLConnectionBase.java:165) at com.google.firebase.perf.network.InstrHttpsURLConnection.getOutputStream(InstrHttpsURLConnection.java:89) at xyz.juicebox.sdk.Client$Companion$createNative$httpSend$1$1.invoke(Client.kt:163) at xyz.juicebox.sdk.Client$Companion$createNative$httpSend$1$1.invoke(Client.kt:125) at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30) ``` --- .../main/kotlin/xyz/juicebox/sdk/Client.kt | 114 ++++++++++-------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/android/src/main/kotlin/xyz/juicebox/sdk/Client.kt b/android/src/main/kotlin/xyz/juicebox/sdk/Client.kt index aa78e6f..bd7b6db 100644 --- a/android/src/main/kotlin/xyz/juicebox/sdk/Client.kt +++ b/android/src/main/kotlin/xyz/juicebox/sdk/Client.kt @@ -1,4 +1,5 @@ package xyz.juicebox.sdk +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import xyz.juicebox.sdk.internal.Native @@ -123,74 +124,87 @@ class Client private constructor ( private fun createNative(configuration: Configuration, previousConfigurations: Array, authTokens: Map?): Long { val httpSend = Native.HttpSendFn { httpClient, request -> thread { - val urlConnection = URL(request.url).openConnection() as HttpsURLConnection - - pinnedCertificates?.let { - val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) - keyStore.load(null, null) - it.forEachIndexed { index, certificate -> - keyStore.setCertificateEntry(index.toString(), certificate) + try { + val urlConnection = URL(request.url).openConnection() as HttpsURLConnection + + pinnedCertificates?.let { + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + keyStore.load(null, null) + it.forEachIndexed { index, certificate -> + keyStore.setCertificateEntry(index.toString(), certificate) + } + + val trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + trustManagerFactory.init(keyStore) + val trustManagers = trustManagerFactory.trustManagers + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustManagers, null) + urlConnection.sslSocketFactory = sslContext.socketFactory } - val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - trustManagerFactory.init(keyStore) - val trustManagers = trustManagerFactory.trustManagers - - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustManagers, null) - urlConnection.sslSocketFactory = sslContext.socketFactory - } + urlConnection.requestMethod = request.method - urlConnection.requestMethod = request.method + urlConnection.setRequestProperty( + "User-Agent", + "JuiceboxSdk-Android/${Native.sdkVersion()}" + ) - urlConnection.setRequestProperty( - "User-Agent", - "JuiceboxSdk-Android/${Native.sdkVersion()}" - ) + urlConnection.setRequestProperty( + "X-Juicebox-Version", + Native.sdkVersion() + ) - urlConnection.setRequestProperty( - "X-Juicebox-Version", - Native.sdkVersion() - ) + request.headers?.forEach { + urlConnection.setRequestProperty(it.name, it.value) + } - request.headers?.forEach { - urlConnection.setRequestProperty(it.name, it.value) - } + urlConnection.doInput = true + request.body?.let { + urlConnection.doOutput = true + urlConnection.outputStream.write(it) + } - urlConnection.doInput = true - request.body?.let { - urlConnection.doOutput = true - urlConnection.outputStream.write(it) - } + val response = Native.HttpResponse() - val response = Native.HttpResponse() + response.id = request.id + response.statusCode = urlConnection.responseCode.toShort() + response.headers = urlConnection.headerFields.filterKeys { it != null }.map { (key, values) -> + Native.HttpHeader(key, values.joinToString(",")) + }.toTypedArray() - response.id = request.id - response.statusCode = urlConnection.responseCode.toShort() - response.headers = urlConnection.headerFields.filterKeys { it != null }.map { (key, values) -> - Native.HttpHeader(key, values.joinToString(",")) - }.toTypedArray() + if (response.statusCode == 200.toShort()) { + response.body = urlConnection.inputStream.readBytes() + } else { + response.body = urlConnection.errorStream.readBytes() + } - if (response.statusCode == 200.toShort()) { - response.body = urlConnection.inputStream.readBytes() - } else { - response.body = urlConnection.errorStream.readBytes() + Native.httpClientRequestComplete(httpClient, response) + } catch (t: Throwable) { + Log.e("JuiceboxClient", "Failed to make http call", t) + val fakeErrorResponse = Native.HttpResponse() + fakeErrorResponse.statusCode = -1 + Native.httpClientRequestComplete(httpClient, fakeErrorResponse) } - - Native.httpClientRequestComplete(httpClient, response) } } val getAuthToken = Native.GetAuthTokenFn { context, contextId, realmId -> thread { - authTokens?.let { - Native.authTokenGetComplete(context, contextId, it[realmId]?.native ?: 0) - } ?: run { - fetchAuthTokenCallback?.let { callback -> - Native.authTokenGetComplete(context, contextId, callback(realmId)?.native ?: 0) + try { + authTokens?.let { + Native.authTokenGetComplete(context, contextId, it[realmId]?.native ?: 0) } ?: run { - Native.authTokenGetComplete(context, contextId, 0) + fetchAuthTokenCallback?.let { callback -> + Native.authTokenGetComplete(context, contextId, callback(realmId)?.native ?: 0) + } ?: run { + Native.authTokenGetComplete(context, contextId, 0) + } } + } catch (t: Throwable) { + Log.e("JuiceboxClient", "Failed to get auth token", t) + Native.authTokenGetComplete(context, contextId, -1) } } }