'use strict'
const { tspl } = require('@matteo.collina/tspl')
const { test, after } = require('node:test')
const { createServer } = require('node:http')
const { once } = require('node:events')
const { Readable } = require('node:stream')
const { RetryHandler, Client, RetryAgent } = require('..')
const { RequestHandler } = require('../lib/api/api-request')
test('Reuses socket on retry instead of closing it', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
let socketPort
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
counter++
res.writeHead(500)
res.end('internal err')
if (!socketPort) {
socketPort = req.socket.remotePort
}
t.strictEqual(socketPort, req.socket.remotePort)
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const retryAgent = new RetryAgent(client, {
throwOnError: false,
maxRetries: 2
})
retryAgent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}).then(res => {
t.strictEqual(res.statusCode, 500)
t.strictEqual(counter, 3)
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('throws an error on network error', async t => {
t = tspl(t, { plan: 1 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.destroy()
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const retryAgent = new RetryAgent(client, {
throwOnError: false,
maxRetries: 2
})
retryAgent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}).catch(err => {
t.strictEqual(err.code, 'UND_ERR_SOCKET')
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Show pass status code errors when not eligible for retry, as normal response instead of throwing error', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
counter++
res.writeHead(500)
res.end('internal err')
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const retryAgent = new RetryAgent(client, {
throwOnError: false,
maxRetries: 2
})
retryAgent.request({
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}).then(res => {
t.strictEqual(res.statusCode, 500)
t.strictEqual(counter, 3)
res.body.text().then(text => {
t.strictEqual(text, 'internal err')
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should retry status code without throwing an error | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
++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: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
t.strictEqual(counter, 2)
},
onResponseError () {
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 | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter = state.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: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
t.strictEqual(counter, 2)
},
onResponseError () {
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('Issue #3288 - request with body (asynciterable) should fail, without throwing an error', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: (function * () {
yield 'hello'
yield 'world'
})()
}
server.on('request', (req, res) => {
res.writeHead(500, {
'content-type': 'application/json'
})
res.end('{"message": "failed"}')
})
const chunks = []
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.ok(true, 'pass')
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
const data = Buffer.concat(chunks).toString('utf-8')
t.strictEqual(data, '{"message": "failed"}')
},
onResponseError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('Should use retry-after header for retries | throwOnError: false', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(429, {
'retry-after': 1
})
res.end('rate limit')
checkpoint = Date.now()
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 500)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onResponseError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should use retry-after header for retries (date) | throwOnError: false', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
checkpoint = Date.now()
res.writeHead(429, {
'retry-after': new Date(
checkpoint + 2000
).toUTCString()
})
res.end('rate limit')
counter++
return
case 1:
res.writeHead(200)
res.end('hello world!')
t.ok(Date.now() - checkpoint >= 1000)
counter++
return
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onResponseError () {
t.fail()
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})
test('Should retry with defaults | throwOnError: false', async t => {
t = tspl(t, { plan: 5 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.on('request', (req, res) => {
switch (counter) {
case 0:
req.destroy()
counter++
return
case 1:
res.writeHead(500)
res.end('failed')
counter++
return
case 2:
res.writeHead(200)
res.end('hello world!')
counter++
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: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onResponseError () {
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 handle 206 partial content | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-5/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onResponseError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should handle 206 partial content - bad-etag | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-5/6')
res.setHeader('etag', 'erwsd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(
dispatchOptions,
{
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, _status, _headers, _statusMessage) {
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.ifError('should not complete')
},
onResponseError (_controller, err) {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abc')
t.strictEqual(err.code, 'UND_ERR_REQ_RETRY')
t.strictEqual(err.message, 'ETag mismatch')
t.deepEqual(err.data, { count: 2 })
}
}
}
)
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('retrying a request with a body | throwOnError: false', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ hello: 'world' })
}
t = tspl(t, { plan: 1 })
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: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ hello: 'world' })
},
handler
)
})
await t.completed
})
test('retrying a request with a body (stream) | throwOnError: false', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: Readable.from(Buffer.from(JSON.stringify({ hello: 'world' })))
}
t = tspl(t, { plan: 3 })
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(500)
res.end('failed')
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: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
t.equal(data.statusCode, 500)
data.body.text().then(text => {
t.equal(text, 'failed')
})
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('retrying a request with a body (buffer) | throwOnError: false', async t => {
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
if (
err.statusCode === 500 ||
err.message.includes('other side closed')
) {
setTimeout(done, 500)
return
}
return done(err)
}
},
method: 'POST',
path: '/',
headers: {
'content-type': 'application/json'
},
body: Buffer.from(JSON.stringify({ hello: 'world' }))
}
t = tspl(t, { plan: 1 })
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: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
dispatchOptions,
handler
)
})
await t.completed
})
test('should not error if request is not meant to be retried | throwOnError: false', async t => {
t = tspl(t, { plan: 3 })
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
res.writeHead(400)
res.end('Bad request')
})
const dispatchOptions = {
retryOptions: {
throwOnError: false
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const chunks = []
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 400)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'Bad request')
},
onResponseError (_controller, err) {
t.fail(err)
}
}
})
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 be able to properly pass the minTimeout to the RetryContext when constructing a RetryCallback function | throwOnError: false', async t => {
t = tspl(t, { plan: 2 })
let counter = 0
const server = createServer({ joinDuplicateHeaders: true })
server.on('request', (req, res) => {
switch (counter) {
case 0:
res.writeHead(500)
res.end('failed')
return
case 1:
res.writeHead(200)
res.end('hello world!')
return
default:
t.fail()
}
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: (err, { state, opts }, done) => {
counter++
t.strictEqual(opts.retryOptions.minTimeout, 100)
if (err.statusCode === 500) {
return done()
}
return done(err)
},
minTimeout: 100
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: new RequestHandler(dispatchOptions, (err, data) => {
t.ifError(err)
})
})
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('Issue#2986 - Handle custom 206 | throwOnError: false', async t => {
t = tspl(t, { plan: 6 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes 3-5/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onResponseError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Issue#3128 - Support if-match | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.deepStrictEqual(req.headers['if-match'], 'asd')
res.setHeader('content-range', 'bytes 3-5/6')
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onResponseError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Issue#3128 - Should ignore weak etags | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'W/asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.equal(req.headers['if-match'], undefined)
res.setHeader('content-range', 'bytes 3-5/6')
res.setHeader('etag', 'W/asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onResponseError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Weak etags are ignored on range-requests | throwOnError: false', async t => {
t = tspl(t, { plan: 7 })
const chunks = []
let counter = 0
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.deepStrictEqual(req.headers.range, 'bytes=0-3')
res.setHeader('etag', 'W/asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
t.equal(req.headers['if-match'], undefined)
res.setHeader('content-range', 'bytes 3-5/6')
res.setHeader('etag', 'W/efg')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
counter++
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abcdef')
t.strictEqual(counter, 1)
},
onResponseError () {
t.fail()
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json',
Range: 'bytes=0-3'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should throw RequestRetryError when Content-Range mismatch | throwOnError: false', async t => {
t = tspl(t, { plan: 8 })
const chunks = []
// Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47
let x = 0
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
if (x === 0) {
t.ok(true, 'pass')
res.setHeader('etag', 'asd')
res.write('abc')
setTimeout(() => {
res.destroy()
}, 1e2)
} else if (x === 1) {
t.deepStrictEqual(req.headers.range, 'bytes=3-')
res.setHeader('content-range', 'bytes bad') // intentionally bad to trigger error
res.setHeader('etag', 'asd')
res.statusCode = 206
res.end('def')
}
x++
})
const dispatchOptions = {
retryOptions: {
throwOnError: false,
retry: function (err, _, done) {
if (err.code && err.code === 'UND_ERR_DESTROYED') {
return done(false)
}
if (err.statusCode === 206) return done(err)
setTimeout(done, 800)
}
},
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
}
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: (...args) => {
return client.dispatch(...args)
},
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.ifError('should not complete')
},
onResponseError (_controller, err) {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'abc')
t.strictEqual(err.code, 'UND_ERR_REQ_RETRY')
t.strictEqual(err.message, 'Content-Range mismatch')
t.deepEqual(err.data, { count: 2 })
}
}
})
client.dispatch(
{
method: 'GET',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
})
await t.completed
})
test('Should use retry-after header for retries (date) but date format is wrong | throwOnError: false', async t => {
t = tspl(t, { plan: 3 })
let counter = 0
const chunks = []
const server = createServer({ joinDuplicateHeaders: true })
let checkpoint
const dispatchOptions = {
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
},
retryOptions: {
minTimeout: 1000,
throwOnError: false
}
}
const minRetryDelay = dispatchOptions.retryOptions.minTimeout
server.on('request', (req, res) => {
switch (counter) {
case 0: {
checkpoint = process.hrtime.bigint()
res.writeHead(429, {
'retry-after': 'this is not a date'
})
res.end('rate limit')
counter++
return
}
case 1: {
res.writeHead(200)
res.end('hello world!')
const elapsedMs = Number(process.hrtime.bigint() - checkpoint) / 1e6
t.ok(elapsedMs >= minRetryDelay - 100)
counter++
return
}
default:
t.fail('unexpected request')
}
})
server.listen(0, () => {
const client = new Client(`http://localhost:${server.address().port}`)
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onRequestStart () {
t.ok(true, 'pass')
},
onResponseStart (_controller, status, _headers, _statusMessage) {
t.strictEqual(status, 200)
return true
},
onResponseData (_controller, chunk) {
chunks.push(chunk)
return true
},
onResponseEnd () {
t.strictEqual(Buffer.concat(chunks).toString('utf-8'), 'hello world!')
},
onResponseError (_controller, err) {
t.ifError(err)
}
}
})
after(async () => {
await client.close()
server.close()
await once(server, 'close')
})
client.dispatch(
{
method: 'PUT',
path: '/',
headers: {
'content-type': 'application/json'
}
},
handler
)
})
await t.completed
})