Skip to content

Commit a47d463

Browse files
author
Mert Can Altin
committed
feat: implement throwOnMaxRedirect option for RedirectHandler
feat: add RedirectHandler invalid URL protocol test feat: test repair & added flag fix :lint feat: added doc for redirectHandler feat: added doc for redirectHandler feat: added type for maxRedirection option delete notes test: added for redirectionLimitReached
1 parent 35b049b commit a47d463

5 files changed

Lines changed: 136 additions & 1 deletion

File tree

docs/api/RedirectHandler.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Class: RedirectHandler
2+
3+
A class that handles redirection logic for HTTP requests.
4+
5+
## `new RedirectHandler(dispatch, maxRedirections, opts, handler, redirectionLimitReached)`
6+
7+
Arguments:
8+
9+
- **dispatch** `function` - The dispatch function to be called after every retry.
10+
- **maxRedirections** `number` - Maximum number of redirections allowed.
11+
- **opts** `object` - Options for handling redirection.
12+
- **handler** `object` - An object containing handlers for different stages of the request lifecycle.
13+
- **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior.
14+
15+
Returns: `RedirectHandler`
16+
17+
### Parameters
18+
19+
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection.
20+
- **maxRedirections** `number` (required) - Maximum number of redirections allowed.
21+
- **opts** `object` (required) - Options for handling redirection.
22+
- **handler** `object` (required) - Handlers for different stages of the request lifecycle.
23+
- **redirectionLimitReached** `boolean` (default: `false`) - A flag that the implementer can provide to enable or disable the feature. If set to `false`, it indicates that the caller doesn't want to use the feature and prefers the old behavior.
24+
25+
### Properties
26+
27+
- **location** `string` - The current redirection location.
28+
- **abort** `function` - The abort function.
29+
- **opts** `object` - The options for handling redirection.
30+
- **maxRedirections** `number` - Maximum number of redirections allowed.
31+
- **handler** `object` - Handlers for different stages of the request lifecycle.
32+
- **history** `Array` - An array representing the history of URLs during redirection.
33+
- **redirectionLimitReached** `boolean` - Indicates whether the redirection limit has been reached.
34+
35+
### Methods
36+
37+
#### `onConnect(abort)`
38+
39+
Called when the connection is established.
40+
41+
Parameters:
42+
43+
- **abort** `function` - The abort function.
44+
45+
#### `onUpgrade(statusCode, headers, socket)`
46+
47+
Called when an upgrade is requested.
48+
49+
Parameters:
50+
51+
- **statusCode** `number` - The HTTP status code.
52+
- **headers** `object` - The headers received in the response.
53+
- **socket** `object` - The socket object.
54+
55+
#### `onError(error)`
56+
57+
Called when an error occurs.
58+
59+
Parameters:
60+
61+
- **error** `Error` - The error that occurred.
62+
63+
#### `onHeaders(statusCode, headers, resume, statusText)`
64+
65+
Called when headers are received.
66+
67+
Parameters:
68+
69+
- **statusCode** `number` - The HTTP status code.
70+
- **headers** `object` - The headers received in the response.
71+
- **resume** `function` - The resume function.
72+
- **statusText** `string` - The status text.
73+
74+
#### `onData(chunk)`
75+
76+
Called when data is received.
77+
78+
Parameters:
79+
80+
- **chunk** `Buffer` - The data chunk received.
81+
82+
#### `onComplete(trailers)`
83+
84+
Called when the request is complete.
85+
86+
Parameters:
87+
88+
- **trailers** `object` - The trailers received.
89+
90+
#### `onBodySent(chunk)`
91+
92+
Called when the request body is sent.
93+
94+
Parameters:
95+
96+
- **chunk** `Buffer` - The chunk of the request body sent.

lib/handler/RedirectHandler.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class RedirectHandler {
3838
this.maxRedirections = maxRedirections
3939
this.handler = handler
4040
this.history = []
41+
this.redirectionLimitReached = false
4142

4243
if (util.isStream(this.opts.body)) {
4344
// TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
@@ -91,6 +92,15 @@ class RedirectHandler {
9192
? null
9293
: parseLocation(statusCode, headers)
9394

95+
if (this.history.length >= this.maxRedirections && !this.redirectionLimitReached) {
96+
if (this.request) {
97+
this.request.abort()
98+
}
99+
100+
this.redirectionLimitReached = true
101+
this.abort(new Error('max redirects'))
102+
}
103+
94104
if (this.opts.origin) {
95105
this.history.push(new URL(this.opts.path, this.opts.origin))
96106
}

test/redirect-request.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,29 @@ for (const factory of [
267267
t.equal(body.length, 0)
268268
})
269269

270+
t.test('should follow a redirect chain up to the allowed number of times for redirectionLimitReached', async t => {
271+
const server = await startRedirectingServer(t)
272+
273+
try {
274+
const { statusCode, headers, body: bodyStream, context: { history } } = await request(t, server, undefined, `http://${server}/300`, {
275+
maxRedirections: 2
276+
})
277+
278+
const body = await bodyStream.text()
279+
280+
t.equal(statusCode, 300)
281+
t.equal(headers.location, `http://${server}/300/2`)
282+
t.same(history.map(x => x.toString()), [`http://${server}/300`, `http://${server}/300/1`])
283+
t.equal(body.length, 0)
284+
} catch (error) {
285+
if (error.message.startsWith('max redirects')) {
286+
t.pass('Max redirects handled correctly')
287+
} else {
288+
t.fail(`Unexpected error: ${error.message}`)
289+
}
290+
}
291+
})
292+
270293
t.test('when a Location response header is NOT present', async t => {
271294
const redirectCodes = [300, 301, 302, 303, 307, 308]
272295
const server = await startRedirectingWithoutLocationServer(t)

types/dispatcher.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ declare namespace Dispatcher {
131131
opaque?: unknown;
132132
/** Default: 0 */
133133
maxRedirections?: number;
134+
/** Default: false */
135+
redirectionLimitReached?: boolean;
134136
/** Default: `null` */
135137
responseHeader?: 'raw' | null;
136138
}
@@ -141,6 +143,8 @@ declare namespace Dispatcher {
141143
signal?: AbortSignal | EventEmitter | null;
142144
/** Default: 0 */
143145
maxRedirections?: number;
146+
/** Default: false */
147+
redirectionLimitReached?: boolean;
144148
/** Default: `null` */
145149
onInfo?: (info: { statusCode: number, headers: Record<string, string | string[]> }) => void;
146150
/** Default: `null` */
@@ -164,6 +168,8 @@ declare namespace Dispatcher {
164168
signal?: AbortSignal | EventEmitter | null;
165169
/** Default: 0 */
166170
maxRedirections?: number;
171+
/** Default: false */
172+
redirectionLimitReached?: boolean;
167173
/** Default: `null` */
168174
responseHeader?: 'raw' | null;
169175
}

types/handlers.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Dispatcher from "./dispatcher";
22

33
export declare class RedirectHandler implements Dispatcher.DispatchHandlers{
4-
constructor (dispatch: Dispatcher, maxRedirections: number, opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers)
4+
constructor (dispatch: Dispatcher, maxRedirections: number, opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers, redirectionLimitReached: boolean)
55
}
66

77
export declare class DecoratorHandler implements Dispatcher.DispatchHandlers{

0 commit comments

Comments
 (0)