Skip to content

Commit 2ef9f8e

Browse files
committed
fix(core): detect files in nested body objects for multipart uploads
1 parent 49d84bb commit 2ef9f8e

File tree

3 files changed

+51
-4
lines changed

3 files changed

+51
-4
lines changed

.changeset/deep-file-detection.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tuyau/core': patch
3+
---
4+
5+
Fix shallow file detection in request body. `#hasFile` now recursively checks nested objects and arrays (up to 5 levels deep) to properly detect files and switch to multipart/form-data.

packages/core/src/client/tuyau.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,15 @@ export class Tuyau<
129129
/**
130130
* Checks if an object contains any file uploads
131131
*/
132-
#hasFile(obj: Record<string, any>) {
133-
if (!obj) return false
132+
#hasFile(obj: Record<string, any>, depth = 0): boolean {
133+
if (!obj || depth > 5) return false
134134

135135
return Object.values(obj).some((val) => {
136-
if (Array.isArray(val)) return val.some(this.#isFile)
137-
return this.#isFile(val)
136+
if (this.#isFile(val)) return true
137+
if (Array.isArray(val)) return val.some((item) => this.#isFile(item) || (isObject(item) && this.#hasFile(item, depth + 1)))
138+
if (isObject(val)) return this.#hasFile(val, depth + 1)
139+
140+
return false
138141
})
139142
}
140143

packages/core/tests/client.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,45 @@ test.group('Client | Chained', () => {
300300
await tuyau.api.users.store({ body: { file: blob } })
301301
})
302302

303+
test('send as form data when file is nested in object', async () => {
304+
const tuyau = createTuyau({ baseUrl: 'http://localhost:3333', registry })
305+
306+
nock('http://localhost:3333')
307+
.post('/auth/login')
308+
.reply(200, { token: '123' })
309+
.matchHeader('content-type', /multipart\/form-data/)
310+
311+
await tuyau.api.auth.login({
312+
body: { email: 'foo@ok.com', password: 'foo', profile: { avatar: new File(['img'], 'avatar.png') } },
313+
})
314+
})
315+
316+
test('send as form data when file is deeply nested', async () => {
317+
const tuyau = createTuyau({ baseUrl: 'http://localhost:3333', registry })
318+
319+
nock('http://localhost:3333')
320+
.post('/auth/login')
321+
.reply(200, { token: '123' })
322+
.matchHeader('content-type', /multipart\/form-data/)
323+
324+
await tuyau.api.auth.login({
325+
body: { email: 'foo@ok.com', data: { nested: { deep: { file: new File(['hello'], 'hello.txt') } } } },
326+
})
327+
})
328+
329+
test('send as form data when file is inside array of objects', async () => {
330+
const tuyau = createTuyau({ baseUrl: 'http://localhost:3333', registry })
331+
332+
nock('http://localhost:3333')
333+
.post('/auth/login')
334+
.reply(200, { token: '123' })
335+
.matchHeader('content-type', /multipart\/form-data/)
336+
337+
await tuyau.api.auth.login({
338+
body: { email: 'foo@ok.com', attachments: [{ file: new File(['doc'], 'doc.pdf') }] },
339+
})
340+
})
341+
303342
test('pass form data directly', async () => {
304343
const tuyau = createTuyau({ baseUrl: 'http://localhost:3333', registry })
305344

0 commit comments

Comments
 (0)