Skip to content

Commit c61a57d

Browse files
metcoder95Mert Can Altin
authored andcommitted
fix: count for error response and network errors (nodejs#2966)
* fix: count for error response and network errors * test: add specific test for fix * fix: conflict * refactor: address feedback
1 parent 07647db commit c61a57d

File tree

4 files changed

+120
-10
lines changed

4 files changed

+120
-10
lines changed

docs/docs/api/RetryHandler.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
3535
- `state`: `RetryState` - Current retry state. It can be mutated.
3636
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
3737

38+
**`RetryState`**
39+
40+
It represents the retry state for a given request.
41+
42+
- `counter`: `number` - Current retry attempt.
43+
3844
### Parameter `RetryHandlers`
3945

4046
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.

lib/handler/retry-handler.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class RetryHandler {
6060
}
6161

6262
this.retryCount = 0
63+
this.retryCountCheckpoint = 0
6364
this.start = 0
6465
this.end = null
6566
this.etag = null
@@ -112,10 +113,7 @@ class RetryHandler {
112113
errorCodes,
113114
methods
114115
} = retryOptions
115-
let { counter, currentTimeout } = state
116-
117-
currentTimeout =
118-
currentTimeout != null && currentTimeout > 0 ? currentTimeout : minTimeout
116+
const { counter } = state
119117

120118
// Any code that is not a Undici's originated and allowed to retry
121119
if (
@@ -160,9 +158,7 @@ class RetryHandler {
160158
const retryTimeout =
161159
retryAfterHeader > 0
162160
? Math.min(retryAfterHeader, maxTimeout)
163-
: Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout)
164-
165-
state.currentTimeout = retryTimeout
161+
: Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
166162

167163
setTimeout(() => cb(null), retryTimeout)
168164
}
@@ -310,10 +306,19 @@ class RetryHandler {
310306
return this.handler.onError(err)
311307
}
312308

309+
// We reconcile in case of a mix between network errors
310+
// and server error response
311+
if (this.retryCount - this.retryCountCheckpoint > 0) {
312+
// We count the difference between the last checkpoint and the current retry count
313+
this.retryCount = this.retryCountCheckpoint + (this.retryCount - this.retryCountCheckpoint)
314+
} else {
315+
this.retryCount += 1
316+
}
317+
313318
this.retryOpts.retry(
314319
err,
315320
{
316-
state: { counter: this.retryCount++, currentTimeout: this.retryAfter },
321+
state: { counter: this.retryCount },
317322
opts: { retryOptions: this.retryOpts, ...this.opts }
318323
},
319324
onRetry.bind(this)
@@ -335,6 +340,7 @@ class RetryHandler {
335340
}
336341

337342
try {
343+
this.retryCountCheckpoint = this.retryCount
338344
this.dispatch(this.opts, this)
339345
} catch (err) {
340346
this.handler.onError(err)

test/retry-handler.js

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,105 @@ test('Should retry status code', async t => {
1717
const dispatchOptions = {
1818
retryOptions: {
1919
retry: (err, { state, opts }, done) => {
20-
counter++
20+
++counter
21+
22+
if (
23+
err.statusCode === 500 ||
24+
err.message.includes('other side closed')
25+
) {
26+
setTimeout(done, 500)
27+
return
28+
}
29+
30+
return done(err)
31+
}
32+
},
33+
method: 'GET',
34+
path: '/',
35+
headers: {
36+
'content-type': 'application/json'
37+
}
38+
}
39+
40+
server.on('request', (req, res) => {
41+
switch (counter) {
42+
case 0:
43+
req.destroy()
44+
return
45+
case 1:
46+
res.writeHead(500)
47+
res.end('failed')
48+
return
49+
case 2:
50+
res.writeHead(200)
51+
res.end('hello world!')
52+
return
53+
default:
54+
t.fail()
55+
}
56+
})
57+
58+
server.listen(0, () => {
59+
const client = new Client(`http://localhost:${server.address().port}`)
60+
const handler = new RetryHandler(dispatchOptions, {
61+
dispatch: client.dispatch.bind(client),
62+
handler: {
63+
onConnect () {
64+
t.ok(true, 'pass')
65+
},
66+
onBodySent () {
67+
t.ok(true, 'pass')
68+
},
69+
onHeaders (status, _rawHeaders, resume, _statusMessage) {
70+
t.strictEqual(status, 200)
71+
return true
72+
},
73+
onData (chunk) {
74+
chunks.push(chunk)
75+
return true
76+
},
77+
onComplete () {
78+
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
79+
t.strictEqual(counter, 2)
80+
},
81+
onError () {
82+
t.fail()
83+
}
84+
}
85+
})
86+
87+
after(async () => {
88+
await client.close()
89+
server.close()
90+
91+
await once(server, 'close')
92+
})
93+
94+
client.dispatch(
95+
{
96+
method: 'GET',
97+
path: '/',
98+
headers: {
99+
'content-type': 'application/json'
100+
}
101+
},
102+
handler
103+
)
104+
})
105+
106+
await t.completed
107+
})
108+
109+
test('Should account for network and response errors', async t => {
110+
t = tspl(t, { plan: 4 })
111+
112+
let counter = 0
113+
const chunks = []
114+
const server = createServer()
115+
const dispatchOptions = {
116+
retryOptions: {
117+
retry: (err, { state, opts }, done) => {
118+
counter = state.counter
21119

22120
if (
23121
err.statusCode === 500 ||

types/retry-handler.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ declare class RetryHandler implements Dispatcher.DispatchHandlers {
1212
}
1313

1414
declare namespace RetryHandler {
15-
export type RetryState = { counter: number; currentTimeout: number };
15+
export type RetryState = { counter: number; };
1616

1717
export type RetryContext = {
1818
state: RetryState;

0 commit comments

Comments
 (0)