diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt index 3718d0463261..ead05404002b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/blob/BlobModule.kt @@ -63,7 +63,7 @@ public class BlobModule(reactContext: ReactApplicationContext) : } } - private val networkingUriHandler = + internal val networkingUriHandler = object : NetworkingModule.UriHandler { override fun supports(uri: Uri, responseType: String): Boolean { val scheme = uri.scheme diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt index 997b446b7de3..8f17c9ff41c5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkEventUtil.kt @@ -19,9 +19,7 @@ import com.facebook.react.common.build.ReactBuildConfig import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import java.net.SocketTimeoutException import okhttp3.Headers -import okhttp3.Protocol import okhttp3.Request -import okhttp3.Response /** * Utility class for reporting network lifecycle events to JavaScript and InspectorNetworkReporter. @@ -229,11 +227,12 @@ internal object NetworkEventUtil { requestId: Int, devToolsRequestId: String, requestUrl: String?, - response: Response, + statusCode: Int, + headers: Map, + contentLength: Long, ) { - val headersMap = okHttpHeadersToMap(response.headers()) val headersBundle = Bundle() - for ((headerName, headerValue) in headersMap) { + for ((headerName, headerValue) in headers) { headersBundle.putString(headerName, headerValue) } @@ -241,22 +240,23 @@ internal object NetworkEventUtil { InspectorNetworkReporter.reportResponseStart( devToolsRequestId, requestUrl.orEmpty(), - response.code(), - headersMap, - response.body()?.contentLength() ?: 0, + statusCode, + headers, + contentLength, ) } reactContext?.emitDeviceEvent( "didReceiveNetworkResponse", Arguments.createArray().apply { pushInt(requestId) - pushInt(response.code()) + pushInt(statusCode) pushMap(Arguments.fromBundle(headersBundle)) pushString(requestUrl) }, ) } + // TODO(#55747): Remove this overload once fbsource no longer depends on it. @Deprecated("Compatibility overload") @JvmStatic fun onResponseReceived( @@ -267,14 +267,15 @@ internal object NetworkEventUtil { headers: WritableMap?, url: String?, ) { - val headersBuilder = Headers.Builder() - headers?.let { map -> - val iterator = map.keySetIterator() - while (iterator.hasNextKey()) { - val key = iterator.nextKey() - val value = map.getString(key) - if (value != null) { - headersBuilder.add(key, value) + val responseHeaders = buildMap { + headers?.let { map -> + val iterator = map.keySetIterator() + while (iterator.hasNextKey()) { + val key = iterator.nextKey() + val value = map.getString(key) + if (value != null) { + put(key, value) + } } } } @@ -283,17 +284,12 @@ internal object NetworkEventUtil { requestId, devToolsRequestId, url, - Response.Builder() - .protocol(Protocol.HTTP_1_1) - .request(Request.Builder().url(url.orEmpty()).build()) - .headers(headersBuilder.build()) - .code(statusCode) - .message("") - .build(), - ) + statusCode, + responseHeaders, + 0) } - private fun okHttpHeadersToMap(headers: Headers): Map { + internal fun okHttpHeadersToMap(headers: Headers): Map { val responseHeaders = mutableMapOf() for (i in 0 until headers.size()) { val headerName = headers.name(i) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt index 1d66e629d6df..e4204b538f2a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/network/NetworkingModule.kt @@ -36,7 +36,6 @@ import okhttp3.JavaNetCookieJar import okhttp3.MediaType import okhttp3.MultipartBody import okhttp3.OkHttpClient -import okhttp3.Protocol import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response @@ -310,21 +309,14 @@ public class NetworkingModule( if (handler.supports(uri, responseType)) { val (res, rawBody) = handler.fetch(uri) val encodedDataLength = res.toString().toByteArray().size - // fix: UriHandlers which are not using file:// scheme fail in whatwg-fetch at this line - // https://github.com/JakeChampion/fetch/blob/main/fetch.js#L547 - val response = - Response.Builder() - .protocol(Protocol.HTTP_1_1) - .request(Request.Builder().url(url.orEmpty()).build()) - .code(200) - .message("OK") - .build() NetworkEventUtil.onResponseReceived( reactApplicationContext, requestId, devToolsRequestId, url, - response, + 200, + emptyMap(), + encodedDataLength.toLong(), ) NetworkEventUtil.onDataReceived( reactApplicationContext, @@ -645,7 +637,9 @@ public class NetworkingModule( requestId, devToolsRequestId, response.request().url().toString(), - response, + response.code(), + NetworkEventUtil.okHttpHeadersToMap(response.headers()), + response.body()?.contentLength() ?: 0L, ) try { diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt index a95b50fd8fdb..0cbdab2bc2f7 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/blob/BlobModuleTest.kt @@ -10,7 +10,10 @@ package com.facebook.react.modules.blob import android.net.Uri import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactTestHelper +import com.facebook.testutils.shadows.ShadowArguments +import java.io.ByteArrayInputStream import java.nio.ByteBuffer import java.util.UUID import kotlin.random.Random @@ -20,13 +23,15 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) -@Config(manifest = Config.NONE) +@Config(manifest = Config.NONE, shadows = [ShadowArguments::class]) class BlobModuleTest { private lateinit var bytes: ByteArray private lateinit var blobId: String + private lateinit var context: ReactApplicationContext private lateinit var blobModule: BlobModule @Before @@ -34,7 +39,8 @@ class BlobModuleTest { bytes = ByteArray(120) Random.Default.nextBytes(bytes) - blobModule = BlobModule(ReactTestHelper.createCatalystContextForTest()) + context = ReactTestHelper.createCatalystContextForTest() + blobModule = BlobModule(context) blobId = blobModule.store(bytes) } @@ -136,4 +142,76 @@ class BlobModuleTest { assertThat(blobModule.resolve(blobId, 0, bytes.size)).isNull() } + + @Test + fun testUriHandlerSupportsContentUri() { + val handler = blobModule.networkingUriHandler + val uri = Uri.parse("content://com.example.provider/blob/123") + assertThat(handler.supports(uri, "blob")).isTrue() + } + + @Test + fun testUriHandlerDoesNotSupportContentUriWithNonBlobResponseType() { + val handler = blobModule.networkingUriHandler + val uri = Uri.parse("content://com.example.provider/blob/123") + assertThat(handler.supports(uri, "text")).isFalse() + } + + @Test + fun testUriHandlerDoesNotSupportHttpUri() { + val handler = blobModule.networkingUriHandler + val uri = Uri.parse("http://example.com/blob/123") + assertThat(handler.supports(uri, "blob")).isFalse() + } + + @Test + fun testUriHandlerDoesNotSupportHttpsUri() { + val handler = blobModule.networkingUriHandler + val uri = Uri.parse("https://example.com/blob/123") + assertThat(handler.supports(uri, "blob")).isFalse() + } + + @Test + fun testUriHandlerSupportsFileUriWithBlobResponseType() { + val handler = blobModule.networkingUriHandler + val uri = Uri.parse("file:///storage/emulated/0/Download/test.pdf") + assertThat(handler.supports(uri, "blob")).isTrue() + } + + @Test + fun testUriHandlerFetchesContentUri() { + val testData = "Hello from content provider!".toByteArray() + val contentUri = Uri.parse("content://com.example.provider/files/test.txt") + + val shadowResolver = shadowOf(context.contentResolver) + shadowResolver.registerInputStream(contentUri, ByteArrayInputStream(testData)) + + val handler = blobModule.networkingUriHandler + assertThat(handler.supports(contentUri, "blob")).isTrue() + + val (blob, data) = handler.fetch(contentUri) + assertThat(data).isEqualTo(testData) + assertThat(blob.getInt("offset")).isEqualTo(0) + assertThat(blob.getInt("size")).isEqualTo(testData.size) + assertThat(blob.getString("blobId")).isNotEmpty() + } + + @Test + fun testUriHandlerFetchesFileUri() { + val testData = "Hello from a local file!".toByteArray() + val fileUri = Uri.parse("file:///storage/emulated/0/Download/test.txt") + + val shadowResolver = shadowOf(context.contentResolver) + shadowResolver.registerInputStream(fileUri, ByteArrayInputStream(testData)) + + val handler = blobModule.networkingUriHandler + + assertThat(handler.supports(fileUri, "blob")).isTrue() + + val (blob, data) = handler.fetch(fileUri) + assertThat(data).isEqualTo(testData) + assertThat(blob.getInt("offset")).isEqualTo(0) + assertThat(blob.getInt("size")).isEqualTo(testData.size) + assertThat(blob.getString("blobId")).isNotEmpty() + } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt index 9e344d655662..6f4d4dcd8270 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkEventUtilTest.kt @@ -16,10 +16,6 @@ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.testutils.shadows.ShadowArguments import java.net.SocketTimeoutException -import okhttp3.Headers -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.Response import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -265,24 +261,17 @@ class NetworkEventUtilTest { fun testOnResponseReceived() { val requestId = 1 val statusCode = 200 - val headers = Headers.Builder().add("Content-Type", "application/json").build() + val headersMap = mapOf("Content-Type" to "application/json") val url = "http://example.com" - val request = Request.Builder().url(url).build() - val response = - Response.Builder() - .protocol(Protocol.HTTP_1_1) - .request(request) - .headers(headers) - .code(statusCode) - .message("OK") - .build() NetworkEventUtil.onResponseReceived( reactContext, requestId, "test_devtools_request_$requestId", url, - response, + statusCode, + headersMap, + 0L, ) val eventNameCaptor = ArgumentCaptor.forClass(String::class.java) @@ -306,15 +295,6 @@ class NetworkEventUtilTest { @Test fun testNullReactContext() { val url = "http://example.com" - val request = Request.Builder().url(url).build() - val response = - Response.Builder() - .protocol(Protocol.HTTP_1_1) - .request(request) - .headers(Headers.Builder().build()) - .code(200) - .message("OK") - .build() NetworkEventUtil.onDataSend(null, 1, 100, 1000) NetworkEventUtil.onIncrementalDataReceived( @@ -336,7 +316,7 @@ class NetworkEventUtilTest { ) NetworkEventUtil.onRequestError(null, 1, "test_devtools_request_1", "error", null) NetworkEventUtil.onRequestSuccess(null, 1, "test_devtools_request_1", 0) - NetworkEventUtil.onResponseReceived(null, 1, "test_devtools_request_1", url, response) + NetworkEventUtil.onResponseReceived(null, 1, "test_devtools_request_1", url, 200, emptyMap(), 0L) verify(reactContext, never()).emitDeviceEvent(any(), any()) } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt index f180a4b05dae..47159f88c4d7 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt @@ -10,15 +10,19 @@ package com.facebook.react.modules.network +import android.net.Uri +import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap import com.facebook.react.common.network.OkHttpCallUtil import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsDefaults import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.testutils.shadows.ShadowArguments +import java.io.IOException import java.io.InputStream import java.nio.charset.StandardCharsets import okhttp3.Call @@ -599,6 +603,66 @@ class NetworkingModuleTest { assertThat(requestIdArguments.allValues.contains(idx + 1)).isTrue } } + + @Test + fun testFileUriHandlerEmitsSuccessEvents() { + val testData = "file content".toByteArray() + val blobResponse = + JavaOnlyMap().apply { + putString("blobId", "test-blob-id") + putInt("offset", 0) + putInt("size", testData.size) + } + + val fileUriHandler = + object : NetworkingModule.UriHandler { + override fun supports(uri: Uri, responseType: String): Boolean = + uri.scheme == "file" && responseType == "blob" + + @Throws(IOException::class) + override fun fetch(uri: Uri): Pair = blobResponse to testData + } + + networkingModule.addUriHandler(fileUriHandler) + + networkingModule.sendRequest( + "GET", + "file:///storage/emulated/0/Download/test.pdf", + 1.0, /* requestId */ + JavaOnlyArray.of(), /* headers */ + null, /* body */ + "blob", /* responseType */ + false, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false, /* withCredentials */ + ) + + // Should not have made an OkHttp call; fileUriHandler should have handled the request. + verify(httpClient, times(0)).newCall(any()) + + // Verify the three expected events were emitted in order: + // didReceiveNetworkResponse, didReceiveNetworkData, didCompleteNetworkResponse + val eventNameCaptor = argumentCaptor() + val eventArgsCaptor = argumentCaptor() + verify(context, times(3)) + .emitDeviceEvent(eventNameCaptor.capture(), eventArgsCaptor.capture()) + + assertThat(eventNameCaptor.allValues[0]).isEqualTo("didReceiveNetworkResponse") + assertThat(eventNameCaptor.allValues[1]).isEqualTo("didReceiveNetworkData") + assertThat(eventNameCaptor.allValues[2]).isEqualTo("didCompleteNetworkResponse") + + // Verify response event has status 200, request ID 1, empty headers, and expected URL + val responseArgs = eventArgsCaptor.allValues[0] + assertThat(responseArgs.getInt(0)).isEqualTo(1) + assertThat(responseArgs.getInt(1)).isEqualTo(200) + assertThat(responseArgs.getMap(2)).isEqualTo(JavaOnlyMap.of()) + assertThat(responseArgs.getString(3)).isEqualTo("file:///storage/emulated/0/Download/test.pdf") + + // Verify completion event has request ID 1 and null error + val completionArgs = eventArgsCaptor.allValues[2] + assertThat(completionArgs.getInt(0)).isEqualTo(1) + assertThat(completionArgs.isNull(1)).isTrue() + } } private val FORM = MediaType.get("multipart/form-data")