Skip to content

Commit 885b4af

Browse files
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 <[email protected]>
1 parent 00d97b9 commit 885b4af

4 files changed

Lines changed: 164 additions & 0 deletions

File tree

lib/helpers/toFormData.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ function toFormData(obj, formData, options) {
156156
function defaultVisitor(value, key, path) {
157157
let arr = value;
158158

159+
if (utils.isReactNative(formData) && utils.isReactNativeBlob(value)) {
160+
formData.append(renderKey(path, key, dots), convertValue(value));
161+
return false;
162+
}
163+
159164
if (value && !path && typeof value === 'object') {
160165
if (utils.endsWith(key, '{}')) {
161166
// eslint-disable-next-line no-param-reassign

lib/utils.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,31 @@ const isDate = kindOfTest('Date');
186186
*/
187187
const isFile = kindOfTest('File');
188188

189+
/**
190+
* Determine if a value is a React Native Blob
191+
* React Native "blob": an object with a `uri` attribute. Optionally, it can
192+
* also have a `name` and `type` attribute to specify filename and content type
193+
*
194+
* @see https://github.com/facebook/react-native/blob/26684cf3adf4094eb6c405d345a75bf8c7c0bf88/Libraries/Network/FormData.js#L68-L71
195+
*
196+
* @param {*} value The value to test
197+
*
198+
* @returns {boolean} True if value is a React Native Blob, otherwise false
199+
*/
200+
const isReactNativeBlob = (value) => {
201+
return !!(value && typeof value.uri !== 'undefined');
202+
}
203+
204+
/**
205+
* Determine if environment is React Native
206+
* ReactNative `FormData` has a non-standard `getParts()` method
207+
*
208+
* @param {*} formData The formData to test
209+
*
210+
* @returns {boolean} True if environment is React Native, otherwise false
211+
*/
212+
const isReactNative = (formData) => formData && typeof formData.getParts !== 'undefined';
213+
189214
/**
190215
* Determine if a value is a Blob
191216
*
@@ -850,6 +875,8 @@ export default {
850875
isUndefined,
851876
isDate,
852877
isFile,
878+
isReactNativeBlob,
879+
isReactNative,
853880
isBlob,
854881
isRegExp,
855882
isFunction,

test/unit/helpers/toFormData.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ import toFormData from '../../../lib/helpers/toFormData.js';
33
import FormData from 'form-data';
44

55
describe('helpers::toFormData', function () {
6+
function createRNFormDataSpy() {
7+
const calls = [];
8+
return {
9+
calls,
10+
append(key, value) {
11+
calls.push([key, value]);
12+
},
13+
getParts() {
14+
return [];
15+
}
16+
};
17+
}
18+
619
it('should convert a flat object to FormData', function () {
720
const data = {
821
foo: 'bar',
@@ -50,4 +63,70 @@ describe('helpers::toFormData', function () {
5063
const formData = toFormData(data, new FormData());
5164
assert.ok(formData instanceof FormData);
5265
});
66+
67+
it('should append root-level React Native blob without recursion', function () {
68+
const formData = createRNFormDataSpy();
69+
70+
const blob = {
71+
uri: 'file://test.png',
72+
type: 'image/png',
73+
name: 'test.png'
74+
};
75+
76+
toFormData({ file: blob }, formData);
77+
78+
assert.strictEqual(formData.calls.length, 1);
79+
assert.strictEqual(formData.calls[0][0], 'file');
80+
assert.strictEqual(formData.calls[0][1], blob);
81+
});
82+
83+
it('should append nested React Native blob without recursion', function () {
84+
const formData = createRNFormDataSpy();
85+
86+
const blob = {
87+
uri: 'file://nested.png',
88+
type: 'image/png',
89+
name: 'nested.png'
90+
};
91+
92+
toFormData({ nested: { file: blob } }, formData);
93+
94+
assert.strictEqual(formData.calls.length, 1);
95+
assert.strictEqual(formData.calls[0][0], 'nested[file]');
96+
assert.strictEqual(formData.calls[0][1], blob);
97+
});
98+
99+
it('should append deeply nested React Native blob without recursion', function () {
100+
const formData = createRNFormDataSpy();
101+
102+
const blob = {
103+
uri: 'file://deep.png',
104+
name: 'deep.png'
105+
};
106+
107+
toFormData({ a: { b: { c: blob } } }, formData);
108+
109+
assert.strictEqual(formData.calls.length, 1);
110+
assert.strictEqual(formData.calls[0][0], 'a[b][c]');
111+
assert.strictEqual(formData.calls[0][1], blob);
112+
});
113+
114+
it('should NOT recurse into React Native blob properties', function () {
115+
const formData = createRNFormDataSpy();
116+
117+
const blob = {
118+
uri: 'file://nope.png',
119+
type: 'image/png',
120+
name: 'nope.png'
121+
};
122+
123+
toFormData({ file: blob }, formData);
124+
125+
const keys = formData.calls.map(call => call[0]);
126+
127+
assert.deepStrictEqual(keys, ['file']);
128+
assert.ok(!keys.some(k => k.includes('uri')));
129+
assert.ok(!keys.some(k => k.includes('type')));
130+
assert.ok(!keys.some(k => k.includes('name')));
131+
});
53132
});

test/unit/utils/utils.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,57 @@ describe('utils', function () {
120120
assert.strictEqual(result, null);
121121
});
122122
});
123+
124+
describe('utils::isReactNativeBlob', function () {
125+
it('should return true for objects with uri property', function () {
126+
assert.strictEqual(utils.isReactNativeBlob({ uri: 'file://path/to/file' }), true);
127+
assert.strictEqual(utils.isReactNativeBlob({ uri: 'content://media/image' }), true);
128+
});
129+
130+
it('should return true for React Native blob-like objects with optional name and type', function () {
131+
assert.strictEqual(utils.isReactNativeBlob({
132+
uri: 'file://path/to/file',
133+
name: 'image.png',
134+
type: 'image/png'
135+
}), true);
136+
});
137+
138+
it('should return false for objects without uri property', function () {
139+
assert.strictEqual(utils.isReactNativeBlob({ path: 'file://path' }), false);
140+
assert.strictEqual(utils.isReactNativeBlob({ url: 'http://example.com' }), false);
141+
assert.strictEqual(utils.isReactNativeBlob({}), false);
142+
});
143+
144+
it('should return false for non-objects', function () {
145+
assert.strictEqual(utils.isReactNativeBlob(null), false);
146+
assert.strictEqual(utils.isReactNativeBlob(undefined), false);
147+
assert.strictEqual(utils.isReactNativeBlob('string'), false);
148+
assert.strictEqual(utils.isReactNativeBlob(123), false);
149+
assert.strictEqual(utils.isReactNativeBlob(false), false);
150+
});
151+
152+
it('should return true even if uri is empty string', function () {
153+
assert.strictEqual(utils.isReactNativeBlob({ uri: '' }), true);
154+
});
155+
});
156+
157+
describe('utils::isReactNative', function () {
158+
it('should return true for FormData with getParts method', function () {
159+
const mockReactNativeFormData = {
160+
append: function() {},
161+
getParts: function() { return []; }
162+
};
163+
assert.strictEqual(utils.isReactNative(mockReactNativeFormData), true);
164+
});
165+
166+
it('should return false for standard FormData without getParts method', function () {
167+
const standardFormData = new FormData();
168+
assert.strictEqual(utils.isReactNative(standardFormData), false);
169+
});
170+
171+
it('should return false for objects without getParts method', function () {
172+
assert.strictEqual(utils.isReactNative({ append: function() {} }), false);
173+
assert.strictEqual(utils.isReactNative({}), false);
174+
});
175+
});
123176
});

0 commit comments

Comments
 (0)