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

Feature/retry on 503 #200

Merged
merged 8 commits into from
Sep 30, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@ On which methods should the connection be retried in case of socket hang up.

By default: `['GET', 'HEAD', 'OPTIONS', 'TRACE' ]`

This plugin will always retry on 503 errors, _unless_ `retryMethods` does not contain `GET`.

---

#### `maxRetriesOn503`

This plugin will always retry on `GET` requests that returns 503 errors, _unless_ `retryMethods` does not contain `GET`.

This option set the limit on how many times the plugin should retry the request, specifically for 503 errors.

By Default: 10


---

### `reply.from(source, [opts])`
Expand Down
33 changes: 23 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ module.exports = fp(function from (fastify, opts, next) {
const getUpstream = opts.getUpstream || upstreamNoOp
const onError = opts.onError || onErrorDefault
const retriesCount = opts.retriesCount || 0
const maxRetriesOn503 = opts.maxRetriesOn503 || 10

if (!source) {
source = req.url
Expand Down Expand Up @@ -118,9 +119,8 @@ module.exports = fp(function from (fastify, opts, next) {
const requestHeaders = rewriteRequestHeaders(req, headers)
const contentLength = requestHeaders['content-length']
let requestImpl

if (retriesCount && retryMethods.has(req.method) && !contentLength) {
requestImpl = createRequestRetry(request, this, retriesCount, retryOnError)
if (retryMethods.has(req.method) && !contentLength) {
requestImpl = createRequestRetry(request, this, retriesCount, retryOnError, maxRetriesOn503)
} else {
requestImpl = request
}
Expand Down Expand Up @@ -213,25 +213,38 @@ function isFastifyMultipartRegistered (fastify) {
return fastify.hasContentTypeParser('multipart') && fastify.hasRequestDecorator('multipart')
}

function createRequestRetry (requestImpl, reply, retriesCount, retryOnError) {
function createRequestRetry (requestImpl, reply, retriesCount, retryOnError, maxRetriesOn503) {
function requestRetry (req, cb) {
let retries = 0

function run () {
requestImpl(req, function (err, res) {
if (err && !reply.sent && retriesCount > retries) {
if (err.code === retryOnError) {
retries += 1
// Magic number, so why not 42? We might want to make this configurable.
let retryAfter = 42 * Math.random() * (retries + 1)

run()
return
if (res && res.headers['retry-after']) {
retryAfter = res.headers['retry-after']
}
if (!reply.sent) {
// always retry on 503 errors
if (res && res.statusCode === 503 && req.method === 'GET') {
if (retriesCount === 0 && retries < maxRetriesOn503) {
// we should stop at some point
return retry(retryAfter)
}
} else if (retriesCount > retries && err && err.code === retryOnError) {
return retry(retryAfter)
}
}

cb(err, res)
})
}

function retry (after) {
retries += 1
setTimeout(run, after)
}

run()
}

Expand Down
101 changes: 101 additions & 0 deletions test/retry-on-503.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict'

const { test } = require('tap')
const Fastify = require('fastify')
const From = require('..')
const http = require('http')
const got = require('got')

function createTargetServer (withRetryAfterHeader, stopAfter = 1) {
let requestCount = 0
return http.createServer((req, res) => {
if (requestCount++ < stopAfter) {
res.statusCode = 503
res.setHeader('Content-Type', 'text/plain')
if (withRetryAfterHeader) {
res.setHeader('Retry-After', 100)
}
return res.end('This Service is Unavailable')
}
res.statusCode = 205
res.setHeader('Content-Type', 'text/plain')
return res.end(`Hello World ${requestCount}!`)
})
}

test('Should retry on 503 HTTP error', async function (t) {
t.plan(3)
const target = createTargetServer()
await target.listen(0)
t.teardown(target.close.bind(target))

const instance = Fastify()

instance.register(From, {
base: `http://localhost:${target.address().port}`
})

instance.get('/', (request, reply) => {
reply.from()
})

t.teardown(instance.close.bind(instance))
await instance.listen(0)

const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 0 })
t.equal(res.headers['content-type'], 'text/plain')
t.equal(res.statusCode, 205)
t.equal(res.body.toString(), 'Hello World 2!')
})

test('Should retry on 503 HTTP error with Retry-After response header', async function (t) {
t.plan(3)
const target = createTargetServer(true)
await target.listen(0)
t.teardown(target.close.bind(target))

const instance = Fastify()

instance.register(From, {
base: `http://localhost:${target.address().port}`
})

instance.get('/', (request, reply) => {
reply.from()
})

t.teardown(instance.close.bind(instance))
await instance.listen(0)

const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 0 })
t.equal(res.headers['content-type'], 'text/plain')
t.equal(res.statusCode, 205)
t.equal(res.body.toString(), 'Hello World 2!')
})

test('Should abort if server is always returning 503', async function (t) {
t.plan(2)
const target = createTargetServer(true, Number.MAX_SAFE_INTEGER)
await target.listen(0)
t.teardown(target.close.bind(target))

const instance = Fastify()

instance.register(From, {
base: `http://localhost:${target.address().port}`
})

instance.get('/', (request, reply) => {
reply.from()
})

t.teardown(instance.close.bind(instance))
await instance.listen(0)
try {
await got.get(`http://localhost:${instance.server.address().port}`, { retry: 0 })
t.fail()
} catch (err) {
t.equal(err.response.statusCode, 503)
t.equal(err.response.body.toString(), 'This Service is Unavailable')
}
})