diff --git a/index.js b/index.js index dc73ce7..0f96791 100644 --- a/index.js +++ b/index.js @@ -309,10 +309,7 @@ function prepareRequest (options) { } } }) - return { - options, - formData - } + return formData } /** @@ -323,11 +320,8 @@ function prepareRequest (options) { * @param {{options: object, formData: object}} * @returns {Promise} */ -function sendRequest (args) { - // { options, formData } - const options = args.options - const formData = args.formData - return request(options.endpoint, formData).then(() => options) +function sendRequest (options) { + return new Promise((resolve, reject) => request(options.endpoint, () => prepareRequest(options), () => resolve(options), reject)) } /** @@ -372,7 +366,6 @@ function upload (options, callback) { return options }) .then(transformOptions) - .then(prepareRequest) .then(sendRequest) .catch(err => { return cleanupTempFiles(options) diff --git a/lib/request.js b/lib/request.js index 5c33923..ead1db1 100644 --- a/lib/request.js +++ b/lib/request.js @@ -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) } diff --git a/package-lock.json b/package-lock.json index c5cf6d6..3655ab8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "version": "6.5.5", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", @@ -183,6 +184,7 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, "requires": { "safer-buffer": "~2.1.0" } @@ -724,6 +726,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, "requires": { "tweetnacl": "^0.14.3" } @@ -813,6 +816,11 @@ "node-int64": "^0.4.0" } }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -864,7 +872,8 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true }, "chalk": { "version": "1.1.3", @@ -965,6 +974,17 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "contains-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", @@ -1092,6 +1112,19 @@ "tough-cookie": "~2.3.0", "tunnel-agent": "~0.4.1", "uuid": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + } } }, "tough-cookie": { @@ -1168,6 +1201,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, "requires": { "assert-plus": "^1.0.0" }, @@ -1175,7 +1209,8 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true } } }, @@ -1300,6 +1335,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -1914,17 +1950,20 @@ "extsprintf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true }, "fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", @@ -2101,17 +2140,27 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true }, "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", + "combined-stream": "^1.0.6", "mime-types": "^2.1.12" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + } } }, "forwarded": { @@ -2174,6 +2223,7 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, "requires": { "assert-plus": "^1.0.0" }, @@ -2181,7 +2231,8 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true } } }, @@ -2258,12 +2309,14 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true }, "har-validator": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" @@ -2402,8 +2455,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -2746,7 +2798,8 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, "is-utf8": { "version": "0.2.1", @@ -2777,7 +2830,8 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true }, "istanbul-api": { "version": "1.1.11", @@ -3192,7 +3246,8 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true }, "jsdom": { "version": "9.12.0", @@ -3236,12 +3291,14 @@ "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "json-stable-stringify": { "version": "1.0.1", @@ -3261,7 +3318,8 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true }, "json5": { "version": "0.5.1", @@ -3285,6 +3343,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.0.2", @@ -3295,7 +3354,8 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true } } }, @@ -3586,14 +3646,12 @@ "mime-db": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", - "dev": true + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" }, "mime-types": { "version": "2.1.15", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", - "dev": true, "requires": { "mime-db": "~1.27.0" } @@ -3754,7 +3812,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -3776,7 +3833,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true } @@ -3937,7 +3994,8 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true }, "pify": { "version": "2.3.0", @@ -4106,17 +4164,20 @@ "psl": { "version": "1.1.29", "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "dev": true }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true }, "range-parser": { "version": "1.2.0", @@ -4221,6 +4282,16 @@ } } }, + "readable-stream": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", + "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -4282,6 +4353,7 @@ "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "dev": true, "requires": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -4308,22 +4380,26 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true }, "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true }, "combined-stream": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -4331,12 +4407,14 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -4347,6 +4425,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -4356,12 +4435,14 @@ "mime-db": { "version": "1.37.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "dev": true }, "mime-types": { "version": "2.1.21", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "dev": true, "requires": { "mime-db": "~1.37.0" } @@ -4369,12 +4450,14 @@ "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "dev": true, "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" @@ -4383,7 +4466,8 @@ "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true } } }, @@ -4473,7 +4557,8 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "sane": { "version": "1.6.0", @@ -4655,6 +4740,7 @@ "version": "1.15.2", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "dev": true, "requires": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -4670,7 +4756,8 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true } } }, @@ -4741,6 +4828,14 @@ "strip-ansi": "^3.0.0" } }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", @@ -4961,6 +5056,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, "requires": { "safe-buffer": "^5.0.1" } @@ -4968,7 +5064,8 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true }, "type-check": { "version": "0.3.2", @@ -5006,6 +5103,11 @@ } } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "uglify-js": { "version": "3.4.9", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", @@ -5049,6 +5151,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, "requires": { "punycode": "^2.1.0" }, @@ -5056,10 +5159,16 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true } } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5091,6 +5200,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true, "requires": { "extsprintf": "1.0.2" } @@ -5187,8 +5297,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", diff --git a/package.json b/package.json index 4f3a393..a1595cb 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,14 @@ "url": "https://github.com/bugsnag/bugsnag-sourcemaps.git" }, "dependencies": { + "concat-stream": "^2.0.0", + "form-data": "^2.3.3", "graceful-fs": "^4.1.11", "listr": "^0.12.0", "meow": "^3.7.0", + "once": "^1.4.0", "rc": "^1.2.8", - "read-pkg-up": "^2.0.0", - "request": "^2.88.0" + "read-pkg-up": "^2.0.0" }, "devDependencies": { "coveralls": "^2.13.1", diff --git a/test/index.test.js b/test/index.test.js index a7f509b..48a389c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -75,9 +75,9 @@ describe('validateOptions', () => { describe('prepareRequest', () => { test('removes options.overwrite when false', () => { - expect(prepareRequest({ overwrite: false }).formData).toEqual({}) + expect(prepareRequest({ overwrite: false })).toEqual({}) }) test('does not remove options.overwrite when true', () => { - expect(prepareRequest({ overwrite: true }).formData).toEqual({ overwrite: 'true' }) + expect(prepareRequest({ overwrite: true })).toEqual({ overwrite: 'true' }) }) }) diff --git a/test/integration.test.js b/test/integration.test.js index 831ffc2..60e6679 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,107 +1,185 @@ 'use strict' -/* global test, expect, beforeEach, afterEach */ +/* global test, expect, beforeEach, afterEach, fail */ process.env.BUGSNAG_RETRY_INTERVAL = 100 +process.env.BUGSNAG_TIMEOUT = 100 const express = require('express') const upload = require('../').upload +const net = require('net') -beforeEach(() => createTestServer()) -afterEach(() => closeTestServer()) +describe('HTTP level tests', () => { + beforeEach(() => createTestServer()) + afterEach(() => closeTestServer()) -test('it makes a post request to the provided endpoint', () => { - let n = 0 - app.post('/', (req, res) => { - n++ - res.end() - }) - return upload({ - apiKey: 'API_KEY', - endpoint: `http://localhost:${server.address().port}`, - sourceMap: `${__dirname}/fixtures/noop.min.js.map` - }).then(() => { - expect(n).toBe(1) + test('it makes a post request to the provided endpoint', () => { + let n = 0 + app.post('/', (req, res) => { + n++ + res.end() + }) + return upload({ + apiKey: 'API_KEY', + endpoint: `http://localhost:${server.address().port}`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + expect(n).toBe(1) + }) }) -}) -test('it retries upon 50x failure', () => { - let n = 0 - app.post('/', (req, res) => { - n++ - if (n < 5) return res.sendStatus(500) - return res.sendStatus(200) + test('it retries upon 50x failure', () => { + let n = 0 + app.post('/', (req, res) => { + n++ + if (n < 5) return res.sendStatus(500) + return res.sendStatus(200) + }) + return upload({ + apiKey: 'API_KEY', + endpoint: `http://localhost:${server.address().port}`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + expect(n).toBe(5) + }) }) - return upload({ - apiKey: 'API_KEY', - endpoint: `http://localhost:${server.address().port}`, - sourceMap: `${__dirname}/fixtures/noop.min.js.map` - }).then(() => { - expect(n).toBe(5) + + test('it retries upon socket hangup', () => { + let n = 0 + app.post('/', (req, res) => { + n++ + if (n < 5) return req.connection.destroy() + return res.sendStatus(200) + }) + return upload({ + apiKey: 'API_KEY', + endpoint: `http://localhost:${server.address().port}`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + expect(n).toBe(5) + }) }) -}) -test('it eventually gives up retrying', () => { - let n = 0 - app.post('/', (req, res) => { - n++ - return res.sendStatus(500) + test('it eventually gives up retrying', () => { + let n = 0 + app.post('/', (req, res) => { + n++ + return res.sendStatus(500) + }) + return upload({ + apiKey: 'API_KEY', + endpoint: `http://localhost:${server.address().port}`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + fail(new Error('expected promise to be rejected')) + }).catch(err => { + expect(n).toBe(5) + expect(err).toBeTruthy() + }) }) - return upload({ - apiKey: 'API_KEY', - endpoint: `http://localhost:${server.address().port}`, - sourceMap: `${__dirname}/fixtures/noop.min.js.map` - }).then(() => { - throw new Error('expected promise to be rejected') - }).catch(err => { - expect(n).toBe(5) - expect(err).toBeTruthy() + + test('it doesn’t retry on a 40x failure', () => { + let n = 0 + app.post('/', (req, res) => { + n++ + return res.sendStatus(400) + }) + return upload({ + apiKey: 'API_KEY', + endpoint: `http://localhost:${server.address().port}`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + fail(new Error('expected promise to be rejected')) + }).catch(err => { + expect(err).toBeTruthy() + expect(n).toBe(1) + }) }) }) -test('it doesn’t retry on a 40x failure', () => { - let n = 0 - app.post('/', (req, res) => { - n++ - return res.sendStatus(400) +describe('socket level tests', () => { + test('it retries upon timeout', (done) => { + let n = 0 + const socketServer = net.createServer(socket => { + n++ + // this socket server never says anything + }) + socketServer.listen(() => { + upload({ + apiKey: 'API_KEY', + endpoint: `http://localhost:${socketServer.address().port}`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + socketServer.close() + fail(new Error('expected promise to be rejected')) + }).catch(e => { + socketServer.close() + expect(n).toBe(5) + expect(e).toBeTruthy() + expect(e.message).toBe('Connection timed out') + done() + }) + }) }) - return upload({ - apiKey: 'API_KEY', - endpoint: `http://localhost:${server.address().port}`, - sourceMap: `${__dirname}/fixtures/noop.min.js.map` - }).then(() => { - throw new Error('expected promise to be rejected') - }).catch(err => { - expect(err).toBeTruthy() - expect(n).toBe(1) + + test('it works when the server stops timing out', (done) => { + let n = 0 + let port = null + const socketServer = net.createServer(socket => { + n++ + if (n < 3) return + socketServer.close() + createTestServer(port).then(() => { + app.post('/', (req, res) => { + n++ + return res.sendStatus(200) + }) + }) + }) + socketServer.listen(() => { + port = socketServer.address().port + upload({ + apiKey: 'API_KEY', + endpoint: `http://localhost:${port}`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + socketServer.close() + expect(n).toBe(4) + closeTestServer() + done() + }).catch(err => fail(err)) + }) }) -}) -test('it returns the correct error in a synchronous failure', () => { - return upload({ - apiKey: 'API_KEY', - // the easiest way to trigger a synchronous - // thrown error in request is a malformed url: - endpoint: `1231..;`, - sourceMap: `${__dirname}/fixtures/noop.min.js.map` - }).then(() => { - throw new Error('expected promise to be rejected') - }).catch(err => { - expect(err).toBeTruthy() - expect(err.message).toBe('Invalid URI "1231..;"') + test('it returns the correct error in a synchronous failure', () => { + return upload({ + apiKey: 'API_KEY', + // the easiest way to trigger a synchronous + // thrown error in request is a malformed url: + endpoint: `1231..;`, + sourceMap: `${__dirname}/fixtures/noop.min.js.map` + }).then(() => { + fail(new Error('expected promise to be rejected')) + }).catch(err => { + expect(err).toBeTruthy() + expect(err.code).toBe('ECONNREFUSED') + }) }) }) let server, app -const createTestServer = () => { +const createTestServer = (port) => { return new Promise((resolve, reject) => { const _app = express() - const _server = _app.listen((err) => { - if (err) return reject(err) - server = _server - app = _app - resolve() - }) + const listenArgs = [] + .concat(port || []) + .concat((err) => { + if (err) return reject(err) + server = _server + app = _app + resolve() + }) + const _server = _app.listen.apply(_app, listenArgs) }) }