Skip to content

Commit 5f3fbb9

Browse files
authored
add: error object to custom retry callback (#337)
1 parent 8671f3b commit 5f3fbb9

5 files changed

Lines changed: 140 additions & 68 deletions

File tree

README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,18 +268,24 @@ By Default: 10
268268

269269
---
270270

271-
### `customRetry`
271+
### `retryDelay`
272272

273273
- `handler`. Required
274-
- `retries`. Optional
275274

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
275+
This plugin gives the client an option to pass their own retry callback to allow the client to define what retryDelay they would like on any retries
276+
outside the scope of what is handled by default in fastify-reply-from. To see the default please refer to index.js `getDefaultDelay()`
277+
If a `handler` is passed to the `retryDelay` object the onus is on the client to invoke the default retry logic in their callback otherwise default cases such as 500 will not be handled
278+
279+
- `err` is the error thrown by making a request using whichever agent is configured
280+
- `req` is the raw request details sent to the underlying agent. __Note__: this object is not a Fastify request object, but instead the low-level request for the agent.
281+
- `res` is the raw response returned by the underlying agent (if available) __Note__: this object is not a Fastify response, but instead the low-level response from the agent. This property may be null if no response was obtained at all, like from a connection reset or timeout.
282+
- `attempt` in the object callback refers to the current retriesAttempt number. You are given the freedom to use this in concert with the retryCount property set to handle retries
283+
- `getDefaultRetry` refers to the default retry handler. If this callback returns not null and you wish to handle those case of errors simply invoke it as done below.
278284

279285
Given example
280286

281287
```js
282-
const customRetryLogic = (req, res, getDefaultRetry) => {
288+
const customRetryLogic = ({err, req, res, attempt, getDefaultRetry}) => {
283289
//If this block is not included all non 500 errors will not be retried
284290
const defaultDelay = getDefaultDelay();
285291
if (defaultDelay) return defaultDelay();
@@ -288,18 +294,31 @@ Given example
288294
if (res && res.statusCode === 500 && req.method === 'GET') {
289295
return 300
290296
}
297+
298+
if (err && err.code == "UND_ERR_SOCKET"){
299+
return 600
300+
}
301+
291302
return null
292303
}
293304

294305
.......
295306

296307
fastify.register(FastifyReplyFrom, {
297308
base: 'http://localhost:3001/',
298-
customRetry: {handler: customRetryLogic, retries: 10}
309+
retryDelay: customRetryLogic
299310
})
300311

301312
```
302313

314+
Note the Typescript Equivalent
315+
```
316+
const customRetryLogic = ({req, res, err, getDefaultRetry}: RetryDetails) => {
317+
...
318+
}
319+
...
320+
321+
```
303322
---
304323

305324
### `reply.from(source, [opts])`

index.js

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
5959
const onError = opts.onError || onErrorDefault
6060
const retriesCount = opts.retriesCount || 0
6161
const maxRetriesOn503 = opts.maxRetriesOn503 || 10
62-
const customRetry = opts.customRetry || undefined
62+
const retryDelay = opts.retryDelay || undefined
6363

6464
if (!source) {
6565
source = req.url
@@ -142,38 +142,32 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
142142
const requestHeaders = rewriteRequestHeaders(this.request, headers)
143143
const contentLength = requestHeaders['content-length']
144144
let requestImpl
145-
if (retryMethods.has(method) && !contentLength) {
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-
}
164145

165-
if (customRetry && customRetry.handler) {
166-
const customRetries = customRetry.retries || 1
167-
if (++retries < customRetries) {
168-
return customRetry.handler(req, res, defaultDelay)
146+
const getDefaultDelay = (req, res, err, retries) => {
147+
if (retryMethods.has(method) && !contentLength) {
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+
return retryAfter
169157
}
158+
} else if (retriesCount > retries && err && err.code === retryOnError) {
159+
return retryAfter
170160
}
171-
return defaultDelay()
172161
}
162+
return null
163+
}
173164

174-
requestImpl = createRequestRetry(request, this, retryHandler)
165+
if (retryDelay) {
166+
requestImpl = createRequestRetry(request, this, (req, res, err, retries) => {
167+
return retryDelay({ err, req, res, attempt: retries, getDefaultDelay })
168+
})
175169
} else {
176-
requestImpl = request
170+
requestImpl = createRequestRetry(request, this, getDefaultDelay)
177171
}
178172

179173
requestImpl({ method, url, qs, headers: requestHeaders, body }, (err, res) => {
@@ -228,7 +222,6 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
228222
// actually destroy those sockets
229223
setImmediate(next)
230224
})
231-
232225
next()
233226
}, {
234227
fastify: '4.x',

test/retry-with-a-custom-handler.test.js

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ const From = require('..')
66
const http = require('node:http')
77
const got = require('got')
88

9-
function serverWithCustomError (stopAfter, statusCodeToFailOn) {
9+
function serverWithCustomError (stopAfter, statusCodeToFailOn, closeSocket) {
1010
let requestCount = 0
1111
return http.createServer((req, res) => {
1212
if (requestCount++ < stopAfter) {
13+
if (closeSocket) req.socket.end()
1314
res.statusCode = statusCodeToFailOn
1415
res.setHeader('Content-Type', 'text/plain')
1516
return res.end('This Service is Unavailable')
@@ -21,8 +22,9 @@ function serverWithCustomError (stopAfter, statusCodeToFailOn) {
2122
})
2223
}
2324

24-
async function setupServer (t, fromOptions = {}, statusCodeToFailOn = 500, stopAfter = 4) {
25-
const target = serverWithCustomError(stopAfter, statusCodeToFailOn)
25+
async function setupServer (t, fromOptions = {}, statusCodeToFailOn = 500, stopAfter = 4, closeSocket = false) {
26+
const target = serverWithCustomError(stopAfter, statusCodeToFailOn, closeSocket)
27+
2628
await target.listen({ port: 0 })
2729
t.teardown(target.close.bind(target))
2830

@@ -48,7 +50,7 @@ test('a 500 status code with no custom handler should fail', async (t) => {
4850

4951
let errorMessage
5052
try {
51-
await got.get(`http://localhost:${instance.server.address().port}`, { retry: 0 })
53+
await got.get(`http://localhost:${instance.server.address().port}`, { retry: 3 })
5254
} catch (error) {
5355
errorMessage = error.message
5456
}
@@ -57,63 +59,103 @@ test('a 500 status code with no custom handler should fail', async (t) => {
5759
})
5860

5961
test("a server 500's with a custom handler and should revive", async (t) => {
60-
const customRetryLogic = (req, res, getDefaultDelay) => {
62+
const customRetryLogic = ({ req, res, err, attempt, getDefaultDelay }) => {
6163
const defaultDelay = getDefaultDelay()
6264
if (defaultDelay) return defaultDelay
6365

6466
if (res && res.statusCode === 500 && req.method === 'GET') {
65-
return 300
67+
return 0.1
6668
}
6769
return null
6870
}
6971

70-
const { instance } = await setupServer(t, { customRetry: { handler: customRetryLogic, retries: 10 } })
72+
const { instance } = await setupServer(t, { retryDelay: customRetryLogic })
7173

72-
const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 0 })
74+
const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 5 })
7375

7476
t.equal(res.headers['content-type'], 'text/plain')
7577
t.equal(res.statusCode, 205)
7678
t.equal(res.body.toString(), 'Hello World 5!')
7779
})
7880

79-
test('custom retry does not invoke the default delay causing a 503', async (t) => {
80-
// the key here is our customRetryHandler doesn't register the deefault handler and as a result it doesn't work
81-
const customRetryLogic = (req, res, getDefaultDelay) => {
81+
test('custom retry does not invoke the default delay causing a 501', async (t) => {
82+
// the key here is our retryDelay doesn't register the deefault handler and as a result it doesn't work
83+
const customRetryLogic = ({ req, res, err, attempt, getDefaultDelay }) => {
8284
if (res && res.statusCode === 500 && req.method === 'GET') {
83-
return 300
85+
return 0
8486
}
8587
return null
8688
}
8789

88-
const { instance } = await setupServer(t, { customRetry: { handler: customRetryLogic, retries: 10 } }, 503)
90+
const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 501)
8991

9092
let errorMessage
9193
try {
92-
await got.get(`http://localhost:${instance.server.address().port}`, { retry: 0 })
94+
await got.get(`http://localhost:${instance.server.address().port}`, { retry: 5 })
9395
} catch (error) {
9496
errorMessage = error.message
9597
}
9698

97-
t.equal(errorMessage, 'Response code 503 (Service Unavailable)')
99+
t.equal(errorMessage, 'Response code 501 (Not Implemented)')
98100
})
99101

100102
test('custom retry delay functions can invoke the default delay', async (t) => {
101-
const customRetryLogic = (req, res, getDefaultDelay) => {
103+
const customRetryLogic = ({ req, res, err, attempt, getDefaultDelay }) => {
102104
// registering the default retry logic for non 500 errors if it occurs
103105
const defaultDelay = getDefaultDelay()
104106
if (defaultDelay) return defaultDelay
105107

106108
if (res && res.statusCode === 500 && req.method === 'GET') {
107-
return 300
109+
return 0.1
110+
}
111+
112+
return null
113+
}
114+
115+
const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 500)
116+
117+
const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 5 })
118+
119+
t.equal(res.headers['content-type'], 'text/plain')
120+
t.equal(res.statusCode, 205)
121+
t.equal(res.body.toString(), 'Hello World 5!')
122+
})
123+
124+
test('custom retry delay function inspects the err paramater', async (t) => {
125+
const customRetryLogic = ({ req, res, err, attempt, getDefaultDelay }) => {
126+
if (err && (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET')) {
127+
return 0.1
128+
}
129+
return null
130+
}
131+
132+
const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 500, 4, true)
133+
134+
const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 5 })
135+
136+
t.equal(res.headers['content-type'], 'text/plain')
137+
t.equal(res.statusCode, 205)
138+
t.equal(res.body.toString(), 'Hello World 5!')
139+
})
140+
141+
test('we can exceed our retryCount and introspect attempts independently', async (t) => {
142+
const attemptCounter = []
143+
144+
const customRetryLogic = ({ req, res, err, attempt, getDefaultDelay }) => {
145+
attemptCounter.push(attempt)
146+
147+
if (err && (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET')) {
148+
return 0.1
108149
}
109150

110151
return null
111152
}
112153

113-
const { instance } = await setupServer(t, { customRetry: { handler: customRetryLogic, retries: 10 } }, 503)
154+
const { instance } = await setupServer(t, { retryDelay: customRetryLogic }, 500, 4, true)
114155

115-
const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 0 })
156+
const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 5 })
116157

158+
t.match(attemptCounter, [0, 1, 2, 3, 4])
117159
t.equal(res.headers['content-type'], 'text/plain')
118160
t.equal(res.statusCode, 205)
119161
t.equal(res.body.toString(), 'Hello World 5!')

types/index.d.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
/// <reference types="node" />
22

33
import {
4-
FastifyRequest,
4+
FastifyPluginCallback,
55
FastifyReply,
6+
FastifyRequest,
7+
HTTPMethods,
68
RawReplyDefaultExpression,
79
RawServerBase,
810
RequestGenericInterface,
9-
HTTPMethods,
10-
FastifyPluginCallback,
1111
} from 'fastify';
1212

1313
import {
14+
Agent,
15+
AgentOptions,
1416
IncomingHttpHeaders,
1517
RequestOptions,
16-
AgentOptions,
17-
Agent,
1818
} from "http";
1919
import {
20-
RequestOptions as SecureRequestOptions,
21-
AgentOptions as SecureAgentOptions,
22-
Agent as SecureAgent
23-
} from "https";
24-
import {
25-
IncomingHttpHeaders as Http2IncomingHttpHeaders,
26-
ClientSessionRequestOptions,
2720
ClientSessionOptions,
21+
ClientSessionRequestOptions,
22+
IncomingHttpHeaders as Http2IncomingHttpHeaders,
2823
SecureClientSessionOptions,
2924
} from "http2";
30-
import { Pool } from 'undici'
25+
import {
26+
Agent as SecureAgent,
27+
AgentOptions as SecureAgentOptions,
28+
RequestOptions as SecureRequestOptions
29+
} from "https";
30+
import { Pool } from 'undici';
3131

3232
declare module "fastify" {
3333
interface FastifyReply {
@@ -39,12 +39,21 @@ declare module "fastify" {
3939
}
4040

4141
type FastifyReplyFrom = FastifyPluginCallback<fastifyReplyFrom.FastifyReplyFromOptions>
42-
4342
declare namespace fastifyReplyFrom {
4443
type QueryStringFunction = (search: string | undefined, reqUrl: string) => string;
44+
45+
export type RetryDetails = {
46+
err: Error;
47+
req: FastifyRequest<RequestGenericInterface, RawServerBase>;
48+
res: FastifyReply<RawServerBase>;
49+
attempt: number;
50+
getDefaultDelay: () => number | null;
51+
}
4552
export interface FastifyReplyFromHooks {
4653
queryString?: { [key: string]: unknown } | QueryStringFunction;
4754
contentType?: string;
55+
retryDelay?: (details: RetryDetails) => {} | null;
56+
retriesCount?: number;
4857
onResponse?: (
4958
request: FastifyRequest<RequestGenericInterface, RawServerBase>,
5059
reply: FastifyReply<RawServerBase>,
@@ -99,7 +108,7 @@ declare namespace fastifyReplyFrom {
99108
}
100109

101110
export const fastifyReplyFrom: FastifyReplyFrom
102-
export { fastifyReplyFrom as default }
111+
export { fastifyReplyFrom as default };
103112
}
104113

105114
declare function fastifyReplyFrom(...params: Parameters<FastifyReplyFrom>): ReturnType<FastifyReplyFrom>

types/index.test-d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ async function main() {
9191
instance.get("/http2", (request, reply) => {
9292
reply.from("/", {
9393
method: "POST",
94+
retryDelay: ({err, req, res, attempt, getDefaultDelay}) => {
95+
const defaultDelay = getDefaultDelay();
96+
if (defaultDelay) return defaultDelay;
97+
98+
if (res && res.statusCode === 500 && req.method === "GET") {
99+
return 300;
100+
}
101+
return null;
102+
},
94103
rewriteHeaders(headers, req) {
95104
return headers;
96105
},

0 commit comments

Comments
 (0)