diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt index bd84c1c..a806130 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/AutoPrefetcher.kt @@ -197,7 +197,6 @@ object AutoPrefetcher { put("headers", headersObj) if (method != null && method.isNotEmpty() && method != "GET") put("method", method) if (bodyString != null) put("bodyString", bodyString) - if (bodyBytes != null) put("bodyBytes", bodyBytes) if (!bodyFormData.isNullOrEmpty()) { val arr = JSONArray() bodyFormData.forEach { part -> @@ -233,9 +232,6 @@ object AutoPrefetcher { ?.takeIf { it.has("bodyString") && !it.isNull("bodyString") } ?.optString("bodyString") val bodyString = injectBodyFields(rawBodyString, tokens.bodyFields) - val bodyBytes = entry - ?.takeIf { it.has("bodyBytes") && !it.isNull("bodyBytes") } - ?.optString("bodyBytes") val timeoutMs = entry ?.takeIf { it.has("timeoutMs") && !it.isNull("timeoutMs") } ?.optDouble("timeoutMs") @@ -267,7 +263,7 @@ object AutoPrefetcher { method = method, headers = headerObjs, bodyString = bodyString, - bodyBytes = bodyBytes, + bodyBytes = null, bodyFormData = bodyFormData, timeoutMs = timeoutMs, followRedirects = followRedirects, diff --git a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt index 0274807..5bc7819 100644 --- a/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt +++ b/packages/react-native-nitro-fetch/android/src/main/java/com/margelo/nitro/nitrofetch/NitroFetchClient.kt @@ -285,7 +285,7 @@ class NitroFetchClient(private val engine: CronetEngine, private val executor: E val bodyStr = req.bodyString if ((bodyBytes != null) || !bodyStr.isNullOrEmpty()) { val body: ByteArray = when { - bodyBytes != null -> ByteArray(1) + bodyBytes != null -> bodyBytes.getBuffer(true).toByteArray() !bodyStr.isNullOrEmpty() -> bodyStr!!.toByteArray(Charsets.UTF_8) else -> ByteArray(0) } diff --git a/packages/react-native-nitro-fetch/ios/NitroAutoPrefetcher.swift b/packages/react-native-nitro-fetch/ios/NitroAutoPrefetcher.swift index 85ff074..2ca339e 100644 --- a/packages/react-native-nitro-fetch/ios/NitroAutoPrefetcher.swift +++ b/packages/react-native-nitro-fetch/ios/NitroAutoPrefetcher.swift @@ -217,7 +217,6 @@ public final class NitroAutoPrefetcher: NSObject { ] if let method = method, !method.isEmpty, method != "GET" { entry["method"] = method } if let bodyString = bodyString { entry["bodyString"] = bodyString } - if let bodyBytes = bodyBytes { entry["bodyBytes"] = bodyBytes } if let parts = bodyFormData, !parts.isEmpty { entry["bodyFormData"] = parts.map { part -> [String: String] in var clean: [String: String] = [:] @@ -244,7 +243,6 @@ public final class NitroAutoPrefetcher: NSObject { let methodStr = entry["method"] as? String let method: NitroRequestMethod? = methodStr.flatMap { NitroRequestMethod(fromString: $0) } let bodyString = injectBodyFields(entry["bodyString"] as? String, fields: tokens.bodyFields) - let bodyBytes = entry["bodyBytes"] as? String let timeoutMs = (entry["timeoutMs"] as? NSNumber)?.doubleValue let followRedirects = (entry["followRedirects"] as? Bool) ?? true let prefetchCacheTtlMs = (entry["prefetchCacheTtlMs"] as? NSNumber)?.doubleValue @@ -265,7 +263,7 @@ public final class NitroAutoPrefetcher: NSObject { method: method, headers: mergedHeaders, bodyString: bodyString, - bodyBytes: bodyBytes, + bodyBytes: nil, bodyFormData: formData, timeoutMs: timeoutMs, followRedirects: followRedirects, diff --git a/packages/react-native-nitro-fetch/ios/NitroFetchClient.swift b/packages/react-native-nitro-fetch/ios/NitroFetchClient.swift index ad3c9e3..bf688d2 100644 --- a/packages/react-native-nitro-fetch/ios/NitroFetchClient.swift +++ b/packages/react-native-nitro-fetch/ios/NitroFetchClient.swift @@ -331,6 +331,8 @@ final class NitroFetchClient: HybridNitroFetchClientSpec { r.setValue(contentType, forHTTPHeaderField: "Content-Type") } else if let s = req.bodyString { r.httpBody = s.data(using: .utf8) + } else if let bytes = req.bodyBytes { + r.httpBody = bytes.toData(copyIfNeeded: true) } if let t = req.timeoutMs, t > 0 { r.timeoutInterval = TimeInterval(t) / 1000.0 } return (r, nil) diff --git a/packages/react-native-nitro-fetch/nitrogen/generated/android/c++/JNitroRequest.hpp b/packages/react-native-nitro-fetch/nitrogen/generated/android/c++/JNitroRequest.hpp index ec36e9a..fa01979 100644 --- a/packages/react-native-nitro-fetch/nitrogen/generated/android/c++/JNitroRequest.hpp +++ b/packages/react-native-nitro-fetch/nitrogen/generated/android/c++/JNitroRequest.hpp @@ -16,6 +16,8 @@ #include "NitroFormDataPart.hpp" #include "NitroHeader.hpp" #include "NitroRequestMethod.hpp" +#include +#include #include #include #include @@ -47,8 +49,8 @@ namespace margelo::nitro::nitrofetch { jni::local_ref> headers = this->getFieldValue(fieldHeaders); static const auto fieldBodyString = clazz->getField("bodyString"); jni::local_ref bodyString = this->getFieldValue(fieldBodyString); - static const auto fieldBodyBytes = clazz->getField("bodyBytes"); - jni::local_ref bodyBytes = this->getFieldValue(fieldBodyBytes); + static const auto fieldBodyBytes = clazz->getField("bodyBytes"); + jni::local_ref bodyBytes = this->getFieldValue(fieldBodyBytes); static const auto fieldBodyFormData = clazz->getField>("bodyFormData"); jni::local_ref> bodyFormData = this->getFieldValue(fieldBodyFormData); static const auto fieldTimeoutMs = clazz->getField("timeoutMs"); @@ -73,7 +75,7 @@ namespace margelo::nitro::nitrofetch { return __vector; }()) : std::nullopt, bodyString != nullptr ? std::make_optional(bodyString->toStdString()) : std::nullopt, - bodyBytes != nullptr ? std::make_optional(bodyBytes->toStdString()) : std::nullopt, + bodyBytes != nullptr ? std::make_optional(bodyBytes->cthis()->getArrayBuffer()) : std::nullopt, bodyFormData != nullptr ? std::make_optional([&]() { size_t __size = bodyFormData->size(); std::vector __vector; @@ -97,7 +99,7 @@ namespace margelo::nitro::nitrofetch { */ [[maybe_unused]] static jni::local_ref fromCpp(const NitroRequest& value) { - using JSignature = JNitroRequest(jni::alias_ref, jni::alias_ref, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref); + using JSignature = JNitroRequest(jni::alias_ref, jni::alias_ref, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref); static const auto clazz = javaClassStatic(); static const auto create = clazz->getStaticMethod("fromCpp"); return create( @@ -115,7 +117,7 @@ namespace margelo::nitro::nitrofetch { return __array; }() : nullptr, value.bodyString.has_value() ? jni::make_jstring(value.bodyString.value()) : nullptr, - value.bodyBytes.has_value() ? jni::make_jstring(value.bodyBytes.value()) : nullptr, + value.bodyBytes.has_value() ? JArrayBuffer::wrap(value.bodyBytes.value()) : nullptr, value.bodyFormData.has_value() ? [&]() { size_t __size = value.bodyFormData.value().size(); jni::local_ref> __array = jni::JArrayClass::newArray(__size); diff --git a/packages/react-native-nitro-fetch/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt b/packages/react-native-nitro-fetch/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt index 6958dfb..3fc20bd 100644 --- a/packages/react-native-nitro-fetch/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt +++ b/packages/react-native-nitro-fetch/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrofetch/NitroRequest.kt @@ -9,7 +9,7 @@ package com.margelo.nitro.nitrofetch import androidx.annotation.Keep import com.facebook.proguard.annotations.DoNotStrip - +import com.margelo.nitro.core.ArrayBuffer /** * Represents the JavaScript object/struct "NitroRequest". @@ -31,7 +31,7 @@ data class NitroRequest( val bodyString: String?, @DoNotStrip @Keep - val bodyBytes: String?, + val bodyBytes: ArrayBuffer?, @DoNotStrip @Keep val bodyFormData: Array?, @@ -58,7 +58,7 @@ data class NitroRequest( @Keep @Suppress("unused") @JvmStatic - private fun fromCpp(url: String, method: NitroRequestMethod?, headers: Array?, bodyString: String?, bodyBytes: String?, bodyFormData: Array?, timeoutMs: Double?, followRedirects: Boolean?, prefetchCacheTtlMs: Double?, requestId: String?): NitroRequest { + private fun fromCpp(url: String, method: NitroRequestMethod?, headers: Array?, bodyString: String?, bodyBytes: ArrayBuffer?, bodyFormData: Array?, timeoutMs: Double?, followRedirects: Boolean?, prefetchCacheTtlMs: Double?, requestId: String?): NitroRequest { return NitroRequest(url, method, headers, bodyString, bodyBytes, bodyFormData, timeoutMs, followRedirects, prefetchCacheTtlMs, requestId) } } diff --git a/packages/react-native-nitro-fetch/nitrogen/generated/ios/swift/NitroRequest.swift b/packages/react-native-nitro-fetch/nitrogen/generated/ios/swift/NitroRequest.swift index ec4bf8c..e86d95b 100644 --- a/packages/react-native-nitro-fetch/nitrogen/generated/ios/swift/NitroRequest.swift +++ b/packages/react-native-nitro-fetch/nitrogen/generated/ios/swift/NitroRequest.swift @@ -18,7 +18,7 @@ public extension NitroRequest { /** * Create a new instance of `NitroRequest`. */ - init(url: String, method: NitroRequestMethod?, headers: [NitroHeader]?, bodyString: String?, bodyBytes: String?, bodyFormData: [NitroFormDataPart]?, timeoutMs: Double?, followRedirects: Bool?, prefetchCacheTtlMs: Double?, requestId: String?) { + init(url: String, method: NitroRequestMethod?, headers: [NitroHeader]?, bodyString: String?, bodyBytes: ArrayBuffer?, bodyFormData: [NitroFormDataPart]?, timeoutMs: Double?, followRedirects: Bool?, prefetchCacheTtlMs: Double?, requestId: String?) { self.init(std.string(url), { () -> bridge.std__optional_NitroRequestMethod_ in if let __unwrappedValue = method { return bridge.create_std__optional_NitroRequestMethod_(__unwrappedValue) @@ -43,9 +43,9 @@ public extension NitroRequest { } else { return .init() } - }(), { () -> bridge.std__optional_std__string_ in + }(), { () -> bridge.std__optional_std__shared_ptr_ArrayBuffer__ in if let __unwrappedValue = bodyBytes { - return bridge.create_std__optional_std__string_(std.string(__unwrappedValue)) + return bridge.create_std__optional_std__shared_ptr_ArrayBuffer__(__unwrappedValue.getArrayBuffer()) } else { return .init() } @@ -132,11 +132,11 @@ public extension NitroRequest { } @inline(__always) - var bodyBytes: String? { - return { () -> String? in - if bridge.has_value_std__optional_std__string_(self.__bodyBytes) { - let __unwrapped = bridge.get_std__optional_std__string_(self.__bodyBytes) - return String(__unwrapped) + var bodyBytes: ArrayBuffer? { + return { () -> ArrayBuffer? in + if bridge.has_value_std__optional_std__shared_ptr_ArrayBuffer__(self.__bodyBytes) { + let __unwrapped = bridge.get_std__optional_std__shared_ptr_ArrayBuffer__(self.__bodyBytes) + return ArrayBuffer(__unwrapped) } else { return nil } diff --git a/packages/react-native-nitro-fetch/nitrogen/generated/shared/c++/NitroRequest.hpp b/packages/react-native-nitro-fetch/nitrogen/generated/shared/c++/NitroRequest.hpp index 72f3ba0..e2168ec 100644 --- a/packages/react-native-nitro-fetch/nitrogen/generated/shared/c++/NitroRequest.hpp +++ b/packages/react-native-nitro-fetch/nitrogen/generated/shared/c++/NitroRequest.hpp @@ -40,6 +40,7 @@ namespace margelo::nitro::nitrofetch { struct NitroFormDataPart; } #include #include "NitroHeader.hpp" #include +#include #include "NitroFormDataPart.hpp" namespace margelo::nitro::nitrofetch { @@ -53,7 +54,7 @@ namespace margelo::nitro::nitrofetch { std::optional method SWIFT_PRIVATE; std::optional> headers SWIFT_PRIVATE; std::optional bodyString SWIFT_PRIVATE; - std::optional bodyBytes SWIFT_PRIVATE; + std::optional> bodyBytes SWIFT_PRIVATE; std::optional> bodyFormData SWIFT_PRIVATE; std::optional timeoutMs SWIFT_PRIVATE; std::optional followRedirects SWIFT_PRIVATE; @@ -62,7 +63,7 @@ namespace margelo::nitro::nitrofetch { public: NitroRequest() = default; - explicit NitroRequest(std::string url, std::optional method, std::optional> headers, std::optional bodyString, std::optional bodyBytes, std::optional> bodyFormData, std::optional timeoutMs, std::optional followRedirects, std::optional prefetchCacheTtlMs, std::optional requestId): url(url), method(method), headers(headers), bodyString(bodyString), bodyBytes(bodyBytes), bodyFormData(bodyFormData), timeoutMs(timeoutMs), followRedirects(followRedirects), prefetchCacheTtlMs(prefetchCacheTtlMs), requestId(requestId) {} + explicit NitroRequest(std::string url, std::optional method, std::optional> headers, std::optional bodyString, std::optional> bodyBytes, std::optional> bodyFormData, std::optional timeoutMs, std::optional followRedirects, std::optional prefetchCacheTtlMs, std::optional requestId): url(url), method(method), headers(headers), bodyString(bodyString), bodyBytes(bodyBytes), bodyFormData(bodyFormData), timeoutMs(timeoutMs), followRedirects(followRedirects), prefetchCacheTtlMs(prefetchCacheTtlMs), requestId(requestId) {} public: friend bool operator==(const NitroRequest& lhs, const NitroRequest& rhs) = default; @@ -82,7 +83,7 @@ namespace margelo::nitro { JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "method"))), JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "headers"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyString"))), - JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyBytes"))), + JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyBytes"))), JSIConverter>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyFormData"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timeoutMs"))), JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "followRedirects"))), @@ -96,7 +97,7 @@ namespace margelo::nitro { obj.setProperty(runtime, PropNameIDCache::get(runtime, "method"), JSIConverter>::toJSI(runtime, arg.method)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "headers"), JSIConverter>>::toJSI(runtime, arg.headers)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "bodyString"), JSIConverter>::toJSI(runtime, arg.bodyString)); - obj.setProperty(runtime, PropNameIDCache::get(runtime, "bodyBytes"), JSIConverter>::toJSI(runtime, arg.bodyBytes)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "bodyBytes"), JSIConverter>>::toJSI(runtime, arg.bodyBytes)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "bodyFormData"), JSIConverter>>::toJSI(runtime, arg.bodyFormData)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "timeoutMs"), JSIConverter>::toJSI(runtime, arg.timeoutMs)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "followRedirects"), JSIConverter>::toJSI(runtime, arg.followRedirects)); @@ -116,7 +117,7 @@ namespace margelo::nitro { if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "method")))) return false; if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "headers")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyString")))) return false; - if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyBytes")))) return false; + if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyBytes")))) return false; if (!JSIConverter>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "bodyFormData")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "timeoutMs")))) return false; if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "followRedirects")))) return false; diff --git a/packages/react-native-nitro-fetch/src/NitroFetch.nitro.ts b/packages/react-native-nitro-fetch/src/NitroFetch.nitro.ts index 9f2053b..a6ca1ff 100644 --- a/packages/react-native-nitro-fetch/src/NitroFetch.nitro.ts +++ b/packages/react-native-nitro-fetch/src/NitroFetch.nitro.ts @@ -30,7 +30,7 @@ export interface NitroRequest { headers?: NitroHeader[]; // Body as either UTF-8 string or raw bytes. bodyString?: string; - bodyBytes?: string; //will be ArrayBuffer in future + bodyBytes?: ArrayBuffer; // Multipart form data parts (for file uploads) bodyFormData?: NitroFormDataPart[]; // Controls diff --git a/packages/react-native-nitro-fetch/src/__tests__/fetch.test.ts b/packages/react-native-nitro-fetch/src/__tests__/fetch.test.ts new file mode 100644 index 0000000..7186d9c --- /dev/null +++ b/packages/react-native-nitro-fetch/src/__tests__/fetch.test.ts @@ -0,0 +1,43 @@ +jest.mock('react-native-nitro-modules', () => ({ + NitroModules: { + box: (value: unknown) => value, + createHybridObject: () => ({}), + }, +})); + +import { buildNitroRequestPure } from '../fetch'; + +function toArrayBuffer(bytes: Uint8Array): ArrayBuffer { + return bytes.buffer.slice( + bytes.byteOffset, + bytes.byteOffset + bytes.byteLength + ) as ArrayBuffer; +} + +describe('buildNitroRequestPure', () => { + it('passes ArrayBuffer request bodies through as bodyBytes', () => { + const bytes = new Uint8Array([0x00, 0x7f, 0x80, 0xff]); + const req = buildNitroRequestPure('https://example.com/upload', { + method: 'POST', + body: toArrayBuffer(bytes), + }); + + expect(req.bodyString).toBeUndefined(); + expect(req.bodyBytes).toBeInstanceOf(ArrayBuffer); + expect(new Uint8Array(req.bodyBytes as ArrayBuffer)).toEqual(bytes); + }); + + it('copies typed array request bodies into exact bodyBytes', () => { + const backing = new Uint8Array([0xaa, 0x00, 0x01, 0x02, 0xbb]); + const view = backing.subarray(1, 4); + const req = buildNitroRequestPure('https://example.com/upload', { + method: 'POST', + body: view, + }); + + expect(req.bodyString).toBeUndefined(); + expect(new Uint8Array(req.bodyBytes as ArrayBuffer)).toEqual( + new Uint8Array([0x00, 0x01, 0x02]) + ); + }); +}); diff --git a/packages/react-native-nitro-fetch/src/fetch.ts b/packages/react-native-nitro-fetch/src/fetch.ts index 5b0a0a9..11eded7 100644 --- a/packages/react-native-nitro-fetch/src/fetch.ts +++ b/packages/react-native-nitro-fetch/src/fetch.ts @@ -230,7 +230,7 @@ function buildNitroRequest( method: (method?.toUpperCase() as any) ?? 'GET', headers: headers.length > 0 ? headers : undefined, bodyString: normalized?.bodyString, - bodyBytes: undefined as any, + bodyBytes: normalized?.bodyBytes, bodyFormData: normalized?.bodyFormData, followRedirects, prefetchCacheTtlMs, @@ -376,8 +376,7 @@ export function buildNitroRequestPure( method: (method?.toUpperCase() as any) ?? 'GET', headers, bodyString: normalized?.bodyString, - // Only include bodyBytes when provided to avoid signaling upload data unintentionally - bodyBytes: undefined as any, + bodyBytes: normalized?.bodyBytes, followRedirects: true, prefetchCacheTtlMs, }; @@ -794,8 +793,6 @@ export async function prefetchOnAppStart( }; if (req.method && req.method !== 'GET') entry.method = req.method; if (req.bodyString !== undefined) entry.bodyString = req.bodyString; - if (typeof req.bodyBytes === 'string' && req.bodyBytes.length > 0) - entry.bodyBytes = req.bodyBytes; if (req.bodyFormData && req.bodyFormData.length > 0) entry.bodyFormData = req.bodyFormData; if (typeof req.timeoutMs === 'number') entry.timeoutMs = req.timeoutMs;