Skip to content
Merged
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
27 changes: 27 additions & 0 deletions __tests__/lib/fetch-with-retry-undici.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* @format
*/

import { Readable } from 'node:stream';

import { fetchWithRetry } from '../../src/lib/client-file-uploader';
import { getUndiciMockPool, resetUndiciMockAgent } from '../../test-utils/undici-mock';

describe( 'fetchWithRetry() with real undici', () => {
afterEach( resetUndiciMockAgent );

it( 'should add duplex for stream bodies created per attempt', async () => {
const pool = getUndiciMockPool( 'https://upload.example.com' );
pool.intercept( { method: 'PUT', path: '/upload' } ).reply( 200, 'ok' );

const response = await fetchWithRetry(
'https://upload.example.com/upload',
{ method: 'PUT' },
0,
() => Readable.from( [ 'hello' ] )
);

expect( response.status ).toBe( 200 );
await expect( response.text() ).resolves.toBe( 'ok' );
} );
} );
15 changes: 12 additions & 3 deletions src/lib/client-file-uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export function parseEtagHeader( etag: string ): string {

export type BodyFactory = () => RequestInit[ 'body' ];

type RequestInitWithDuplex = RequestInit & { duplex?: 'half' };

const isStreamBody = ( body: RequestInit[ 'body' ] ): boolean =>
typeof ( body as { pipe?: unknown } | null | undefined )?.pipe === 'function';

/**
* Wraps `fetch` with exponential-backoff retries.
*
Expand All @@ -54,13 +59,17 @@ export async function fetchWithRetry(
retries = 3,
createBody?: BodyFactory
): Promise< Response > {
const bodyIsStream =
typeof ( init.body as { pipe?: unknown } | null | undefined )?.pipe === 'function';
const bodyIsStream = isStreamBody( init.body );
// Only retry when we can hand `fetch` a fresh, replayable body each attempt.
const maxAttempts = createBody || ! bodyIsStream ? retries : 0;

for ( let attempt = 0; attempt <= maxAttempts; attempt++ ) {
const requestInit = createBody ? { ...init, body: createBody() } : init;
const requestInit: RequestInitWithDuplex = createBody
? { ...init, body: createBody() }
: { ...init };
if ( isStreamBody( requestInit.body ) && ! requestInit.duplex ) {
requestInit.duplex = 'half';
}
try {
// eslint-disable-next-line no-await-in-loop
return await fetch( input, requestInit );
Expand Down
Loading