Skip to content

Commit b79a22d

Browse files
authored
Merge pull request #332 from gadget-inc/add-custom-retry-handler
[feature] custom retry callback
2 parents 5d744b3 + 8e8dc98 commit b79a22d

4 files changed

Lines changed: 219 additions & 47 deletions

File tree

README.md

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ npm i @fastify/reply-from
1414
```
1515

1616
## Compatibility with @fastify/multipart
17-
`@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins which have a parent-child relationship.<br> The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.<br> This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.<br>
17+
`@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins which have a parent-child relationship.`<br>` The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.`<br>` This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.`<br>`
1818
However, the two plugins may be used within the same fastify instance, at the condition that they belong to disjoint branches of the fastify plugins hierarchy tree.
1919

2020
## Usage
@@ -115,7 +115,7 @@ proxy.register(require('@fastify/reply-from'), {
115115

116116
#### `http`
117117

118-
Set the `http` option to an Object to use
118+
Set the `http` option to an Object to use
119119
Node's [`http.request`](https://nodejs.org/api/http.html#http_http_request_options_callback)
120120
will be used if you do not enable [`http2`](#http2). To customize the `request`,
121121
you can pass in [`agentOptions`](https://nodejs.org/api/http.html#http_new_agent_options) and
@@ -136,6 +136,7 @@ proxy.register(require('@fastify/reply-from'), {
136136
```
137137

138138
You can also pass custom HTTP agents. If you pass the agents, then the http.agentOptions will be ignored. To illustrate:
139+
139140
```js
140141
proxy.register(require('@fastify/reply-from'), {
141142
base: 'http://localhost:3001/',
@@ -192,27 +193,27 @@ The number of parsed URLs that will be cached. Default: `100`.
192193

193194
#### `disableCache`
194195

195-
This option will disable the URL caching.
196+
This option will disable the URL caching.
196197
This cache is dedicated to reduce the amount of URL object generation.
197198
Generating URLs is a main bottleneck of this module, please disable this cache with caution.
198199

199200
#### `contentTypesToEncode`
200201

201-
An array of content types whose response body will be passed through `JSON.stringify()`.
202+
An array of content types whose response body will be passed through `JSON.stringify()`.
202203
This only applies when a custom [`body`](#body) is not passed in. Defaults to:
203204

204205
```js
205-
[
206+
[
206207
'application/json'
207208
]
208209
```
209210

210211
#### `retryMethods`
211212

212-
On which methods should the connection be retried in case of socket hang up.
213+
On which methods should the connection be retried in case of socket hang up.
213214
**Be aware** that setting here not idempotent method may lead to unexpected results on target.
214215

215-
By default: `['GET', 'HEAD', 'OPTIONS', 'TRACE' ]`
216+
By default: `['GET', 'HEAD', 'OPTIONS', 'TRACE']`
216217

217218
This plugin will always retry on 503 errors, _unless_ `retryMethods` does not contain `GET`.
218219

@@ -221,6 +222,7 @@ This plugin will always retry on 503 errors, _unless_ `retryMethods` does not co
221222
Enables the possibility to explictly opt-in for global agents.
222223

223224
Usage for undici global agent:
225+
224226
```js
225227
import { setGlobalDispatcher, ProxyAgent } from 'undici'
226228

@@ -234,11 +236,12 @@ fastify.register(FastifyReplyFrom, {
234236
```
235237

236238
Usage for http/https global agent:
239+
237240
```js
238241
fastify.register(FastifyReplyFrom, {
239242
base: 'http://localhost:3001/',
240243
// http and https is allowed to use http.globalAgent or https.globalAgent
241-
globalAgent: true,
244+
globalAgent: true,
242245
http: {
243246
}
244247
})
@@ -263,6 +266,39 @@ This option set the limit on how many times the plugin should retry the request,
263266

264267
By Default: 10
265268

269+
---
270+
271+
### `customRetry`
272+
273+
- `handler`. Required
274+
- `retries`. Optional
275+
276+
This plugin gives the client an option to pass their own retry callback to handle retries on their own.
277+
If a `handler` is passed to the `customRetry` object the onus is on the client to invoke the default retry logic in their callback otherwise default cases such as 503 will not be handled
278+
279+
Given example
280+
281+
```js
282+
const customRetryLogic = (req, res, getDefaultRetry) => {
283+
//If this block is not included all non 500 errors will not be retried
284+
const defaultDelay = getDefaultDelay();
285+
if (defaultDelay) return defaultDelay();
286+
287+
//Custom retry logic
288+
if (res && res.statusCode === 500 && req.method === 'GET') {
289+
return 300
290+
}
291+
return null
292+
}
293+
294+
.......
295+
296+
fastify.register(FastifyReplyFrom, {
297+
base: 'http://localhost:3001/',
298+
customRetry: {handler: customRetryLogic, retries: 10}
299+
})
300+
301+
```
266302

267303
---
268304

@@ -328,6 +364,7 @@ ContraintStrategies.
328364
e.g.:
329365

330366
Route grpc-web/http1 and grpc/http2 to different routes with a ContentType-ConstraintStrategy:
367+
331368
```
332369
const contentTypeMatchContraintStrategy = {
333370
// strategy name for referencing in the route handler `constraints` options
@@ -341,36 +378,36 @@ const contentTypeMatchContraintStrategy = {
341378
}
342379
},
343380
// function to get the value of the constraint from each incoming request
344-
deriveConstraint: (req: any, ctx: any) => {
381+
deriveConstraint: (req: any, ctx: any) => {
345382
return req.headers['content-type']
346383
},
347384
// optional flag marking if handlers without constraints can match requests that have a value for this constraint
348385
mustMatchWhenDerived: true
349386
}
350-
387+
351388
server.addConstraintStrategy(contentTypeMatchContraintStrategy);
352389
```
353390

354391
and then 2 different upstreams with different register's:
392+
355393
```
356394
// grpc-web / http1
357395
server.register(fastifyHttpProxy, {
358396
// Although most browsers send with http2, nodejs cannot handle this http2 request
359397
// therefore we have to transport to the grpc-web-proxy via http1
360398
http2: false,
361399
upstream: 'http://grpc-web-proxy',
362-
constraints: { "contentType": "application/grpc-web+proto" }
400+
constraints: { "contentType": "application/grpc-web+proto" }
363401
});
364402
365403
// grpc / http2
366404
server.register(fastifyHttpProxy, {
367405
http2: true,
368406
upstream: 'http://grpc.server',
369-
constraints: { "contentType": "application/grpc+proto" }
407+
constraints: { "contentType": "application/grpc+proto" }
370408
});
371409
```
372410

373-
374411
#### `queryString` or `queryString(search, reqUrl)`
375412

376413
Replaces the original querystring of the request with what is specified.
@@ -390,12 +427,12 @@ Setting this option to `null` will strip the body (and `content-type` header) en
390427

391428
#### `method`
392429

393-
Replaces the original request method with what is specified.
430+
Replaces the original request method with what is specified.
394431

395432
#### `retriesCount`
396433

397-
How many times it will try to pick another connection on socket hangup (`ECONNRESET` error).
398-
Useful when keeping the connection open (KeepAlive).
434+
How many times it will try to pick another connection on socket hangup (`ECONNRESET` error).
435+
Useful when keeping the connection open (KeepAlive).
399436
This number should be a function of the number of connections and the number of instances of a target.
400437

401438
By default: 0 (disabled)
@@ -407,13 +444,13 @@ already overriding the [`body`](#body).
407444

408445
### Combining with [@fastify/formbody](https://github.com/fastify/fastify-formbody)
409446

410-
`formbody` expects the body to be returned as a string and not an object.
447+
`formbody` expects the body to be returned as a string and not an object.
411448
Use the [`contentTypesToEncode`](#contentTypesToEncode) option to pass in `['application/x-www-form-urlencoded']`
412449

413-
414450
### HTTP & HTTP2 timeouts
415451

416452
This library has:
453+
417454
- `timeout` for `http` set by default. The default value is 10 seconds (`10000`).
418455
- `requestTimeout` & `sessionTimeout` for `http2` set by default.
419456
- The default value for `requestTimeout` is 10 seconds (`10000`).
@@ -426,10 +463,10 @@ will be returned to the client.
426463

427464
* [ ] support overriding the body with a stream
428465
* [ ] forward the request id to the other peer might require some
429-
refactoring because we have to make the `req.id` unique
430-
(see [hyperid](https://npm.im/hyperid)).
466+
refactoring because we have to make the `req.id` unique
467+
(see [hyperid](https://npm.im/hyperid)).
431468
* [ ] Support origin HTTP2 push
432-
* [x] benchmarks
469+
* [X] benchmarks
433470

434471
## License
435472

index.js

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
2929
])
3030

3131
const retryMethods = new Set(opts.retryMethods || [
32-
'GET', 'HEAD', 'OPTIONS', 'TRACE'
33-
])
32+
'GET', 'HEAD', 'OPTIONS', 'TRACE'])
3433

3534
const cache = opts.disableCache ? undefined : lru(opts.cacheURLs || 100)
3635
const base = opts.base
@@ -60,6 +59,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
6059
const onError = opts.onError || onErrorDefault
6160
const retriesCount = opts.retriesCount || 0
6261
const maxRetriesOn503 = opts.maxRetriesOn503 || 10
62+
const customRetry = opts.customRetry || undefined
6363

6464
if (!source) {
6565
source = req.url
@@ -143,7 +143,35 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
143143
const contentLength = requestHeaders['content-length']
144144
let requestImpl
145145
if (retryMethods.has(method) && !contentLength) {
146-
requestImpl = createRequestRetry(request, this, retriesCount, retryOnError, maxRetriesOn503)
146+
const retryHandler = (req, res, err, retries) => {
147+
const defaultDelay = () => {
148+
// Magic number, so why not 42? We might want to make this configurable.
149+
let retryAfter = 42 * Math.random() * (retries + 1)
150+
151+
if (res && res.headers['retry-after']) {
152+
retryAfter = res.headers['retry-after']
153+
}
154+
if (res && res.statusCode === 503 && req.method === 'GET') {
155+
if (retriesCount === 0 && retries < maxRetriesOn503) {
156+
// we should stop at some point
157+
return retryAfter
158+
}
159+
} else if (retriesCount > retries && err && err.code === retryOnError) {
160+
return retryAfter
161+
}
162+
return null
163+
}
164+
165+
if (customRetry && customRetry.handler) {
166+
const customRetries = customRetry.retries || 1
167+
if (++retries < customRetries) {
168+
return customRetry.handler(req, res, defaultDelay)
169+
}
170+
}
171+
return defaultDelay()
172+
}
173+
174+
requestImpl = createRequestRetry(request, this, retryHandler)
147175
} else {
148176
requestImpl = request
149177
}
@@ -251,28 +279,15 @@ function isFastifyMultipartRegistered (fastify) {
251279
return (fastify.hasContentTypeParser('multipart') || fastify.hasContentTypeParser('multipart/form-data')) && fastify.hasRequestDecorator('multipart')
252280
}
253281

254-
function createRequestRetry (requestImpl, reply, retriesCount, retryOnError, maxRetriesOn503) {
282+
function createRequestRetry (requestImpl, reply, retryHandler) {
255283
function requestRetry (req, cb) {
256284
let retries = 0
257285

258286
function run () {
259287
requestImpl(req, function (err, res) {
260-
// Magic number, so why not 42? We might want to make this configurable.
261-
let retryAfter = 42 * Math.random() * (retries + 1)
262-
263-
if (res && res.headers['retry-after']) {
264-
retryAfter = res.headers['retry-after']
265-
}
266-
if (!reply.sent) {
267-
// always retry on 503 errors
268-
if (res && res.statusCode === 503 && req.method === 'GET') {
269-
if (retriesCount === 0 && retries < maxRetriesOn503) {
270-
// we should stop at some point
271-
return retry(retryAfter)
272-
}
273-
} else if (retriesCount > retries && err && err.code === retryOnError) {
274-
return retry(retryAfter)
275-
}
288+
const retryDelay = retryHandler(req, res, err, retries)
289+
if (!reply.sent && retryDelay) {
290+
return retry(retryDelay)
276291
}
277292
cb(err, res)
278293
})

0 commit comments

Comments
 (0)