diff options
Diffstat (limited to 'test/retry-handler.js')
-rw-r--r-- | test/retry-handler.js | 622 |
1 files changed, 622 insertions, 0 deletions
diff --git a/test/retry-handler.js b/test/retry-handler.js new file mode 100644 index 0000000..a4577a6 --- /dev/null +++ b/test/retry-handler.js @@ -0,0 +1,622 @@ +'use strict' +const { createServer } = require('node:http') +const { once } = require('node:events') + +const tap = require('tap') + +const { RetryHandler, Client } = require('..') +const { RequestHandler } = require('../lib/api/api-request') + +tap.test('Should retry status code', t => { + let counter = 0 + const chunks = [] + const server = createServer() + const dispatchOptions = { + retryOptions: { + 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' + } + } + + t.plan(4) + + 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.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + t.equal(counter, 2) + }, + onError () { + t.fail() + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should use retry-after header for retries', t => { + let counter = 0 + const chunks = [] + const server = createServer() + let checkpoint + const dispatchOptions = { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(4) + + 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: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + }, + onError (err) { + t.error(err) + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should use retry-after header for retries (date)', t => { + let counter = 0 + const chunks = [] + const server = createServer() + let checkpoint + const dispatchOptions = { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(4) + + server.on('request', (req, res) => { + switch (counter) { + case 0: + res.writeHead(429, { + 'retry-after': new Date( + new Date().setSeconds(new Date().getSeconds() + 1) + ).toUTCString() + }) + res.end('rate limit') + checkpoint = Date.now() + counter++ + return + case 1: + res.writeHead(200) + res.end('hello world!') + t.ok(Date.now() - checkpoint >= 1) + 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: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + }, + onError (err) { + t.error(err) + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'PUT', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should retry with defaults', t => { + let counter = 0 + const chunks = [] + const server = createServer() + const dispatchOptions = { + 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() + } + }) + + t.plan(3) + + 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.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'hello world!') + }, + onError (err) { + t.error(err) + } + } + }) + + t.teardown(async () => { + await client.close() + server.close() + + await once(server, 'close') + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + }) +}) + +tap.test('Should handle 206 partial content', t => { + 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((req, res) => { + if (x === 0) { + t.pass() + res.setHeader('etag', 'asd') + res.write('abc') + setTimeout(() => { + res.destroy() + }, 1e2) + } else if (x === 1) { + t.same(req.headers.range, 'bytes=3-') + res.setHeader('content-range', 'bytes 3-6/6') + res.setHeader('etag', 'asd') + res.statusCode = 206 + res.end('def') + } + x++ + }) + + const dispatchOptions = { + retryOptions: { + 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' + } + } + + t.plan(8) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler(dispatchOptions, { + dispatch: (...args) => { + return client.dispatch(...args) + }, + handler: { + onRequestSent () { + t.pass() + }, + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.equal(status, 200) + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'abcdef') + t.equal(counter, 1) + }, + onError () { + t.fail() + } + } + }) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + + t.teardown(async () => { + await client.close() + + server.close() + await once(server, 'close') + }) + }) +}) + +tap.test('Should handle 206 partial content - bad-etag', t => { + const chunks = [] + + // Took from: https://github.com/nxtedition/nxt-lib/blob/4b001ebc2f22cf735a398f35ff800dd553fe5933/test/undici/retry.js#L47 + let x = 0 + const server = createServer((req, res) => { + if (x === 0) { + t.pass() + res.setHeader('etag', 'asd') + res.write('abc') + setTimeout(() => { + res.destroy() + }, 1e2) + } else if (x === 1) { + t.same(req.headers.range, 'bytes=3-') + res.setHeader('content-range', 'bytes 3-6/6') + res.setHeader('etag', 'erwsd') + res.statusCode = 206 + res.end('def') + } + x++ + }) + + const dispatchOptions = { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + } + + t.plan(6) + + server.listen(0, () => { + const client = new Client(`http://localhost:${server.address().port}`) + const handler = new RetryHandler( + dispatchOptions, + { + dispatch: (...args) => { + return client.dispatch(...args) + }, + handler: { + onConnect () { + t.pass() + }, + onBodySent () { + t.pass() + }, + onHeaders (status, _rawHeaders, resume, _statusMessage) { + t.pass() + return true + }, + onData (chunk) { + chunks.push(chunk) + return true + }, + onComplete () { + t.error('should not complete') + }, + onError (err) { + t.equal(Buffer.concat(chunks).toString('utf-8'), 'abc') + t.equal(err.code, 'UND_ERR_REQ_RETRY') + } + } + } + ) + + client.dispatch( + { + method: 'GET', + path: '/', + headers: { + 'content-type': 'application/json' + } + }, + handler + ) + + t.teardown(async () => { + await client.close() + + server.close() + await once(server, 'close') + }) + }) +}) + +tap.test('retrying a request with a body', t => { + let counter = 0 + const server = createServer() + const dispatchOptions = { + retryOptions: { + 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.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.error(err) + }) + }) + + t.teardown(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 + ) + }) +}) |