From ed8363c88f445dbd22b738d8be11d9485b404b01 Mon Sep 17 00:00:00 2001 From: Rinat Khaziev Date: Mon, 22 Jun 2026 19:56:28 -0500 Subject: [PATCH] Fix fetch failed due to unsupported expect header --- __tests__/lib/client-file-uploader.js | 64 +++++++++++++++++++++++++++ src/lib/client-file-uploader.ts | 25 ++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/__tests__/lib/client-file-uploader.js b/__tests__/lib/client-file-uploader.js index 76e149dab..938455353 100644 --- a/__tests__/lib/client-file-uploader.js +++ b/__tests__/lib/client-file-uploader.js @@ -15,6 +15,7 @@ import { getFileMeta, getPartBoundaries, parseEtagHeader, + uploadImportFileToS3, uploadParts, } from '../../src/lib/client-file-uploader'; @@ -332,4 +333,67 @@ describe( 'client-file-uploader', () => { expect( progress[ progress.length - 1 ] ).toBe( '100%' ); }, 15000 ); } ); + + describe( 'uploadImportFileToS3()', () => { + let tmpDir; + + const uploadOkResponse = { + status: 200, + text: async () => '', + }; + + const writeTempFile = ( name, contents ) => { + const fileName = path.join( tmpDir, name ); + writeFileSync( fileName, contents ); + return fileName; + }; + + beforeAll( () => { + tmpDir = mkdtempSync( path.join( os.tmpdir(), 'vip-cli-upload-import-' ) ); + } ); + + afterAll( () => { + rmSync( tmpDir, { recursive: true, force: true } ); + } ); + + beforeEach( () => { + fetch.mockReset(); + http.mockReset(); + } ); + + it( 'should strip unsupported Expect headers from presigned PutObject uploads', async () => { + const fileName = writeTempFile( 'small-deploy.zip', 'zip contents' ); + const fileMeta = { + ...( await getFileMeta( fileName ) ), + fileContent: Buffer.from( 'zip contents' ), + }; + + http.mockResolvedValue( { + status: 200, + json: async () => ( { + url: 'https://s3.example.com/upload', + options: { + method: 'PUT', + headers: { + Expect: '100-continue', + 'x-amz-server-side-encryption': 'AES256', + }, + }, + } ), + } ); + fetch.mockResolvedValue( uploadOkResponse ); + + await uploadImportFileToS3( { + app: { id: 1 }, + env: { id: 2 }, + fileMeta, + } ); + + expect( fetch ).toHaveBeenCalledTimes( 1 ); + expect( fetch.mock.calls[ 0 ][ 1 ].headers ).toEqual( { + 'Content-Length': `${ fileMeta.fileSize }`, + 'x-amz-server-side-encryption': 'AES256', + } ); + } ); + } ); } ); diff --git a/src/lib/client-file-uploader.ts b/src/lib/client-file-uploader.ts index abda49cbd..c9e6bca04 100644 --- a/src/lib/client-file-uploader.ts +++ b/src/lib/client-file-uploader.ts @@ -9,7 +9,7 @@ import { setTimeout } from 'node:timers/promises'; import os from 'os'; import path from 'path'; import { PassThrough } from 'stream'; -import { fetch, type HeadersInit, type RequestInit, type Response } from 'undici'; +import { fetch, Headers, type HeadersInit, type RequestInit, type Response } from 'undici'; import { Parser as XmlParser } from 'xml2js'; import { createGunzip, createGzip, Gunzip, ZlibOptions } from 'zlib'; @@ -39,6 +39,28 @@ type RequestInitWithDuplex = RequestInit & { duplex?: 'half' }; const isStreamBody = ( body: RequestInit[ 'body' ] ): boolean => typeof ( body as { pipe?: unknown } | null | undefined )?.pipe === 'function'; +const stripUndiciUnsupportedHeaders = ( headers?: HeadersInit ): HeadersInit | undefined => { + if ( ! headers ) { + return headers; + } + + const isSupportedHeader = ( name: string ) => name.toLowerCase() !== 'expect'; + + if ( headers instanceof Headers ) { + const sanitized = new Headers( headers ); + sanitized.delete( 'expect' ); + return sanitized; + } + + if ( Array.isArray( headers ) ) { + return headers.filter( ( [ name ] ) => isSupportedHeader( name ) ); + } + + return Object.fromEntries( + Object.entries( headers ).filter( ( [ name ] ) => isSupportedHeader( name ) ) + ); +}; + /** * Wraps `fetch` with exponential-backoff retries. * @@ -67,6 +89,7 @@ export async function fetchWithRetry( const requestInit: RequestInitWithDuplex = createBody ? { ...init, body: createBody() } : { ...init }; + requestInit.headers = stripUndiciUnsupportedHeaders( requestInit.headers ); if ( isStreamBody( requestInit.body ) && ! requestInit.duplex ) { requestInit.duplex = 'half'; }