Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions __tests__/lib/client-file-uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
getFileMeta,
getPartBoundaries,
parseEtagHeader,
uploadImportFileToS3,
uploadParts,
} from '../../src/lib/client-file-uploader';

Expand Down Expand Up @@ -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',
} );
} );
} );
} );
25 changes: 24 additions & 1 deletion src/lib/client-file-uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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';
}
Expand Down