feat: support react native blob objects (#5764)

* feat: support react native blob objects

* test: cover react native blob objects detection

* fix: improve isReactNativeBlob and isReactNative checks for better validation

* feat: support appending React Native blob objects to FormData without recursion

---------

Co-authored-by: Jay <jasonsaayman@gmail.com>
This commit is contained in:
Mohsen 2026-02-24 22:06:32 +03:30 committed by GitHub
parent 00d97b9730
commit 885b4af6f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 164 additions and 0 deletions

View File

@ -156,6 +156,11 @@ function toFormData(obj, formData, options) {
function defaultVisitor(value, key, path) {
let arr = value;
if (utils.isReactNative(formData) && utils.isReactNativeBlob(value)) {
formData.append(renderKey(path, key, dots), convertValue(value));
return false;
}
if (value && !path && typeof value === 'object') {
if (utils.endsWith(key, '{}')) {
// eslint-disable-next-line no-param-reassign

View File

@ -186,6 +186,31 @@ const isDate = kindOfTest('Date');
*/
const isFile = kindOfTest('File');
/**
* Determine if a value is a React Native Blob
* React Native "blob": an object with a `uri` attribute. Optionally, it can
* also have a `name` and `type` attribute to specify filename and content type
*
* @see https://github.com/facebook/react-native/blob/26684cf3adf4094eb6c405d345a75bf8c7c0bf88/Libraries/Network/FormData.js#L68-L71
*
* @param {*} value The value to test
*
* @returns {boolean} True if value is a React Native Blob, otherwise false
*/
const isReactNativeBlob = (value) => {
return !!(value && typeof value.uri !== 'undefined');
}
/**
* Determine if environment is React Native
* ReactNative `FormData` has a non-standard `getParts()` method
*
* @param {*} formData The formData to test
*
* @returns {boolean} True if environment is React Native, otherwise false
*/
const isReactNative = (formData) => formData && typeof formData.getParts !== 'undefined';
/**
* Determine if a value is a Blob
*
@ -850,6 +875,8 @@ export default {
isUndefined,
isDate,
isFile,
isReactNativeBlob,
isReactNative,
isBlob,
isRegExp,
isFunction,

View File

@ -3,6 +3,19 @@ import toFormData from '../../../lib/helpers/toFormData.js';
import FormData from 'form-data';
describe('helpers::toFormData', function () {
function createRNFormDataSpy() {
const calls = [];
return {
calls,
append(key, value) {
calls.push([key, value]);
},
getParts() {
return [];
}
};
}
it('should convert a flat object to FormData', function () {
const data = {
foo: 'bar',
@ -50,4 +63,70 @@ describe('helpers::toFormData', function () {
const formData = toFormData(data, new FormData());
assert.ok(formData instanceof FormData);
});
it('should append root-level React Native blob without recursion', function () {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://test.png',
type: 'image/png',
name: 'test.png'
};
toFormData({ file: blob }, formData);
assert.strictEqual(formData.calls.length, 1);
assert.strictEqual(formData.calls[0][0], 'file');
assert.strictEqual(formData.calls[0][1], blob);
});
it('should append nested React Native blob without recursion', function () {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://nested.png',
type: 'image/png',
name: 'nested.png'
};
toFormData({ nested: { file: blob } }, formData);
assert.strictEqual(formData.calls.length, 1);
assert.strictEqual(formData.calls[0][0], 'nested[file]');
assert.strictEqual(formData.calls[0][1], blob);
});
it('should append deeply nested React Native blob without recursion', function () {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://deep.png',
name: 'deep.png'
};
toFormData({ a: { b: { c: blob } } }, formData);
assert.strictEqual(formData.calls.length, 1);
assert.strictEqual(formData.calls[0][0], 'a[b][c]');
assert.strictEqual(formData.calls[0][1], blob);
});
it('should NOT recurse into React Native blob properties', function () {
const formData = createRNFormDataSpy();
const blob = {
uri: 'file://nope.png',
type: 'image/png',
name: 'nope.png'
};
toFormData({ file: blob }, formData);
const keys = formData.calls.map(call => call[0]);
assert.deepStrictEqual(keys, ['file']);
assert.ok(!keys.some(k => k.includes('uri')));
assert.ok(!keys.some(k => k.includes('type')));
assert.ok(!keys.some(k => k.includes('name')));
});
});

View File

@ -120,4 +120,57 @@ describe('utils', function () {
assert.strictEqual(result, null);
});
});
describe('utils::isReactNativeBlob', function () {
it('should return true for objects with uri property', function () {
assert.strictEqual(utils.isReactNativeBlob({ uri: 'file://path/to/file' }), true);
assert.strictEqual(utils.isReactNativeBlob({ uri: 'content://media/image' }), true);
});
it('should return true for React Native blob-like objects with optional name and type', function () {
assert.strictEqual(utils.isReactNativeBlob({
uri: 'file://path/to/file',
name: 'image.png',
type: 'image/png'
}), true);
});
it('should return false for objects without uri property', function () {
assert.strictEqual(utils.isReactNativeBlob({ path: 'file://path' }), false);
assert.strictEqual(utils.isReactNativeBlob({ url: 'http://example.com' }), false);
assert.strictEqual(utils.isReactNativeBlob({}), false);
});
it('should return false for non-objects', function () {
assert.strictEqual(utils.isReactNativeBlob(null), false);
assert.strictEqual(utils.isReactNativeBlob(undefined), false);
assert.strictEqual(utils.isReactNativeBlob('string'), false);
assert.strictEqual(utils.isReactNativeBlob(123), false);
assert.strictEqual(utils.isReactNativeBlob(false), false);
});
it('should return true even if uri is empty string', function () {
assert.strictEqual(utils.isReactNativeBlob({ uri: '' }), true);
});
});
describe('utils::isReactNative', function () {
it('should return true for FormData with getParts method', function () {
const mockReactNativeFormData = {
append: function() {},
getParts: function() { return []; }
};
assert.strictEqual(utils.isReactNative(mockReactNativeFormData), true);
});
it('should return false for standard FormData without getParts method', function () {
const standardFormData = new FormData();
assert.strictEqual(utils.isReactNative(standardFormData), false);
});
it('should return false for objects without getParts method', function () {
assert.strictEqual(utils.isReactNative({ append: function() {} }), false);
assert.strictEqual(utils.isReactNative({}), false);
});
});
});