This repository has been archived by the owner on Jan 12, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #39 from bugsnag/bengourley/retry
feat: Retry requests in more situations
- Loading branch information
Showing
6 changed files
with
383 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,75 @@ | ||
'use strict' | ||
|
||
const request = require('request') | ||
const https = require('https') | ||
const http = require('http') | ||
const concat = require('concat-stream') | ||
const url = require('url') | ||
const once = require('once') | ||
const FormData = require('form-data') | ||
|
||
const MAX_ATTEMPTS = 5 | ||
const RETRY_INTERVAL = process.env.BUGSNAG_RETRY_INTERVAL || 1000 | ||
const RETRY_INTERVAL = parseInt(process.env.BUGSNAG_RETRY_INTERVAL) || 1000 | ||
const TIMEOUT = parseInt(process.env.BUGSNAG_TIMEOUT) || 30000 | ||
|
||
module.exports = (url, data) => { | ||
return new Promise((resolve, reject) => { | ||
let attempts = 0 | ||
const maybeRetry = (err) => { | ||
attempts++ | ||
if (err && err.isRetryable && attempts < MAX_ATTEMPTS) return setTimeout(go, RETRY_INTERVAL) | ||
return reject(err) | ||
} | ||
const go = () => send(url, data, resolve, maybeRetry) | ||
go() | ||
}) | ||
module.exports = (endpoint, makePayload, onSuccess, onError) => { | ||
let attempts = 0 | ||
const maybeRetry = (err) => { | ||
attempts++ | ||
if (err && err.isRetryable !== false && attempts < MAX_ATTEMPTS) return setTimeout(go, RETRY_INTERVAL) | ||
return onError(err) | ||
} | ||
const go = () => send(endpoint, makePayload(), onSuccess, maybeRetry) | ||
go() | ||
} | ||
|
||
const send = (url, formData, onSuccess, onError) => { | ||
request.post({ url, formData }, (err, res, body) => { | ||
if (err || res.statusCode !== 200) { | ||
err = err || new Error(`${res.statusMessage} (${res.statusCode}) - ${body}`) | ||
if (res && (res.statusCode < 400 || res.statusCode >= 500)) { | ||
err.isRetryable = true | ||
const send = (endpoint, data, onSuccess, onError) => { | ||
onError = once(onError) | ||
const formData = new FormData() | ||
Object.keys(data).forEach(k => formData.append(k, data[k])) | ||
const parsedUrl = url.parse(endpoint) | ||
const req = (parsedUrl.protocol === 'https:' ? https : http).request({ | ||
method: 'POST', | ||
hostname: parsedUrl.hostname, | ||
path: parsedUrl.path || '/', | ||
headers: formData.getHeaders(), | ||
port: parsedUrl.port || undefined | ||
}, res => { | ||
res.pipe(concat(body => { | ||
if (res.statusCode === 200) return onSuccess() | ||
if (res.statusCode !== 400) { | ||
const err = new Error(`HTTP status ${res.statusCode} received from upload API`) | ||
if (!isRetryable(res.statusCode)) { | ||
err.isRetryable = false | ||
} | ||
return onError(err) | ||
} | ||
try { | ||
const err = new Error('Invalid payload sent to upload API') | ||
err.errors = JSON.parse(body.toString()).errors | ||
// never retry a 400 | ||
err.isRetryable = false | ||
return onError(err) | ||
} catch (_) { | ||
const e = new Error(`HTTP status ${res.statusCode} received from upload API`) | ||
e.isRetryable = false | ||
return onError(e) | ||
} | ||
onError(err) | ||
} else { | ||
onSuccess() | ||
} | ||
})) | ||
}) | ||
formData.pipe(req) | ||
req.on('error', onError) | ||
req.setTimeout(TIMEOUT, () => { | ||
onError(new Error('Connection timed out')) | ||
req.abort() | ||
}) | ||
} | ||
|
||
const isRetryable = status => { | ||
return ( | ||
status < 400 || | ||
status > 499 || | ||
[ | ||
408, // timeout | ||
429 // too many requests | ||
].indexOf(status) !== -1) | ||
} |
Oops, something went wrong.