Skip to content

Commit d49304e

Browse files
authored
Add RetryAgent (#2798)
Signed-off-by: Matteo Collina <[email protected]>
1 parent 7936d69 commit d49304e

File tree

10 files changed

+183
-4
lines changed

10 files changed

+183
-4
lines changed

docs/api/RetryAgent.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Class: RetryAgent
2+
3+
Extends: `undici.Dispatcher`
4+
5+
A `undici.Dispatcher` that allows to automatically retry a request.
6+
Wraps a `undici.RetryHandler`.
7+
8+
## `new RetryAgent(dispatcher, [options])`
9+
10+
Arguments:
11+
12+
* **dispatcher** `undici.Dispatcher` (required) - the dispactgher to wrap
13+
* **options** `RetryHandlerOptions` (optional) - the options
14+
15+
Returns: `ProxyAgent`
16+
17+
### Parameter: `RetryHandlerOptions`
18+
19+
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
20+
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
21+
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
22+
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
23+
- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
24+
- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
25+
-
26+
- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
27+
- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
28+
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']`
29+
30+
**`RetryContext`**
31+
32+
- `state`: `RetryState` - Current retry state. It can be mutated.
33+
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
34+
35+
Example:
36+
37+
```js
38+
import { Agent, RetryAgent } from 'undici'
39+
40+
const agent = new RetryAgent(new Agent())
41+
42+
const res = await agent.request('http://example.com')
43+
console.log(res.statuCode)
44+
console.log(await res.body.text())
45+
```

docs/api/RetryHandler.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions).
2828
-
2929
- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']`
3030
- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
31-
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN',
31+
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET']`
3232

3333
**`RetryContext`**
3434

docsify/sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* [BalancedPool](/docs/api/BalancedPool.md "Undici API - BalancedPool")
99
* [Agent](/docs/api/Agent.md "Undici API - Agent")
1010
* [ProxyAgent](/docs/api/ProxyAgent.md "Undici API - ProxyAgent")
11+
* [RetryAgent](/docs/api/RetryAgent.md "Undici API - RetryAgent")
1112
* [Connector](/docs/api/Connector.md "Custom connector")
1213
* [Errors](/docs/api/Errors.md "Undici API - Errors")
1314
* [EventSource](/docs/api/EventSource.md "Undici API - EventSource")

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const MockAgent = require('./lib/mock/mock-agent')
1515
const MockPool = require('./lib/mock/mock-pool')
1616
const mockErrors = require('./lib/mock/mock-errors')
1717
const ProxyAgent = require('./lib/proxy-agent')
18+
const RetryAgent = require('./lib/retry-agent')
1819
const RetryHandler = require('./lib/handler/RetryHandler')
1920
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
2021
const DecoratorHandler = require('./lib/handler/DecoratorHandler')
@@ -29,6 +30,7 @@ module.exports.Pool = Pool
2930
module.exports.BalancedPool = BalancedPool
3031
module.exports.Agent = Agent
3132
module.exports.ProxyAgent = ProxyAgent
33+
module.exports.RetryAgent = RetryAgent
3234
module.exports.RetryHandler = RetryHandler
3335

3436
module.exports.DecoratorHandler = DecoratorHandler

lib/handler/RetryHandler.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ class RetryHandler {
5353
'ENETUNREACH',
5454
'EHOSTDOWN',
5555
'EHOSTUNREACH',
56-
'EPIPE'
56+
'EPIPE',
57+
'UND_ERR_SOCKET'
5758
]
5859
}
5960

@@ -119,7 +120,6 @@ class RetryHandler {
119120
if (
120121
code &&
121122
code !== 'UND_ERR_REQ_RETRY' &&
122-
code !== 'UND_ERR_SOCKET' &&
123123
!errorCodes.includes(code)
124124
) {
125125
cb(err)

lib/retry-agent.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict'
2+
3+
const Dispatcher = require('./dispatcher')
4+
const RetryHandler = require('./handler/RetryHandler')
5+
6+
class RetryAgent extends Dispatcher {
7+
#agent = null
8+
#options = null
9+
constructor (agent, options = {}) {
10+
super(options)
11+
this.#agent = agent
12+
this.#options = options
13+
}
14+
15+
dispatch (opts, handler) {
16+
const retry = new RetryHandler({
17+
...opts,
18+
retryOptions: this.#options
19+
}, {
20+
dispatch: this.#agent.dispatch.bind(this.#agent),
21+
handler
22+
})
23+
return this.#agent.dispatch(opts, retry)
24+
}
25+
26+
close () {
27+
return this.#agent.close()
28+
}
29+
30+
destroy () {
31+
return this.#agent.destroy()
32+
}
33+
}
34+
35+
module.exports = RetryAgent

test/retry-agent.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
'use strict'
2+
3+
const { tspl } = require('@matteo.collina/tspl')
4+
const { test, after } = require('node:test')
5+
const { createServer } = require('node:http')
6+
const { once } = require('node:events')
7+
8+
const { RetryAgent, Client } = require('..')
9+
test('Should retry status code', async t => {
10+
t = tspl(t, { plan: 2 })
11+
12+
let counter = 0
13+
const server = createServer()
14+
const opts = {
15+
maxRetries: 5,
16+
timeout: 1,
17+
timeoutFactor: 1
18+
}
19+
20+
server.on('request', (req, res) => {
21+
switch (counter++) {
22+
case 0:
23+
req.destroy()
24+
return
25+
case 1:
26+
res.writeHead(500)
27+
res.end('failed')
28+
return
29+
case 2:
30+
res.writeHead(200)
31+
res.end('hello world!')
32+
return
33+
default:
34+
t.fail()
35+
}
36+
})
37+
38+
server.listen(0, () => {
39+
const client = new Client(`http://localhost:${server.address().port}`)
40+
const agent = new RetryAgent(client, opts)
41+
42+
after(async () => {
43+
await agent.close()
44+
server.close()
45+
46+
await once(server, 'close')
47+
})
48+
49+
agent.request({
50+
method: 'GET',
51+
path: '/',
52+
headers: {
53+
'content-type': 'application/json'
54+
}
55+
}).then((res) => {
56+
t.equal(res.statusCode, 200)
57+
res.body.setEncoding('utf8')
58+
let chunks = ''
59+
res.body.on('data', chunk => { chunks += chunk })
60+
res.body.on('end', () => {
61+
t.equal(chunks, 'hello world!')
62+
})
63+
})
64+
})
65+
66+
await t.completed
67+
})

test/types/retry-agent.test-d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { expectAssignable } from 'tsd'
2+
import { RetryAgent, Agent } from '../..'
3+
4+
const dispatcher = new Agent()
5+
6+
expectAssignable<RetryAgent>(new RetryAgent(dispatcher))
7+
expectAssignable<RetryAgent>(new RetryAgent(dispatcher, { maxRetries: 5 }))
8+
9+
{
10+
const retryAgent = new RetryAgent(dispatcher)
11+
12+
// close
13+
expectAssignable<Promise<void>>(retryAgent.close())
14+
15+
// dispatch
16+
expectAssignable<boolean>(retryAgent.dispatch({ origin: '', path: '', method: 'GET' }, {}))
17+
}

types/index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import MockAgent from'./mock-agent'
1515
import mockErrors from'./mock-errors'
1616
import ProxyAgent from'./proxy-agent'
1717
import RetryHandler from'./retry-handler'
18+
import RetryAgent from'./retry-agent'
1819
import { request, pipeline, stream, connect, upgrade } from './api'
1920

2021
export * from './util'
@@ -30,7 +31,7 @@ export * from './content-type'
3031
export * from './cache'
3132
export { Interceptable } from './mock-interceptor'
3233

33-
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler }
34+
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent }
3435
export default Undici
3536

3637
declare namespace Undici {

types/retry-agent.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Agent from './agent'
2+
import buildConnector from './connector';
3+
import Dispatcher from './dispatcher'
4+
import { IncomingHttpHeaders } from './header'
5+
import RetryHandler from './retry-handler'
6+
7+
export default RetryAgent
8+
9+
declare class RetryAgent extends Dispatcher {
10+
constructor(dispatcher: Dispatcher, options?: RetryHandler.RetryOptions)
11+
}

0 commit comments

Comments
 (0)