Skip to content

Commit 31163eb

Browse files
bhamiltoncxzoontek
authored andcommitted
Fix regression fetching file:// URI scheme in fetch() and XMLHttpRequest requests on Android (facebook#54626) (facebook#55706)
Summary: `fetch()` on `file://` URIs is broken on RN Android, but works on RN iOS (facebook#54626). On Android, `NetworkingModule` powers the implementation of `XMLHttpRequest`, which is the implementation of `fetch()`. iOS's underlying implementation `RCTNetworking` uses `RCTFileRequestHandler` which correctly supports `file://` URIs: https://github.com/facebook/react-native/blob/56e1710b73f0011516872bc6a62e52626588839c/packages/react-native/Libraries/Network/RCTFileRequestHandler.mm#L44 Android's equivalent `BlobModule` works correctly to handle `file://` URIs, but `NetworkingModule` was changed in facebook#52485 to construct a `okhttp3.Response`, passing in an `okhttp3.Request` using the `file://` URI. Unfortunately, `okhttp3.Request.Builder.url()` throws `InvalidArgumentException` whenever given non-HTTP/HTTPS URLs, which caused facebook#54626. The fix is pretty simple: clean up the behavior changed in facebook#52485 to just pass in the data needed (status code, headers, and content length) rather than trying to wrap it in an `okhttp3.Response`. ## Changelog: [ANDROID] [FIXED] - file:// URIs passed to fetch() or XMLHttpRequest no longer fail (facebook#54626) Pull Request resolved: facebook#55706 Test Plan: Unit tests updated. Ran tests with: ``` ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \ --tests "com.facebook.react.modules.blob.BlobModuleTest" \ --tests "com.facebook.react.modules.network.NetworkEventUtilTest" \ --tests "com.facebook.react.modules.network.NetworkingModuleTest" ``` Reviewed By: javache Differential Revision: D94377526 Pulled By: huntie fbshipit-source-id: 87a9c15cf32c230b916abec2954f28c2a0e444fd
1 parent 67c05e1 commit 31163eb

File tree

6 files changed

+182
-59
lines changed

6 files changed

+182
-59
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class BlobModule(reactContext: ReactApplicationContext) :
6363
}
6464
}
6565

66-
private val networkingUriHandler =
66+
internal val networkingUriHandler =
6767
object : NetworkingModule.UriHandler {
6868
override fun supports(uri: Uri, responseType: String): Boolean {
6969
val scheme = uri.scheme

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ import com.facebook.react.common.build.ReactBuildConfig
1919
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags
2020
import java.net.SocketTimeoutException
2121
import okhttp3.Headers
22-
import okhttp3.Protocol
2322
import okhttp3.Request
24-
import okhttp3.Response
2523

2624
/**
2725
* Utility class for reporting network lifecycle events to JavaScript and InspectorNetworkReporter.
@@ -229,28 +227,29 @@ internal object NetworkEventUtil {
229227
requestId: Int,
230228
devToolsRequestId: String,
231229
requestUrl: String?,
232-
response: Response,
230+
statusCode: Int,
231+
headers: Map<String, String>,
232+
contentLength: Long,
233233
) {
234-
val headersMap = okHttpHeadersToMap(response.headers())
235234
val headersBundle = Bundle()
236-
for ((headerName, headerValue) in headersMap) {
235+
for ((headerName, headerValue) in headers) {
237236
headersBundle.putString(headerName, headerValue)
238237
}
239238

240239
if (ReactNativeFeatureFlags.enableNetworkEventReporting()) {
241240
InspectorNetworkReporter.reportResponseStart(
242241
devToolsRequestId,
243242
requestUrl.orEmpty(),
244-
response.code(),
245-
headersMap,
246-
response.body()?.contentLength() ?: 0,
243+
statusCode,
244+
headers,
245+
contentLength,
247246
)
248247
}
249248
reactContext?.emitDeviceEvent(
250249
"didReceiveNetworkResponse",
251250
Arguments.createArray().apply {
252251
pushInt(requestId)
253-
pushInt(response.code())
252+
pushInt(statusCode)
254253
pushMap(Arguments.fromBundle(headersBundle))
255254
pushString(requestUrl)
256255
},
@@ -267,33 +266,35 @@ internal object NetworkEventUtil {
267266
headers: WritableMap?,
268267
url: String?,
269268
) {
270-
val headersBuilder = Headers.Builder()
269+
val headersMap = mutableMapOf<String, String>()
271270
headers?.let { map ->
272271
val iterator = map.keySetIterator()
273272
while (iterator.hasNextKey()) {
274273
val key = iterator.nextKey()
275274
val value = map.getString(key)
276275
if (value != null) {
277-
headersBuilder.add(key, value)
276+
headersMap[key] = value
278277
}
279278
}
280279
}
280+
281+
val contentLength =
282+
headersMap["Content-Length"]?.toLongOrNull()
283+
?: headersMap["content-length"]?.toLongOrNull()
284+
?: 0L
285+
281286
onResponseReceived(
282287
reactContext,
283288
requestId,
284289
devToolsRequestId,
285290
url,
286-
Response.Builder()
287-
.protocol(Protocol.HTTP_1_1)
288-
.request(Request.Builder().url(url.orEmpty()).build())
289-
.headers(headersBuilder.build())
290-
.code(statusCode)
291-
.message("")
292-
.build(),
291+
statusCode,
292+
headersMap,
293+
contentLength,
293294
)
294295
}
295296

296-
private fun okHttpHeadersToMap(headers: Headers): Map<String, String> {
297+
internal fun okHttpHeadersToMap(headers: Headers): Map<String, String> {
297298
val responseHeaders = mutableMapOf<String, String>()
298299
for (i in 0 until headers.size()) {
299300
val headerName = headers.name(i)

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import okhttp3.JavaNetCookieJar
3636
import okhttp3.MediaType
3737
import okhttp3.MultipartBody
3838
import okhttp3.OkHttpClient
39-
import okhttp3.Protocol
4039
import okhttp3.Request
4140
import okhttp3.RequestBody
4241
import okhttp3.Response
@@ -310,21 +309,14 @@ public class NetworkingModule(
310309
if (handler.supports(uri, responseType)) {
311310
val (res, rawBody) = handler.fetch(uri)
312311
val encodedDataLength = res.toString().toByteArray().size
313-
// fix: UriHandlers which are not using file:// scheme fail in whatwg-fetch at this line
314-
// https://github.com/JakeChampion/fetch/blob/main/fetch.js#L547
315-
val response =
316-
Response.Builder()
317-
.protocol(Protocol.HTTP_1_1)
318-
.request(Request.Builder().url(url.orEmpty()).build())
319-
.code(200)
320-
.message("OK")
321-
.build()
322312
NetworkEventUtil.onResponseReceived(
323313
reactApplicationContext,
324314
requestId,
325315
devToolsRequestId,
326316
url,
327-
response,
317+
200,
318+
emptyMap(),
319+
encodedDataLength.toLong(),
328320
)
329321
NetworkEventUtil.onDataReceived(
330322
reactApplicationContext,
@@ -645,7 +637,9 @@ public class NetworkingModule(
645637
requestId,
646638
devToolsRequestId,
647639
response.request().url().toString(),
648-
response,
640+
response.code(),
641+
NetworkEventUtil.okHttpHeadersToMap(response.headers()),
642+
response.body()?.contentLength() ?: 0L,
649643
)
650644

651645
try {

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ package com.facebook.react.modules.blob
1010
import android.net.Uri
1111
import com.facebook.react.bridge.JavaOnlyArray
1212
import com.facebook.react.bridge.JavaOnlyMap
13+
import com.facebook.react.bridge.ReactApplicationContext
1314
import com.facebook.react.bridge.ReactTestHelper
15+
import com.facebook.testutils.shadows.ShadowArguments
16+
import java.io.ByteArrayInputStream
1417
import java.nio.ByteBuffer
1518
import java.util.UUID
1619
import kotlin.random.Random
@@ -20,21 +23,24 @@ import org.junit.Before
2023
import org.junit.Test
2124
import org.junit.runner.RunWith
2225
import org.robolectric.RobolectricTestRunner
26+
import org.robolectric.Shadows.shadowOf
2327
import org.robolectric.annotation.Config
2428

2529
@RunWith(RobolectricTestRunner::class)
26-
@Config(manifest = Config.NONE)
30+
@Config(manifest = Config.NONE, shadows = [ShadowArguments::class])
2731
class BlobModuleTest {
2832
private lateinit var bytes: ByteArray
2933
private lateinit var blobId: String
34+
private lateinit var context: ReactApplicationContext
3035
private lateinit var blobModule: BlobModule
3136

3237
@Before
3338
fun prepareModules() {
3439
bytes = ByteArray(120)
3540
Random.Default.nextBytes(bytes)
3641

37-
blobModule = BlobModule(ReactTestHelper.createCatalystContextForTest())
42+
context = ReactTestHelper.createCatalystContextForTest()
43+
blobModule = BlobModule(context)
3844
blobId = blobModule.store(bytes)
3945
}
4046

@@ -136,4 +142,76 @@ class BlobModuleTest {
136142

137143
assertThat(blobModule.resolve(blobId, 0, bytes.size)).isNull()
138144
}
145+
146+
@Test
147+
fun testUriHandlerSupportsContentUri() {
148+
val handler = blobModule.networkingUriHandler
149+
val uri = Uri.parse("content://com.example.provider/blob/123")
150+
assertThat(handler.supports(uri, "blob")).isTrue()
151+
}
152+
153+
@Test
154+
fun testUriHandlerDoesNotSupportContentUriWithNonBlobResponseType() {
155+
val handler = blobModule.networkingUriHandler
156+
val uri = Uri.parse("content://com.example.provider/blob/123")
157+
assertThat(handler.supports(uri, "text")).isFalse()
158+
}
159+
160+
@Test
161+
fun testUriHandlerDoesNotSupportHttpUri() {
162+
val handler = blobModule.networkingUriHandler
163+
val uri = Uri.parse("http://example.com/blob/123")
164+
assertThat(handler.supports(uri, "blob")).isFalse()
165+
}
166+
167+
@Test
168+
fun testUriHandlerDoesNotSupportHttpsUri() {
169+
val handler = blobModule.networkingUriHandler
170+
val uri = Uri.parse("https://example.com/blob/123")
171+
assertThat(handler.supports(uri, "blob")).isFalse()
172+
}
173+
174+
@Test
175+
fun testUriHandlerSupportsFileUriWithBlobResponseType() {
176+
val handler = blobModule.networkingUriHandler
177+
val uri = Uri.parse("file:///storage/emulated/0/Download/test.pdf")
178+
assertThat(handler.supports(uri, "blob")).isTrue()
179+
}
180+
181+
@Test
182+
fun testUriHandlerFetchesContentUri() {
183+
val testData = "Hello from content provider!".toByteArray()
184+
val contentUri = Uri.parse("content://com.example.provider/files/test.txt")
185+
186+
val shadowResolver = shadowOf(context.contentResolver)
187+
shadowResolver.registerInputStream(contentUri, ByteArrayInputStream(testData))
188+
189+
val handler = blobModule.networkingUriHandler
190+
assertThat(handler.supports(contentUri, "blob")).isTrue()
191+
192+
val (blob, data) = handler.fetch(contentUri)
193+
assertThat(data).isEqualTo(testData)
194+
assertThat(blob.getInt("offset")).isEqualTo(0)
195+
assertThat(blob.getInt("size")).isEqualTo(testData.size)
196+
assertThat(blob.getString("blobId")).isNotEmpty()
197+
}
198+
199+
@Test
200+
fun testUriHandlerFetchesFileUri() {
201+
val testData = "Hello from a local file!".toByteArray()
202+
val fileUri = Uri.parse("file:///storage/emulated/0/Download/test.txt")
203+
204+
val shadowResolver = shadowOf(context.contentResolver)
205+
shadowResolver.registerInputStream(fileUri, ByteArrayInputStream(testData))
206+
207+
val handler = blobModule.networkingUriHandler
208+
209+
assertThat(handler.supports(fileUri, "blob")).isTrue()
210+
211+
val (blob, data) = handler.fetch(fileUri)
212+
assertThat(data).isEqualTo(testData)
213+
assertThat(blob.getInt("offset")).isEqualTo(0)
214+
assertThat(blob.getInt("size")).isEqualTo(testData.size)
215+
assertThat(blob.getString("blobId")).isNotEmpty()
216+
}
139217
}

packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults
1616
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
1717
import com.facebook.testutils.shadows.ShadowArguments
1818
import java.net.SocketTimeoutException
19-
import okhttp3.Headers
20-
import okhttp3.Protocol
21-
import okhttp3.Request
22-
import okhttp3.Response
2319
import org.assertj.core.api.Assertions.assertThat
2420
import org.junit.After
2521
import org.junit.Before
@@ -265,24 +261,17 @@ class NetworkEventUtilTest {
265261
fun testOnResponseReceived() {
266262
val requestId = 1
267263
val statusCode = 200
268-
val headers = Headers.Builder().add("Content-Type", "application/json").build()
264+
val headersMap = mapOf("Content-Type" to "application/json")
269265
val url = "http://example.com"
270266

271-
val request = Request.Builder().url(url).build()
272-
val response =
273-
Response.Builder()
274-
.protocol(Protocol.HTTP_1_1)
275-
.request(request)
276-
.headers(headers)
277-
.code(statusCode)
278-
.message("OK")
279-
.build()
280267
NetworkEventUtil.onResponseReceived(
281268
reactContext,
282269
requestId,
283270
"test_devtools_request_$requestId",
284271
url,
285-
response,
272+
statusCode,
273+
headersMap,
274+
0L,
286275
)
287276

288277
val eventNameCaptor = ArgumentCaptor.forClass(String::class.java)
@@ -306,15 +295,6 @@ class NetworkEventUtilTest {
306295
@Test
307296
fun testNullReactContext() {
308297
val url = "http://example.com"
309-
val request = Request.Builder().url(url).build()
310-
val response =
311-
Response.Builder()
312-
.protocol(Protocol.HTTP_1_1)
313-
.request(request)
314-
.headers(Headers.Builder().build())
315-
.code(200)
316-
.message("OK")
317-
.build()
318298

319299
NetworkEventUtil.onDataSend(null, 1, 100, 1000)
320300
NetworkEventUtil.onIncrementalDataReceived(
@@ -336,7 +316,15 @@ class NetworkEventUtilTest {
336316
)
337317
NetworkEventUtil.onRequestError(null, 1, "test_devtools_request_1", "error", null)
338318
NetworkEventUtil.onRequestSuccess(null, 1, "test_devtools_request_1", 0)
339-
NetworkEventUtil.onResponseReceived(null, 1, "test_devtools_request_1", url, response)
319+
NetworkEventUtil.onResponseReceived(
320+
null,
321+
1,
322+
"test_devtools_request_1",
323+
url,
324+
200,
325+
emptyMap(),
326+
0L,
327+
)
340328

341329
verify(reactContext, never()).emitDeviceEvent(any<String>(), any())
342330
}

0 commit comments

Comments
 (0)