From bfc0e79f314594e0f64ae00f693790d06f52e125 Mon Sep 17 00:00:00 2001 From: Maruthan G Date: Sun, 19 Apr 2026 17:40:25 +0530 Subject: [PATCH] fix(interceptor): add throwOnMaxRedirect to types and interceptor opts The `throwOnMaxRedirect` option was respected per-request inside `RedirectHandler` but was missing from the TypeScript declarations and was never plumbed from `redirect({ throwOnMaxRedirect: true })` (the documented usage). Add it to `RedirectInterceptorOpts`, forward the interceptor-level default through `dispatchOpts`, and validate the type. Refs #4376, #4377. --- lib/handler/redirect-handler.js | 4 +++ lib/interceptor/redirect.js | 6 ++-- test/interceptors/redirect.js | 37 +++++++++++++++++++++++ test/types/redirect-interceptor.test-d.ts | 10 ++++++ types/interceptors.d.ts | 2 +- 5 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 test/types/redirect-interceptor.test-d.ts diff --git a/lib/handler/redirect-handler.js b/lib/handler/redirect-handler.js index bdc8cdfd004..35a4aa4232e 100644 --- a/lib/handler/redirect-handler.js +++ b/lib/handler/redirect-handler.js @@ -23,6 +23,10 @@ class RedirectHandler { throw new InvalidArgumentError('maxRedirections must be a positive number') } + if (opts.throwOnMaxRedirect != null && typeof opts.throwOnMaxRedirect !== 'boolean') { + throw new InvalidArgumentError('throwOnMaxRedirect must be a boolean') + } + this.dispatch = dispatch this.location = null const { maxRedirections: _, ...cleanOpts } = opts diff --git a/lib/interceptor/redirect.js b/lib/interceptor/redirect.js index b7df180433e..83b4c9a7b2f 100644 --- a/lib/interceptor/redirect.js +++ b/lib/interceptor/redirect.js @@ -2,16 +2,16 @@ const RedirectHandler = require('../handler/redirect-handler') -function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections } = {}) { +function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections, throwOnMaxRedirect: defaultThrowOnMaxRedirect } = {}) { return (dispatch) => { return function Intercept (opts, handler) { - const { maxRedirections = defaultMaxRedirections, ...rest } = opts + const { maxRedirections = defaultMaxRedirections, throwOnMaxRedirect = defaultThrowOnMaxRedirect, ...rest } = opts if (maxRedirections == null || maxRedirections === 0) { return dispatch(opts, handler) } - const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting. + const dispatchOpts = { ...rest, throwOnMaxRedirect } // Stop sub dispatcher from also redirecting. const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler) return dispatch(dispatchOpts, redirectHandler) } diff --git a/test/interceptors/redirect.js b/test/interceptors/redirect.js index 88f89818f79..716379d65da 100644 --- a/test/interceptors/redirect.js +++ b/test/interceptors/redirect.js @@ -466,6 +466,43 @@ for (const factory of [ await t.completed }) + test('should throw when max redirections is reached and throwOnMaxRedirect is set as interceptor default', async t => { + t = tspl(t, { plan: 1 }) + + const server = await startRedirectingServer() + + const dispatcher = new undici.Agent().compose( + redirect({ maxRedirections: 2, throwOnMaxRedirect: true }) + ) + after(() => dispatcher.close()) + + try { + await undici.request(`http://${server}/300`, { dispatcher }) + t.fail('Did not throw') + } catch (error) { + t.strictEqual(error.message, 'max redirects') + } + + await t.completed + }) + + test('should not allow invalid throwOnMaxRedirect arguments', async t => { + t = tspl(t, { plan: 1 }) + + try { + await request(t, 'localhost', undefined, 'http://localhost', { + method: 'GET', + maxRedirections: 1, + throwOnMaxRedirect: 'INVALID' + }) + t.fail('Did not throw') + } catch (err) { + t.strictEqual(err.message, 'throwOnMaxRedirect must be a boolean') + } + + await t.completed + }) + test('when a Location response header is NOT present', async t => { t = tspl(t, { plan: 6 * 3 }) diff --git a/test/types/redirect-interceptor.test-d.ts b/test/types/redirect-interceptor.test-d.ts new file mode 100644 index 00000000000..42612e0dec7 --- /dev/null +++ b/test/types/redirect-interceptor.test-d.ts @@ -0,0 +1,10 @@ +import { expectAssignable, expectNotAssignable } from 'tsd' +import Interceptors from '../../types/interceptors' + +expectAssignable({}) +expectAssignable({ maxRedirections: 3 }) +expectAssignable({ throwOnMaxRedirect: true }) +expectAssignable({ maxRedirections: 3, throwOnMaxRedirect: true }) + +expectNotAssignable({ maxRedirections: 'INVALID' }) +expectNotAssignable({ throwOnMaxRedirect: 'INVALID' }) diff --git a/types/interceptors.d.ts b/types/interceptors.d.ts index 71983a768c0..3b90a28592f 100644 --- a/types/interceptors.d.ts +++ b/types/interceptors.d.ts @@ -8,7 +8,7 @@ export default Interceptors declare namespace Interceptors { export type DumpInterceptorOpts = { maxSize?: number } export type RetryInterceptorOpts = RetryHandler.RetryOptions - export type RedirectInterceptorOpts = { maxRedirections?: number } + export type RedirectInterceptorOpts = { maxRedirections?: number, throwOnMaxRedirect?: boolean } export type DecompressInterceptorOpts = { skipErrorResponses?: boolean skipStatusCodes?: number[]