Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -248,16 +248,17 @@ internal object NetworkEventUtil {
// Unwrap ProgressRequestBody
val body = (requestBody as? ProgressRequestBody)?.innerBody() ?: requestBody

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
if (isBinaryBody(body)) {
// Reading a file would be costly for large uploads, and reading a one-shot body would
// drain it 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() }) {
if (body is MultipartBody && body.parts().any { isBinaryBody(it.body()) }) {
return previewMultipartWithBinaryParts(body)
}

Expand Down Expand Up @@ -292,7 +293,7 @@ internal object NetworkEventUtil {
partBody.contentType()?.let { out.append("Content-Type: ").append(it).append("\r\n") }
out.append("\r\n")

if (partBody.isOneShot()) {
if (isBinaryBody(partBody)) {
out.append(binaryPartLabel(partBody))
} else {
try {
Expand All @@ -314,7 +315,10 @@ internal object NetworkEventUtil {
}
}

/** Placeholder for a one-shot body, including the byte count when known. */
/** Whether the preview should show a placeholder instead of reading [body]. */
private fun isBinaryBody(body: RequestBody): Boolean = body is UriRequestBody || body.isOneShot()

/** Placeholder for a file or one-shot body, including the byte count when known. */
private fun binaryPartLabel(body: RequestBody): String {
val length =
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,9 @@ public class NetworkingModule(
)
return
}
val fileInputStream = RequestBodyUtil.getFileInputStream(getReactApplicationContext(), uri)
if (fileInputStream == null) {
requestBody =
RequestBodyUtil.create(getReactApplicationContext(), MediaType.parse(contentType), uri)
if (requestBody == null) {
NetworkEventUtil.onRequestError(
reactApplicationContext,
requestId,
Expand All @@ -585,7 +586,6 @@ public class NetworkingModule(
)
return
}
requestBody = RequestBodyUtil.create(MediaType.parse(contentType), fileInputStream)
}
data.hasKey(REQUEST_BODY_KEY_FORMDATA) -> {
if (contentType == null) {
Expand Down Expand Up @@ -997,9 +997,9 @@ public class NetworkingModule(
)
return null
}
val fileInputStream =
RequestBodyUtil.getFileInputStream(getReactApplicationContext(), fileContentUriStr)
if (fileInputStream == null) {
val fileBody =
RequestBodyUtil.create(getReactApplicationContext(), partContentType, fileContentUriStr)
if (fileBody == null) {
NetworkEventUtil.onRequestError(
reactApplicationContext,
requestId,
Expand All @@ -1009,7 +1009,7 @@ public class NetworkingModule(
)
return null
}
multipartBuilder.addPart(headers, RequestBodyUtil.create(partContentType, fileInputStream))
multipartBuilder.addPart(headers, fileBody)
} else {
NetworkEventUtil.onRequestError(
reactApplicationContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.URL
import java.nio.channels.Channels
import java.util.concurrent.atomic.AtomicReference
import java.util.zip.GZIPOutputStream
import okhttp3.MediaType
import okhttp3.RequestBody
Expand Down Expand Up @@ -57,7 +59,7 @@ internal object RequestBodyUtil {
val fileContentUri = Uri.parse(fileContentUriStr)

if (fileContentUri.scheme?.startsWith("http") == true) {
return getDownloadFileInputStream(context, fileContentUri)
return FileInputStream(downloadFile(context, fileContentUri))
}

if (fileContentUriStr.startsWith("data:")) {
Expand All @@ -79,12 +81,70 @@ internal object RequestBodyUtil {
}
}

/**
* Creates a [RequestBody] for the file given by its contentUri, which may be a `content://`,
* `file://` or `android.resource://` URI, an `http(s)://` URL or a base64 `data:` URI. Returns
* null if the file has not been found or if an error has occurred.
*
* The body opens a new stream each time it is written after the first, so OkHttp can resend it
* when it retries a request.
*/
@JvmStatic
fun create(context: Context, mediaType: MediaType?, fileContentUriStr: String): RequestBody? {
try {
val fileContentUri = Uri.parse(fileContentUriStr)

if (fileContentUri.scheme?.startsWith("http") == true) {
val file = downloadFile(context, fileContentUri)
return UriRequestBody(mediaType, file.length()) { FileInputStream(file) }
}

if (fileContentUriStr.startsWith("data:")) {
val decodedDataUrString =
Base64.decode(
fileContentUriStr
.split(",".toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()[1],
Base64.DEFAULT,
)
return UriRequestBody(mediaType, decodedDataUrString.size.toLong()) {
ByteArrayInputStream(decodedDataUrString)
}
}

val contentResolver = context.contentResolver
val openStream = {
contentResolver.openInputStream(fileContentUri)
?: throw FileNotFoundException("Could not open $fileContentUriStr")
}
// Opening the file here fails the request early if it can't be read. available() is a guess
// (e.g. 0 for pipe-backed providers); if it's too low, OkHttp fails the upload
val firstStream = openStream()
val contentLength =
try {
firstStream.available().toLong()
} catch (e: IOException) {
firstStream.close()
throw e
}
// The first write uses the stream opened here, so the file is only opened again on a retry
val pendingStream = AtomicReference<InputStream?>(firstStream)
return UriRequestBody(mediaType, contentLength) {
pendingStream.getAndSet(null) ?: openStream()
}
} catch (e: Exception) {
FLog.e(ReactConstants.TAG, "Could not retrieve file for contentUri $fileContentUriStr", e)
return null
}
}

/**
* Download and cache a file locally. This should be used when document picker returns a URI that
* points to a file on the network. Returns input stream for the downloaded file.
* points to a file on the network. Returns the downloaded file.
*/
@Throws(IOException::class)
private fun getDownloadFileInputStream(context: Context, uri: Uri): InputStream {
private fun downloadFile(context: Context, uri: Uri): File {
val outputDir = context.applicationContext.cacheDir
val file = File.createTempFile(NAME, TEMP_FILE_SUFFIX, outputDir)
file.deleteOnExit()
Expand All @@ -94,10 +154,10 @@ internal object RequestBodyUtil {
url.openStream().use { `is` ->
Channels.newChannel(`is`).use { channel ->
stream.channel.transferFrom(channel, 0, Long.MAX_VALUE)
return FileInputStream(file)
}
}
}
return file
}

/** Creates a [RequestBody] from a mediaType and gzip-ed body string. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

@file:Suppress("DEPRECATION_ERROR") // Conflicting okio versions

package com.facebook.react.modules.network

import java.io.IOException
import java.io.InputStream
import okhttp3.MediaType
import okhttp3.RequestBody
import okio.BufferedSink
import okio.Okio

/**
* A [RequestBody] that calls [openStream] for each [writeTo], so OkHttp can send the body again
* when it retries a request, e.g. after a pooled connection turns out to have been closed by the
* server.
*
* @param contentLength the length in bytes, or -1 if unknown
* @param openStream returns a stream positioned at the start of the content
*/
internal class UriRequestBody(
private val mediaType: MediaType?,
private val contentLength: Long,
private val openStream: () -> InputStream,
) : RequestBody() {

override fun contentType(): MediaType? = mediaType

override fun contentLength(): Long = contentLength

@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
val inputStream =
try {
openStream()
} catch (e: Exception) {
// Opening a content:// stream can throw e.g. SecurityException. OkHttp reports other
// exceptions to onFailure too, but then rethrows them on its dispatcher thread, which
// crashes the app
throw e as? IOException ?: IOException("Could not open request body", e)
}
Okio.source(inputStream).use { source -> sink.writeAll(source) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,45 @@ class NetworkEventUtilTest {
assertThat(stream.available()).isEqualTo(fileBytes.size)
}

@Test
fun testGetRequestBodyPreviewDoesNotReadSingleUriBody() {
// A UriRequestBody isn't one-shot, but reading it would read the whole file just to build
// the preview, so it must get the same placeholder
var opened = false
val body =
UriRequestBody(MediaType.parse("application/octet-stream"), 512) {
opened = true
ByteArrayInputStream(ByteArray(512))
}

val preview = NetworkEventUtil.getRequestBodyPreview(body)

assertThat(preview).isEqualTo("[Binary data, 512 bytes]")
assertThat(opened).isFalse()
}

@Test
fun testGetRequestBodyPreviewDoesNotReadMultipartUriPart() {
var opened = false
val filePart =
UriRequestBody(MediaType.parse("application/octet-stream"), 2048) {
opened = true
ByteArrayInputStream(ByteArray(2048))
}
val body =
MultipartBody.Builder("test-boundary")
.setType(MultipartBody.FORM)
.addFormDataPart("description", "an image")
.addFormDataPart("file", "photo.jpg", filePart)
.build()

val preview = NetworkEventUtil.getRequestBodyPreview(body)

assertThat(preview).contains("an image")
assertThat(preview).contains("[Binary data, 2048 bytes]")
assertThat(opened).isFalse()
}

@Test
fun testNullReactContext() {
val url = "http://example.com"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import com.facebook.react.common.network.OkHttpCallUtil
import com.facebook.testutils.shadows.ShadowArguments
import com.facebook.testutils.shadows.ShadowInspectorNetworkReporter
import com.facebook.testutils.shadows.ShadowSoLoader
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
import java.nio.charset.StandardCharsets
import okhttp3.Call
import okhttp3.Headers
Expand Down Expand Up @@ -414,14 +414,14 @@ class NetworkingModuleTest {

@Test
fun testMultipartPostRequestBody() {
val inputStream = mock<InputStream>()
whenever(inputStream.available()).thenReturn("imageUri".length)
setupRequestBodyUtil()
with(requestBodyUtil) {
`when`<InputStream> { RequestBodyUtil.getFileInputStream(any(), any()) }
.thenReturn(inputStream)
`when`<RequestBody> { RequestBodyUtil.create(any(), any()) }.thenCallRealMethod()
}
requestBodyUtil
.`when`<RequestBody> { RequestBodyUtil.create(any(), any(), any()) }
.thenAnswer { invocation ->
UriRequestBody(invocation.getArgument(1), "imageUri".length.toLong()) {
ByteArrayInputStream("imageUri".toByteArray())
}
}
val multipartBodyBuilderMock =
mockConstruction(MultipartBody.Builder::class.java) { mock, _ ->
whenever(mock.setType(any())).thenReturn(mock)
Expand Down Expand Up @@ -473,8 +473,9 @@ class NetworkingModuleTest {
)

// verify RequestBodyPart for image
requestBodyUtil.verify { RequestBodyUtil.getFileInputStream(any(), eq("imageUri")) }
requestBodyUtil.verify { RequestBodyUtil.create(eq(MediaType.parse("image/jpg")), any()) }
requestBodyUtil.verify {
RequestBodyUtil.create(any(), eq(MediaType.parse("image/jpg")), eq("imageUri"))
}

// verify body
val multipartBuilder = multipartBodyBuilderMock.constructed()[0]
Expand Down
Loading
Loading