Skip to content

Commit 91cdc48

Browse files
committed
Add retryCount to beforeRequest hook context
Fixes #1866
1 parent e5659d4 commit 91cdc48

File tree

4 files changed

+120
-5
lines changed

4 files changed

+120
-5
lines changed

documentation/9-hooks.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,15 @@ console.log(headers.Secret);
107107
**Default: `[]`**
108108

109109
```ts
110-
(options: Options) => Promisable<void | Response | ResponseLike>
110+
(options: Options, context: BeforeRequestHookContext) => Promisable<void | Response | ResponseLike>
111111
```
112112

113113
Called right before making the request with `options.createNativeRequestOptions()`.\
114114
This hook is especially useful in conjunction with `got.extend()` when you want to sign your request.
115115

116+
The second parameter is a context object with the following properties:
117+
- `retryCount` - The current retry count (0 for the initial request, 1+ for retries).
118+
116119
**Note:**
117120
> - Got will make no further changes to the request before it is sent.
118121
@@ -128,7 +131,7 @@ const response = await got.post(
128131
json: {payload: 'old'},
129132
hooks: {
130133
beforeRequest: [
131-
options => {
134+
(options, context) => {
132135
options.body = JSON.stringify({payload: 'new'});
133136
options.headers['content-length'] = Buffer.byteLength(options.body).toString();
134137
}
@@ -138,6 +141,28 @@ const response = await got.post(
138141
);
139142
```
140143

144+
You can use `context.retryCount` to conditionally modify behavior based on whether it's the initial request or a retry:
145+
146+
```js
147+
import got from 'got';
148+
149+
const response = await got('https://httpbin.org/status/500', {
150+
retry: {
151+
limit: 2
152+
},
153+
hooks: {
154+
beforeRequest: [
155+
(options, context) => {
156+
// Only log on initial request, not on retries
157+
if (context.retryCount === 0) {
158+
console.log('Making initial request');
159+
}
160+
}
161+
]
162+
}
163+
});
164+
```
165+
141166
**Tip:**
142167
> - You can indirectly override the `request` function by early returning a [`ClientRequest`-like](https://nodejs.org/api/http.html#http_class_http_clientrequest) instance or a [`IncomingMessage`-like](https://nodejs.org/api/http.html#http_class_http_incomingmessage) instance. This is very useful when creating a custom cache mechanism.
143168
> - [Read more about this tip](cache.md#advanced-caching-mechanisms).

source/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
12191219

12201220
for (const hook of options.hooks.beforeRequest) {
12211221
// eslint-disable-next-line no-await-in-loop
1222-
const result = await hook(options as NormalizedOptions);
1222+
const result = await hook(options as NormalizedOptions, {retryCount: this.retryCount});
12231223

12241224
if (!is.undefined(result)) {
12251225
// @ts-expect-error Skip the type mismatch to support abstract responses

source/core/options.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,17 @@ export type NormalizedOptions = OverrideProperties<Options, {
9191
}>;
9292

9393
export type InitHook = (init: OptionsInit, self: Options) => void;
94-
export type BeforeRequestHook = (options: NormalizedOptions) => Promisable<void | Response | ResponseLike>;
94+
95+
export type BeforeRequestHookContext = {
96+
/**
97+
The current retry count.
98+
99+
It will be `0` for the initial request and increment for each retry.
100+
*/
101+
retryCount: number;
102+
};
103+
104+
export type BeforeRequestHook = (options: NormalizedOptions, context: BeforeRequestHookContext) => Promisable<void | Response | ResponseLike>;
95105
export type BeforeRedirectHook = (updatedOptions: NormalizedOptions, plainResponse: PlainResponse) => Promisable<void>;
96106
export type BeforeErrorHook = (error: RequestError) => Promisable<RequestError>;
97107
export type BeforeRetryHook = (error: RequestError, retryCount: number) => Promisable<void>;
@@ -195,6 +205,8 @@ export type Hooks = {
195205
/**
196206
Called right before making the request with `options.createNativeRequestOptions()`.
197207
208+
The second argument is a context object containing request state information.
209+
198210
This hook is especially useful in conjunction with `got.extend()` when you want to sign your request.
199211
200212
@default []
@@ -215,7 +227,7 @@ export type Hooks = {
215227
json: {payload: 'old'},
216228
hooks: {
217229
beforeRequest: [
218-
options => {
230+
(options, context) => {
219231
options.body = JSON.stringify({payload: 'new'});
220232
options.headers['content-length'] = options.body.length.toString();
221233
}
@@ -225,6 +237,28 @@ export type Hooks = {
225237
);
226238
```
227239
240+
**Example using `context.retryCount`:**
241+
242+
```
243+
import got from 'got';
244+
245+
await got('https://httpbin.org/status/500', {
246+
retry: {
247+
limit: 2
248+
},
249+
hooks: {
250+
beforeRequest: [
251+
(options, context) => {
252+
// Only log on the initial request, not on retries
253+
if (context.retryCount === 0) {
254+
console.log('Making initial request');
255+
}
256+
}
257+
]
258+
}
259+
});
260+
```
261+
228262
**Tip:**
229263
> - You can indirectly override the `request` function by early returning a [`ClientRequest`-like](https://nodejs.org/api/http.html#http_class_http_clientrequest) instance or a [`IncomingMessage`-like](https://nodejs.org/api/http.html#http_class_http_incomingmessage) instance. This is very useful when creating a custom cache mechanism.
230264
> - [Read more about this tip](https://github.com/sindresorhus/got/blob/main/documentation/cache.md#advanced-caching-mechanisms).

test/hooks.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,62 @@ test('beforeRequest allows modifications', withServer, async (t, server, got) =>
367367
t.is(body.foo, 'bar');
368368
});
369369

370+
test('beforeRequest is called with context', withServer, async (t, server, got) => {
371+
server.get('/', echoHeaders);
372+
373+
await got({
374+
hooks: {
375+
beforeRequest: [
376+
(_options, context) => {
377+
t.truthy(context);
378+
t.is(typeof context.retryCount, 'number');
379+
},
380+
],
381+
},
382+
});
383+
});
384+
385+
test('beforeRequest context has retryCount 0 on initial request', withServer, async (t, server, got) => {
386+
server.get('/', echoHeaders);
387+
388+
await got({
389+
hooks: {
390+
beforeRequest: [
391+
(_options, context) => {
392+
t.is(context.retryCount, 0);
393+
},
394+
],
395+
},
396+
});
397+
});
398+
399+
test('beforeRequest context retryCount increments on retries', withServer, async (t, server, got) => {
400+
server.get('/', (_request, response) => {
401+
response.statusCode = 500;
402+
response.end();
403+
});
404+
405+
const retryCounts: number[] = [];
406+
407+
await t.throwsAsync(got({
408+
retry: {
409+
limit: 2,
410+
},
411+
hooks: {
412+
beforeRequest: [
413+
(_options, context) => {
414+
retryCounts.push(context.retryCount);
415+
},
416+
],
417+
},
418+
}), {instanceOf: HTTPError});
419+
420+
t.is(retryCounts.length, 3);
421+
t.is(retryCounts[0], 0); // Initial request
422+
t.is(retryCounts[1], 1); // First retry
423+
t.is(retryCounts[2], 2); // Second retry
424+
});
425+
370426
test('returning HTTP response from a beforeRequest hook', withServer, async (t, server, got) => {
371427
server.get('/', echoUrl);
372428

0 commit comments

Comments
 (0)