TLDR:
Upgrading react-native-intercom forced a shared dependency (okhttp) to update to a newer version, while other dependencies were still expecting the old version, leading to crashes in my Android app.
What's the problem you're solving?
Firebase Crashlytics warned me about Trending Stability Issues for the Android Beta release of my app, Sketch a Day .
The error looked like this:
Fatal Exception: java.lang.NoSuchMethodError
No static method delimiterOffset(Ljava/lang/String;IILjava/lang/String;)I in class Lokhttp3/internal/Util; or its super classes (declaration of 'okhttp3.internal.Util' appears in base.apk!classes4.dex)
What did you do to solve it?
Immediately the error made me think it was some kind of dependency mismatch - the stack trace shows that one dependency (React Native) was making a call to something, which called something, which called delimiterOffset(String, String) and crashed.
I had a look in okhttp source code and saw that the current method signature was pretty different to that, so something must have changed.
Next, I needed to figure out how to list all the dependencies in my project. I know how to do this with NPM and CocoaPods but had no idea how to do it in Gradle. It turns out there's some (to my mind) magical way of listing all dependencies in all your "stuff" in Gradle.
Put this into your android/build.gradle file:
subprojects {
task allDeps(type: DependencyReportTask) {}
}
And then run:
./gradlew allDeps > allDeps.txt
Then you can pore over all the dependencies and see what's going on.
This gives you a file containing some stuff that looks like this:
+--- project :react-native-firebase_functions
| +--- project :react-native-firebase_app (*)
| +--- com.facebook.react:react-native:+ -> 0.64.2 (*)
| +--- com.google.firebase:firebase-bom:26.8.0 (*)
| \--- com.google.firebase:firebase-functions -> 19.2.0
| +--- com.google.android.gms:play-services-base:17.0.0 -> 17.6.0 (*)
| +--- com.google.android.gms:play-services-basement:17.0.0 -> 17.6.0 (*)
| +--- com.google.android.gms:play-services-tasks:17.0.0 -> 17.2.1 (*)
| +--- com.google.firebase:firebase-auth-interop:18.0.0 -> 19.0.2 (*)
| +--- com.google.firebase:firebase-common:19.5.0 (*)
| +--- com.google.firebase:firebase-components:16.1.0 (*)
| +--- com.google.firebase:firebase-iid:20.0.1 -> 21.1.0 (*)
| +--- com.google.firebase:firebase-iid-interop:17.0.0 -> 17.1.0 (*)
| \--- com.squareup.okhttp3:okhttp:3.12.1 -> 4.9.0 (*)
This is a short excerpt, but you can see some interesting stuff going on here, which I had to research to find out what it all means.
What does the -> arrow mean in a gradle dependency tree?
In short, on the left side is the requested dependency, and on the right side is the resolved dependency.
What does the (*) asterisk mean in a gradle dependency tree?
The asterisk denotes that any sub-dependencies are not listed, because they are listed somewhere else in the file, and gradle saves some characters and only lists the sub-tree once.
More information on the asterisk and arrow are given in this Stack Overflow post
Given this information, I figured that this line probably wasn't good, as confirmed by this StackOverflow post
\--- com.squareup.okhttp3:okhttp:3.12.1 -> 4.9.0 (*)
The issue is that there are two packages: okhttp and okhttp-urlconnection, which must both be on the same version as they call each other. There were lines like this in the tree which confirmed the issue:
| +--- com.squareup.okhttp3:okhttp:3.12.12 -> 4.9.0 (*)
| +--- com.squareup.okhttp3:okhttp-urlconnection:3.12.12
Now I had to find who was requesting okhttp:4.9.0 which can be done by searching for okhttp:4 in the dependency tree file and looking up to the root node.
It turns out that it was the newest version of react-native-intercom which depends on io.intercom.android:intercom-sdk-base:9.+ which in turn requests com.squareup.okhttp3:okhttp:4.9.0.
I had updated react-native-intercom recently from version 18 to version 21, which seems like the problem! Going through the native dependencies that that package has told me that I needed to go back to version 18 which uses implementation 'io.intercom.android:intercom-sdk:8.+' which in turn uses okhttp:3.
By reverting that dependency, we then get a tree that looks more like this:
+--- com.squareup.okhttp3:okhttp:3.12.12 (*)
+--- com.squareup.okhttp3:okhttp-urlconnection:3.12.12
š MUCH BETTER!
I'll be honest and say that I haven't actually verified that the crash has fixed, as I didn't reproduce it in the first place, but there's a good chance that solves the problem!
How long did it take to solve this issue?
This took about 2 hours to solve. There were a lot of StackOverflow tabs open by the end!
Are you happy with the solution that you came up with?
I'm mildly confused that not everyone using React Native is having this issue, as React Native itself explicitly depends on okhttp:3.
Maybe everyone using React Native Intercom is having this issue, or it only appears in certain circumstances...I'm unsure.
Incidentally, the next version of React Native (0.65) uses okhttp:4.9 so that might either lead to a load more issues if other libraries still depend on version 3, or maybe it'll fix everything. We'll see!
What could have prevented this issue from happening in the first place?
Whenever gradle does anything, it seems to vomit out hundreds of lines of logs of unknown importance (unknown to me, at least). I wonder if any of those lines say anything like:
okhttp:3.12.12 requested, but resolved to 4.9.0 - this could be a problem!
I have noticed that there are other libraries with the same issue, although I suppose the APIs could have changed in such a way that coincidentally, the parts being used haven't changed. But this is certainly a time-bomb.
I would also like to know if there is any way of including multiple versions of a dependency in the output, as you can with NPM, and have each library resolve its own version if there are incompatibilities found. No doubt there's a gradle command for this.
Did you learn anything new as a result of solving this problem?
Yep, loads!
- delimiterOffset(Ljava/lang/String;IILjava/lang/String;)I is a slightly awkward way of describing the expected method signature - it takes two strings, but I guess the names of the arguments are lost.
- How to list the entire dependency graph for a project using gradle.
- Gradle doesn't seem to care about incompatible dependencies and will happily just crash your app.
- I could detect version mismatches with a bit of grep and checking semver compatibility, but you would hope gradle can do that itself?