From bdc41cac80427a3b8eb52ae9056dfc159cfb6f3a Mon Sep 17 00:00:00 2001 From: Elie Rotenberg Date: Wed, 24 Jun 2026 08:03:14 +0200 Subject: [PATCH] fix: Don't set redundant Content-Length on file upload requests fetch derives Content-Length from the request body. Setting it explicitly sent a duplicate header, which undici >= 7.26 rejects with UND_ERR_INVALID_ARG when a userland dispatcher is active. Fixes #1718 --- src/cross/_cross_uploader.ts | 3 ++- src/node/_node_uploader.ts | 3 ++- test/unit/file_test.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/cross/_cross_uploader.ts b/src/cross/_cross_uploader.ts index 1bdf730a97..361bc63e0c 100644 --- a/src/cross/_cross_uploader.ts +++ b/src/cross/_cross_uploader.ts @@ -143,7 +143,8 @@ async function uploadBlobInternal( ...(httpOptions?.headers || {}), 'X-Goog-Upload-Command': uploadCommand, 'X-Goog-Upload-Offset': String(offset), - 'Content-Length': String(chunkSize), + // Don't set Content-Length: fetch derives it from the body, and a + // duplicate is rejected by undici >= 7.26 (#1718). }; response = await apiClient.request({ diff --git a/src/node/_node_uploader.ts b/src/node/_node_uploader.ts index e4166c148b..0a826245b8 100644 --- a/src/node/_node_uploader.ts +++ b/src/node/_node_uploader.ts @@ -283,8 +283,9 @@ export class NodeUploader implements Uploader { ...(httpOptions?.headers || {}), 'X-Goog-Upload-Command': uploadCommand, 'X-Goog-Upload-Offset': String(offset), - 'Content-Length': String(bytesRead), 'X-Goog-Upload-File-Name': fileName, + // Don't set Content-Length: fetch derives it from the body, and a + // duplicate is rejected by undici >= 7.26 (#1718). }; response = await apiClient.request({ diff --git a/test/unit/file_test.ts b/test/unit/file_test.ts index 4b0bea17ba..072a4ccbfc 100644 --- a/test/unit/file_test.ts +++ b/test/unit/file_test.ts @@ -351,6 +351,39 @@ describe('File', () => { } expect(byteProcessed).toBe(fileSize); }); + it('It should not send a Content-Length that undici >= 7.26 would reject.', async () => { + const testBlob = new Blob([new Uint8Array(8)], { + type: DEFAULT_TEST_MIMETYPE, + }); + + spyOn(global, 'fetch').and.callFake((_input, init) => { + const headers = init?.headers as Headers | undefined; + // Mimic undici >= 7.26, which rejects a request that carries both a + // body and an explicit Content-Length (#1718). + if (init?.body && headers?.get('Content-Length')) { + return Promise.reject( + new TypeError('fetch failed', { + cause: {code: 'UND_ERR_INVALID_ARG'}, + }), + ); + } + // The chunk request carries the upload offset; anything else is the + // initial request that returns the upload url. + if (headers?.get('X-Goog-Upload-Offset') !== null) { + return Promise.resolve( + new Response( + JSON.stringify({file: {name: 'files/test-file'}}), + lastCorrectFetchOkOptions, + ), + ); + } + return Promise.resolve(new Response('', createUrlOkoptions)); + }); + + const uploaded = await client.files.upload({file: testBlob}); + + expect(uploaded.name).toBe('files/test-file'); + }); }); }); describe('registerFiles', () => {