From 882afb5779e2ad2c32e4587ae28e58241b228564 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Tue, 23 Jun 2026 20:22:38 +0200 Subject: [PATCH] preserve MIME type in toFile --- src/internal/to-file.ts | 18 ++++++++++++++++-- tests/uploads.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/internal/to-file.ts b/src/internal/to-file.ts index 30eada32cc..98c7269405 100644 --- a/src/internal/to-file.ts +++ b/src/internal/to-file.ts @@ -95,12 +95,19 @@ export async function toFile( if (value instanceof File) { return value; } - return makeFile([await value.arrayBuffer()], value.name); + return makeFile([await value.arrayBuffer()], value.name, { + type: value.type, + lastModified: value.lastModified, + ...options, + }); } if (isResponseLike(value)) { const blob = await value.blob(); name ||= new URL(value.url).pathname.split(/[\\/]/).pop(); + if (!options?.type && blob.type) { + options = { ...options, type: blob.type }; + } return makeFile(await getBytes(blob), name, options); } @@ -110,7 +117,14 @@ export async function toFile( name ||= getName(value); if (!options?.type) { - const type = parts.find((part) => typeof part === 'object' && 'type' in part && part.type); + const type = parts.find( + (part): part is Blob => + typeof part === 'object' && + part !== null && + 'type' in part && + typeof part.type === 'string' && + part.type.length > 0, + )?.type; if (typeof type === 'string') { options = { ...options, type }; } diff --git a/tests/uploads.test.ts b/tests/uploads.test.ts index 9a3a1e0fc7..d7c5a12cd9 100644 --- a/tests/uploads.test.ts +++ b/tests/uploads.test.ts @@ -43,6 +43,15 @@ describe('toFile', () => { expect(file.name).toEqual('audio.mp3'); }); + it('preserves the MIME type from a Response', async () => { + const response = mockResponse({ + url: 'https://example.com/my/audio.mp3', + content: new Blob(['foo'], { type: 'audio/mpeg' }), + }); + const file = await toFile(response); + expect(file.type).toEqual('audio/mpeg'); + }); + it('extracts a file name from a File', async () => { const input = new File(['foo'], 'input.jsonl'); const file = await toFile(input); @@ -55,6 +64,27 @@ describe('toFile', () => { expect(file.name).toEqual('uploads.test.ts'); }); + it('preserves metadata from a File-like object', async () => { + const input = { + name: 'input.jsonl', + type: 'application/jsonl', + size: 3, + lastModified: 123, + text: async () => 'foo', + slice: () => new Blob(['foo'], { type: 'application/jsonl' }), + arrayBuffer: async () => new Blob(['foo']).arrayBuffer(), + }; + const file = await toFile(input); + expect(file.name).toEqual('input.jsonl'); + expect(file.type).toEqual('application/jsonl'); + expect(file.lastModified).toEqual(123); + }); + + it('infers the MIME type from Blob parts', async () => { + const file = await toFile(new Blob(['foo'], { type: 'text/plain' }), 'input.txt'); + expect(file.type).toEqual('text/plain'); + }); + it('does not copy File objects', async () => { const input = new File(['foo'], 'input.jsonl', { type: 'jsonl' }); const file = await toFile(input);