From f81566e0198d3157dd5285b22a1127c44f5954ed Mon Sep 17 00:00:00 2001 From: Marcin Mosiejko Date: Wed, 27 Aug 2025 11:45:17 +0200 Subject: [PATCH] retry: always pass response to retry function when available - Enables access to response headers for informed retry decisions (e.g., rate limiting) --- fetch.test.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 5 +++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/fetch.test.js b/fetch.test.js index 9c49698..e9b293c 100644 --- a/fetch.test.js +++ b/fetch.test.js @@ -348,6 +348,65 @@ t.test('Retrying', async t => { await res.blob() t.equal(res.headers.get('received-id'), 'foo') }) + + t.test('Response available in retry function', async t => { + let capturedResponse = null + + const server = fastify() + server.get('/test-response', (_request, reply) => { + // Always return a 500 error with some headers + reply.status(500) + reply.header('x-error-code', 'TEMP_ERROR') + reply.header('x-request-id', '12345') + reply.send({error: 'Server error'}) + }) + + await server.listen({port: 0}) + const address = server.server.address() + const serverPort = typeof address === 'object' ? address?.port : null + + try { + await t.rejects( + fetch(`http://localhost:${serverPort}/test-response`, { + validate: true, // This will cause the 500 to throw an error + retry: async ({error: _error, response}) => { + capturedResponse = response + + // Verify we can access response and its properties + t.ok(response, 'Response should be available in retry function') + if (response) { + t.equal( + response.status, + 500, + 'Should have access to response status' + ) + t.equal( + response.headers.get('x-error-code'), + 'TEMP_ERROR', + 'Should have access to response headers' + ) + t.equal( + response.headers.get('x-request-id'), + '12345', + 'Should have access to custom headers' + ) + } + + // Don't retry - we just want to test that response is available + return false + }, + }), + 'Should reject with HttpError' + ) + + t.ok( + capturedResponse, + 'Should have captured the response in retry function' + ) + } finally { + await server.close() + } + }) }) t.test(`Providing custom abort signal`, async t => { diff --git a/index.js b/index.js index 6825202..f90fb45 100644 --- a/index.js +++ b/index.js @@ -314,6 +314,7 @@ const fetch = async (resource, options, state) => { state.size = undefined state[STATE_INTERNAL].timedout = undefined state[STATE_INTERNAL].validateStarted = false + let /** @type {FetchResponse | undefined} */ response try { prepareOptions(state) const { @@ -333,7 +334,7 @@ const fetch = async (resource, options, state) => { ) state.startTs = performance.now() - let response = /** @type {FetchResponse} */ ( + response = /** @type {FetchResponse} */ ( await origFetch(state.resource, currOptions) ) const {body, status} = response @@ -377,7 +378,7 @@ const fetch = async (resource, options, state) => { // Here we catch request errors only state[STATE_INTERNAL].clearAbort?.('request') dbg(`${state.fullId} failed`, error) - if (await shouldRetry({state, error})) { + if (await shouldRetry({state, error, response})) { continue } state[STATE_INTERNAL].signalCompleted(error)