I'm curious around best practices for exception handling & logging.
I have a mobile App which uses Airbrake for exception monitoring and basic Android.Log for logging.
So there are several spots in code that look similar to
// some request
AirbrakeNotifier.notify(exception)
After some updates I'm using a logger interface and Timber.
I'm interested in the code above being changed into something like
//some request
Timber.e("Failed to blah blah", exception.)
And a custom timber tree could be planted like
class MyTree: Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
//if log level error
ExceptionMonitor.notify(t)
logger.log(....)
}
}
In my case, any error I'm logging I want to send to the remote monitoring service as well. All other Log levels will simply log.
Is bundling logging/exception handling under an interface bad practice?
It's quite usual to use Timber for crash analytics services. In fact, the official Timber sample uses FakeCrashLibrary as an example of this use case.
Normally, you shouldn't have debug logs in production anyway, and Timber helps to replace debug logs with some crash analytics service for prod builds.
Related
Once in a while, I see Fatal Exception: x.y Required value 'firstName' (JSON name 'first_name') missing at $[1] crash report in Crashlytics. Missing properties vary from report to report. I can not reprod these crashes so I need to put logging (that makes a network call to server / fire an firebase event with the broken/problematic json text) so I can examine it.
I tried following options but they wont work for me:
Make firstName nullable. This will ideally solve the issue but these reports are for various properties of various models. Making all variables nullable just kills the kotlin fun. Also Server is committed to send firstName so if I can show a json without one, they should fix it.
Add an interceptor to log ALL request responses and then check last network call before crash. But this will load app and backend with unnecessary logs which can quickly get bulky and messy.
I am not hoping to handle the error and avoid the crash (that would be great if there was a silver bullet).
I have this function that makes a network request and writes results to SQLDelight database:
#Throws(Exception::class)
suspend fun updateData()
In iOS project in Xcode I see that this function is translated into a function with completionHandler of type (KotlinUnit?, Error?) -> Void.
In my case Error type has only 1 visible property - localizedDescription. If I cast to NSError then I can get more visible fields but none of them are of any help.
I saw in logs that every error in Ktor http client is thrown as Error in Swift. This makes it hard to know what kind of error was thrown.
I want to be able to see the error type and react differently for certain cases. For example, if it was network unavailable error, to show my custom error text with translations, or if it was 401 error send user to auth page, 500 show some other text, etc.
For Android app I guess I can just forward those exceptions and still be able to type check or access more data about error. But I don't know what to do for iOS app. The solution ideally should work great on both platforms.
What are the best practices for handling network errors in iOS and in Android or in general any error/exception that are thrown from shared module in multiplatform project?
You can use a MVP architecture and let the Presenter component deal with these errors and it just indicates to the view where to go.
Something like that:
shared/MyPresenter.kt
// ...
when (throwable) {
is Unauthorized -> {
view?.showInvalidCredentials()
}
is Conflict -> {
view?.showConflictAccount()
}
else -> {
view?.showError(throwable)
}
}
With this approach you'll remove that responsibility from the platform Views.
A good project to take as a guide is that: https://github.com/jarroyoesp/KotlinMultiPlatform
You can map responses in your network layer to a Result. Often the data modelling of result is specific to your application, so can include some combination of loading/error/result by using generics or sealed classes.
You can also use the Result type in the kotlin standard library.
I want to write a test for an Android Application with WireMock Integration.
I tried using all 3 different Wiremock Integration approaches -
1. Writing a test with JUnit Rule
2. Non-JUnit and general Java usage
3. WireMock APIs against a standalone server
I could run my tests with WireMock APIs against a standalone server.
But since I cannot run on different ports dynamically with this approach, I moved to the other 2 approaches.
Below is the sample Kotlin code (with predefined ports)
For Junit Rule implementation
#Rule
#JvmField
var wireMockRule: WireMockRule = WireMockRule(8888)
#Test
fun exampleTest() {
stubFor(WireMock.get(WireMock.urlPathEqualTo( "/requestURL"))
.willReturn(WireMock.aResponse().withHeader("Content-Type", "application/json")
.withStatus(200).withBody(responseString)))
// App launch and test assertions
}
For Non-JUnit and general Java usage
#Before
fun setUp() {
wireMockServer = WireMockServer(wireMockConfig().port(8888))
wireMockServer.start()
// Tried it by both commenting and uncommenting the following line
// configureFor("localhost", wireMockServer.port())
}
#Test
fun exampleTest() {
val wireMockClient = WireMock("localhost", wireMockServer.port())
wireMockClient.register(stubFor(WireMock.get(WireMock.urlPathMatching( "/requestURL"))
.willReturn(WireMock.aResponse().withHeader("Content-Type", "application/json")
.withStatus(200).withBody(responseString))))
// App launch and test assertions
}
For both these approaches, I am not getting the response I stubbed. I am getting an empty response.
Changed the "withStatus" field to 400 to see if its an issue with response String, but I am not getting the error either, thats when I am sure that its not stubbing anything atall!
Is there anything I am missing from the sample codes shared?
Note: Junit version used is Junit4, writing the tests in Kotlin.
Figured out the reason why the above implementation was not working.
My request calls were going to host and port 10.0.2.2 and 8080 by default(based on the existing code) and I am stubbing the wiremock calls on local host and 8888.
It started working as expected once the host and port for the request call is updated.
I am creating an app using Retrofit 2.1.0 and OkHttp 3.4.2.
In debug mode with minifyEnabled set to false everything works perfectly but as soon as i change minifyEnabled to true i get the following exception:
HTTP FAILED: java.net.ProtocolException: Too many follow-up requests: 21
My Proguard rules for OkHttp are as follows:
-keep class com.squareup.okhttp3.** {
*;
}
-dontwarn okhttp3.**
-dontwarn okio.**
I can't understand why this exception is thrown and i do not understand why the app is seems to be making 21 follow up requests. Can anyone help me?
I've just had this exact same error: java.net.ProtocolException: Too many follow-up requests: 21. For your interest I'm using Retrofit 2.5.0 and OkHttp 3.14.1, although versions don't matter much.
I too only have this error with Proguard enabled (actually I'm using R8 but the result is the same). This is important and hints the root cause.
What's the problem? I authenticate using OAuth, adding an "Authorization" header as usual. When the token expires the server sends a 401 Unauthorized. Since I'm using an OkHttp Authenticator to refresh the token, when a 401 is received the authenticate method is called.
The problem is that I parse the response of this 401 Unauthorized request using Gson, like this:
override fun authenticate(route: Route?, response: Response): Request? {
val responseError: ResponseError? = response.body()?.let {
Gson().fromJson(it.string(), ResponseError::class.java) // <- Fails!
}
// Check server response and decide if should refresh token...
}
But since the class ResponseError is obfuscated by proguard it's fields don't match the names of the JSON that the server sends, which makes Gson().fromJson fail, and consequently the token is not refreshed. The result is that a network call is performed repeatedly until the Exception is thrown.
The fix is trivial. Just add #Keep to ResponseError:
import androidx.annotation.Keep
#Keep
data class ResponseError(
val error: String? = null,
val error_description: String? = null
)
Since your problem happens with proguard enabled only, it's likely that a request or response class is being obfuscated, which makes the JSON parsing fail. Or you may be saving some JSON to Shared Preferences using Gson.fromJson and Gson.fromJson with an obfuscated class.
To fix the problem add #SerializedName and/or #Keep to all your requests and responses (specially enums, which give more trouble). Alternatively you can simply put the all the requests and responses into a single package and exclude it. In addition to requests and responses, pay special attention to any Gson.fromJson and Gson.fromJson call too.
To help diagnose the problem you can check what proguard/R8 does by checking the mappings file you'll find in app/build/outputs/mapping/release/mapping.txt (for a release build). It contains all the transformations that proguard/R8 has done to your code.
You can also analyze the APK by doing Build -> Analyze APK... You'll see which classes are being obfuscated in a very simple way. You can analyze 2 APKs at the same time (one minified and one not minified) and compare them.
When using ADAL for Android I found something a bit confusing regarding the acquireToken() API error handling.
From the source code it seems that error handling should be accomplished by catching AuthenticationException and inspecting its ADALError enum property.
However, the AuthenticationResult class has an AuthenticationStatus property and it could return as Failed in some cases (and then I suppose you should inspect the errorCode & errorDescription properties but these don't conform to ADALError enum).
Also, it seems that the (AuthenticationResult.getStatus()==Failed) case will return on the onSuccess() callback method since the onError() only returns exceptions.
Can someone clear this for me?
I would like to know what are all the possible error paths so I can handle all of them properly.
Exceptions are used to report on errors encountered within the ADAL client code. Error code helps to identify issues such as missing app permission for internet, connection issue, invalid redirectUri format. It also helps for common issues such as SSL handshake. The AuthenticationResult is where errors returned from the server, either AAD or ADFS, are reported. The fact that the onSuccess() callback is called indicates that there were no client side errors, but the AuthenticationResult may still contain errors that the server returned.