Open andreialecu opened 2 years ago
@shubhkhanna
For me it solved the problem for IPv6 support on android.
How to implement this in Expo project?
@JKKholmatov never tried in the expo project. These changes are for bare react-native project.
@JKKholmatov
I used a custom plugin with expo/config-plugins
and withAppBuildGradle
, here is an example:
internal/expo-config-plugins/appBuildGradleDependencies.js
):
const { withAppBuildGradle } = require('@expo/config-plugins');
module.exports = withAppBuildGradleDependencies = (config, customName) => { return withAppBuildGradle(config, (config) => { const initialIndex = config.modResults.contents.indexOf("dependencies {");
config.modResults.contents =
config.modResults.contents.slice(0, initialIndex) +
`dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:5.0.0-alpha.11'` +
config.modResults.contents.slice(initialIndex + "dependencies {".length);
return config;
}); };
* Declare the plugin in `app.json`:
```json
"plugins": [
"./internal/expo-config-plugins/appBuildGradleDependencies.js",
It resolved my issue with ipv6 latency on Android
Switching to OkHttp version 5 did not resolve the issue. However, I found a solution by creating a custom module for API calls that only functions when running on Android and the required VPN is active. Below is the implementation:
package <com.yourpackage.test>
import android.net.TrafficStats
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Headers.Companion.toHeaders
import okhttp3.Dns
import okhttp3.Response
import java.io.IOException
import java.net.Inet4Address
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.facebook.react.bridge.Arguments
class OkHttpModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
override fun getName(): String {
return "OkHttpModule"
}
@ReactMethod
fun fetchFromApi(url: String, headersString: String, body: String, promise: Promise) {
// Create OkHttpClient with IPv4-only DNS resolution
val client = OkHttpClient.Builder()
.dns { hostname ->
// Resolve only IPv4 addresses (filter out IPv6 addresses)
Dns.SYSTEM.lookup(hostname).filter { it is Inet4Address }
}
.build()
// Tag the thread for traffic stats tracking
TrafficStats.setThreadStatsTag(10042)
try {
// Convert headers from a string to OkHttp Headers
val headers = parseHeaders(headersString).toHeaders()
// Create the request body
val requestBody = RequestBody.create("application/x-www-form-urlencoded".toMediaTypeOrNull(), body)
// Build the request
val request = Request.Builder()
.url(url)
.headers(headers)
.post(requestBody)
.build()
// Execute the request
val response: Response = client.newCall(request).execute()
val responseData = response.body?.string() ?: ""
// Send result back to JS for logging
sendEventToJS(reactApplicationContext, url, responseData, headers.toMap())
if (!response.isSuccessful) {
promise.reject("Error", "Request failed with code: ${response.code}")
} else {
promise.resolve(responseData)
}
} catch (e: IOException) {
promise.reject("Error", "Request failed: ${e.message}")
} finally {
// Clear the traffic stats tag after the network operation is complete
TrafficStats.clearThreadStatsTag()
}
}
private fun sendEventToJS(reactContext: ReactApplicationContext, url: String, response: String, headers: Map<String, String>) {
val params = Arguments.createMap()
params.putString("url", url)
params.putString("response", response)
params.putString("headers", headers.toString())
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("networkResponse", params)
}
// Helper method to parse headers from "Key: Value\nKey: Value" format into a map
private fun parseHeaders(headersString: String): Map<String, String> {
val headersMap = mutableMapOf<String, String>()
val headersArray = headersString.split("\n")
for (header in headersArray) {
val keyValue = header.split(": ")
if (keyValue.size == 2) {
headersMap[keyValue[0]] = keyValue[1]
}
}
return headersMap
}}
This module ensures the API calls work specifically with IPv4, mitigating connectivity issues observed when using the VPN.
I'd be interested to understand why it breaks on VPN.
If you configure an event listener in the client, you should see what it's doing with DNS and connection attempts.
This should work without this workaround, so it would be good to find the root cause.
@yschimke
The issue is actually rooted in how Android handles connections with the Happy Eyeballs algorithm, which tries both IPv4 and IPv6 connections to improve speed. On certain devices, though, this approach can cause requests to hang indefinitely when there’s an IPv6 host involved—especially problematic when using a VPN that primarily supports IPv4.
To work around this, I created a custom module that forces OkHttp to resolve only IPv4 addresses, which has been effective in preventing the hang. Ideally, we’d look deeper into how Android handles the Happy Eyeballs fallback in VPN contexts and see if we can prevent this hang without restricting to IPv4.
OkHttp tries to
Which section is hanging with a VPN?
@yschimke The hang isn’t actually with OkHttp itself but with fetch in JavaScript, which stalls on IPv6 with some VPN setups on Android. We’re using OkHttp in a custom module to replace fetch on Android, forcing IPv4 resolution to bypass this issue. This could technically be done with other libraries, but OkHttp makes managing DNS settings straightforward and effective here.
Description
This is a very bizarre issue that has been previously reported a bunch of times, and this is basically a continuation of:
https://github.com/facebook/react-native/issues/29608
I initially started running into this on RN 0.66 with AWS Cognito. Bumping to 0.66.3 didn't help.
I'm also pretty sure this used to work before and I'm not sure when it broke. It's on an app that has been shelved for a while.
The problem is very strange because the network request does not seem to be issued, but simply hitting CMD+S to save any file so that a hot-reload is issued will immediately dispatch the network request.
I discovered the promise hanging issue by adding some logs to the
fetch
calls the cognito library was doing: Notice how the.then
is not executed.While troubleshooting I came across a mention here of a workaround: https://github.com/facebook/react-native/issues/29608#issuecomment-884521699 (courtesy of @danmaas) which seems to completely resolve the issue.
Here's the same
.then
correctly being executed after applying that patch:Version
0.66.3
Output of
react-native info
Steps to reproduce
I'm able to reproduce it with this:
Output:
After applying https://github.com/facebook/react-native/issues/29608#issuecomment-884521699:
Snack, code example, screenshot, or link to a repository
No response
Skip to this comment for the actual cause: https://github.com/facebook/react-native/issues/32730#issuecomment-990764376