Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: count for error response and network errors #2966

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/docs/api/RetryHandler.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
- `state`: `RetryState` - Current retry state. It can be mutated.
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.

**`RetryState`**

It represents the retry state for a given request.

- `counter`: `number` - Current retry attempt.

### Parameter `RetryHandlers`

- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
Expand Down
22 changes: 14 additions & 8 deletions lib/handler/retry-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class RetryHandler {
}

this.retryCount = 0
this.retryCountCheckpoint = 0
this.start = 0
this.end = null
this.etag = null
Expand Down Expand Up @@ -112,10 +113,7 @@ class RetryHandler {
errorCodes,
methods
} = retryOptions
let { counter, currentTimeout } = state

currentTimeout =
currentTimeout != null && currentTimeout > 0 ? currentTimeout : minTimeout
const { counter } = state

// Any code that is not a Undici's originated and allowed to retry
if (
Expand Down Expand Up @@ -160,9 +158,7 @@ class RetryHandler {
const retryTimeout =
retryAfterHeader > 0
? Math.min(retryAfterHeader, maxTimeout)
: Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout)

state.currentTimeout = retryTimeout
: Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)

setTimeout(() => cb(null), retryTimeout)
}
Expand Down Expand Up @@ -310,10 +306,19 @@ class RetryHandler {
return this.handler.onError(err)
}

// We reconcile in case of a mix between network errors
// and server error response
if (this.retryCount - this.retryCountCheckpoint > 0) {
// We count the difference between the last checkpoint and the current retry count
this.retryCount = this.retryCountCheckpoint + (this.retryCount - this.retryCountCheckpoint)
} else {
this.retryCount += 1
}

this.retryOpts.retry(
err,
{
state: { counter: this.retryCount++, currentTimeout: this.retryAfter },
state: { counter: this.retryCount },
opts: { retryOptions: this.retryOpts, ...this.opts }
},
onRetry.bind(this)
Expand All @@ -335,6 +340,7 @@ class RetryHandler {
}

try {
this.retryCountCheckpoint = this.retryCount
this.dispatch(this.opts, this)
} catch (err) {
this.handler.onError(err)
Expand Down
100 changes: 99 additions & 1 deletion test/retry-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,105 @@ test('Should retry status code', async t => {
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
counter++
++counter

if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}

return done(err)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}

server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
return
case 1:
res.writeHead(500)
res.end('failed')
return
case 2:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})

server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect () {
t.ok(true, 'pass')
},
onBodySent () {
t.ok(true, 'pass')
},
onHeaders (status, _rawHeaders, resume, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
t.strictEqual(counter, 2)
},
onError () {
t.fail()
}
}
})

after(async () => {
await client.close()
server.close()

await once(server, 'close')
})

client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})

await t.completed
})

test('Should account for network and response errors', async t => {
t = tspl(t, { plan: 4 })

let counter = 0
const chunks = []
const server = createServer()
const dispatchOptions = {
retryOptions: {
retry: (err, { state, opts }, done) => {
counter = state.counter

if (
err.statusCode === 500 ||
Expand Down
2 changes: 1 addition & 1 deletion types/retry-handler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ declare class RetryHandler implements Dispatcher.DispatchHandlers {
}

declare namespace RetryHandler {
export type RetryState = { counter: number; currentTimeout: number };
export type RetryState = { counter: number; };

export type RetryContext = {
state: RetryState;
Expand Down
Loading