From c099d9d4a4340d7fea569959a71077fa600d2f47 Mon Sep 17 00:00:00 2001 From: HarshitMadhav Date: Sat, 9 May 2026 02:21:40 +0530 Subject: [PATCH 1/2] fix(android): show payload preview for FormData and one-shot uploads in DevTools (#55764) --- .../react/modules/network/NetworkEventUtil.kt | 64 ++++++++++++-- .../modules/network/NetworkEventUtilTest.kt | 86 +++++++++++++++++++ 2 files changed, 142 insertions(+), 8 deletions(-) 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 8965f140ef6f..982b1153a296 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 @@ -5,8 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions - package com.facebook.react.modules.network import android.os.Bundle @@ -238,7 +236,7 @@ internal object NetworkEventUtil { @JvmStatic fun okHttpHeadersToMap(headers: Headers): Map { val responseHeaders = mutableMapOf() - for (i in 0 until headers.size()) { + for (i in 0 until headers.size) { val headerName = headers.name(i) // multiple values for the same header if (responseHeaders.containsKey(headerName)) { @@ -260,22 +258,23 @@ internal object NetworkEventUtil { val body = (requestBody as? ProgressRequestBody)?.innerBody() ?: requestBody if (body.isOneShot()) { - // Fallback - body cannot be read twice - return "[Preview unavailable]" + // Reading would drain the underlying stream and break the real upload, + // so fall back to a placeholder that includes the byte count when known. + return binaryPartLabel(body) } // MultipartBody does not propagate isOneShot() from its parts, so check each // part explicitly. Reading a one-shot part here would drain the underlying // stream and cause the real request to fail. - if (body is MultipartBody && body.parts().any { it.body().isOneShot() }) { - return "[Preview unavailable]" + if (body is MultipartBody && body.parts.any { it.body.isOneShot() }) { + return previewMultipartWithBinaryParts(body) } return try { val buffer = Buffer() body.writeTo(buffer) - val size = buffer.size() + val size = buffer.size if (size <= MAX_BODY_PREVIEW_SIZE) { buffer.readUtf8() } else { @@ -285,4 +284,53 @@ internal object NetworkEventUtil { "[Preview unavailable]" } } + + private fun previewMultipartWithBinaryParts(body: MultipartBody): String { + val boundary = body.boundary + val out = StringBuilder() + + for (part in body.parts) { + out.append("--").append(boundary).append("\r\n") + + part.headers?.let { headers -> + for (i in 0 until headers.size) { + out.append(headers.name(i)).append(": ").append(headers.value(i)).append("\r\n") + } + } + val partBody = part.body + partBody.contentType()?.let { out.append("Content-Type: ").append(it).append("\r\n") } + out.append("\r\n") + + if (partBody.isOneShot()) { + out.append(binaryPartLabel(partBody)) + } else { + try { + val partBuffer = Buffer() + partBody.writeTo(partBuffer) + out.append(partBuffer.readUtf8()) + } catch (e: IOException) { + out.append("[Preview unavailable]") + } + } + out.append("\r\n") + } + out.append("--").append(boundary).append("--\r\n") + + return if (out.length <= MAX_BODY_PREVIEW_SIZE) { + out.toString() + } else { + out.substring(0, MAX_BODY_PREVIEW_SIZE) + "... (truncated, ${out.length} bytes total)" + } + } + + /** Placeholder for a one-shot body, including the byte count when known. */ + private fun binaryPartLabel(body: RequestBody): String { + val length = + try { + body.contentLength() + } catch (e: IOException) { + -1L + } + return if (length >= 0) "[Binary data, $length bytes]" else "[Binary data]" + } } 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 ba47cddec5a0..2f922179c3c5 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 @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions + package com.facebook.react.modules.network import com.facebook.react.bridge.Arguments @@ -15,7 +17,13 @@ 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.ByteArrayInputStream import java.net.SocketTimeoutException +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.toRequestBody import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before @@ -292,6 +300,84 @@ class NetworkEventUtilTest { assertThat(args.getString(3)).isEqualTo(url) } + @Test + fun testGetRequestBodyPreviewReturnsNullForNullBody() { + assertThat(NetworkEventUtil.getRequestBodyPreview(null)).isNull() + } + + @Test + fun testGetRequestBodyPreviewReturnsBodyForStringRequest() { + val payload = """{"key":"value"}""" + val body = payload.toRequestBody("application/json".toMediaTypeOrNull()) + + assertThat(NetworkEventUtil.getRequestBodyPreview(body)).isEqualTo(payload) + } + + @Test + fun testGetRequestBodyPreviewUnwrapsProgressRequestBody() { + val payload = "hello world" + val inner = payload.toRequestBody("text/plain".toMediaTypeOrNull()) + val wrapped = ProgressRequestBody(inner) { _, _, _ -> } + + assertThat(NetworkEventUtil.getRequestBodyPreview(wrapped)).isEqualTo(payload) + } + + @Test + fun testGetRequestBodyPreviewMultipartWithTextParts() { + val body = + MultipartBody.Builder("test-boundary") + .setType(MultipartBody.FORM) + .addFormDataPart("field1", "value1") + .addFormDataPart("field2", "value2") + .build() + + val preview = NetworkEventUtil.getRequestBodyPreview(body) + + assertThat(preview).isNotNull() + assertThat(preview).contains("--test-boundary") + assertThat(preview).contains("--test-boundary--") + assertThat(preview).contains("name=\"field1\"") + assertThat(preview).contains("value1") + assertThat(preview).contains("name=\"field2\"") + assertThat(preview).contains("value2") + assertThat(preview).doesNotContain("[Preview unavailable]") + } + + @Test + fun testGetRequestBodyPreviewMultipartWithFilePartReplacesBinaryContent() { + val fileBytes = ByteArray(2048) { it.toByte() } + val streamingPart = + RequestBodyUtil.create(MediaType.parse("application/octet-stream"), ByteArrayInputStream(fileBytes)) + val body = + MultipartBody.Builder("test-boundary") + .setType(MultipartBody.FORM) + .addFormDataPart("description", "an image") + .addFormDataPart("file", "photo.jpg", streamingPart) + .build() + + val preview = NetworkEventUtil.getRequestBodyPreview(body) + + assertThat(preview).isNotNull() + assertThat(preview).contains("--test-boundary") + assertThat(preview).contains("name=\"description\"") + assertThat(preview).contains("an image") + assertThat(preview).contains("name=\"file\"") + assertThat(preview).contains("filename=\"photo.jpg\"") + assertThat(preview).contains("[Binary data, 2048 bytes]") + assertThat(preview).doesNotContain("[Preview unavailable]") + } + + @Test + fun testGetRequestBodyPreviewSingleOneShotBodyShowsPlaceholder() { + val fileBytes = ByteArray(512) { it.toByte() } + val body = + RequestBodyUtil.create(MediaType.parse("application/octet-stream"), ByteArrayInputStream(fileBytes)) + + val preview = NetworkEventUtil.getRequestBodyPreview(body) + + assertThat(preview).isEqualTo("[Binary data, 512 bytes]") + } + @Test fun testNullReactContext() { val url = "http://example.com" From b93f953fa968f23dc82c581e5857be5c2ab2e991 Mon Sep 17 00:00:00 2001 From: HarshitMadhav Date: Sat, 9 May 2026 03:10:19 +0530 Subject: [PATCH 2/2] Revert: Suppress Deprecation error for okhttp --- .../react/modules/network/NetworkEventUtil.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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 982b1153a296..dbb0d746ad21 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 @@ -5,6 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +@file:Suppress("DEPRECATION_ERROR") // Conflicting okhttp versions + package com.facebook.react.modules.network import android.os.Bundle @@ -236,7 +238,7 @@ internal object NetworkEventUtil { @JvmStatic fun okHttpHeadersToMap(headers: Headers): Map { val responseHeaders = mutableMapOf() - for (i in 0 until headers.size) { + for (i in 0 until headers.size()) { val headerName = headers.name(i) // multiple values for the same header if (responseHeaders.containsKey(headerName)) { @@ -259,14 +261,14 @@ internal object NetworkEventUtil { if (body.isOneShot()) { // Reading would drain the underlying stream and break the real upload, - // so fall back to a placeholder that includes the byte count when known. + // so fall back to a placeholder that includes the byte count when known return binaryPartLabel(body) } // MultipartBody does not propagate isOneShot() from its parts, so check each // part explicitly. Reading a one-shot part here would drain the underlying // stream and cause the real request to fail. - if (body is MultipartBody && body.parts.any { it.body.isOneShot() }) { + if (body is MultipartBody && body.parts().any { it.body().isOneShot() }) { return previewMultipartWithBinaryParts(body) } @@ -274,7 +276,7 @@ internal object NetworkEventUtil { val buffer = Buffer() body.writeTo(buffer) - val size = buffer.size + val size = buffer.size() if (size <= MAX_BODY_PREVIEW_SIZE) { buffer.readUtf8() } else { @@ -286,18 +288,18 @@ internal object NetworkEventUtil { } private fun previewMultipartWithBinaryParts(body: MultipartBody): String { - val boundary = body.boundary + val boundary = body.boundary() val out = StringBuilder() - for (part in body.parts) { + for (part in body.parts()) { out.append("--").append(boundary).append("\r\n") - part.headers?.let { headers -> - for (i in 0 until headers.size) { + part.headers()?.let { headers -> + for (i in 0 until headers.size()) { out.append(headers.name(i)).append(": ").append(headers.value(i)).append("\r\n") } } - val partBody = part.body + val partBody = part.body() partBody.contentType()?.let { out.append("Content-Type: ").append(it).append("\r\n") } out.append("\r\n")