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 7 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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ 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`.

---

### `reply.from(source, [opts])`
Expand Down
29 changes: 21 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ 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) {
if (retryMethods.has(req.method) && !contentLength) {
requestImpl = createRequestRetry(request, this, retriesCount, retryOnError)
} else {
requestImpl = request
Expand Down Expand Up @@ -215,23 +214,37 @@ function isFastifyMultipartRegistered (fastify) {

function createRequestRetry (requestImpl, reply, retriesCount, retryOnError) {
function requestRetry (req, cb) {
const MAX_RETRIES_ON_503 = 10
leorossi marked this conversation as resolved.
Show resolved Hide resolved
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 < MAX_RETRIES_ON_503) {
// 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')
}
})