From c243438d885a4ef78262f68ddc90d3bebbaeb9d0 Mon Sep 17 00:00:00 2001 From: Christian Ortner Date: Mon, 21 Mar 2016 15:15:49 +0100 Subject: [PATCH] Introduce an optional configurable request timeout By default, XMLHttpRequests do not time out. A developer might want to react to errors by redirecting to an error page, which was only possible by listening for ravenSuccess and ravenFailure events. In case of network issues or Sentry being overloaded, error reporting requests may take a very long time. The newly introduced ravenTimeout event is triggered once a user-defined timeout has aborted the request. --- docs/config.rst | 6 ++++++ src/raven.js | 14 +++++++++++--- test/raven.test.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 7eac8c79b09b..4ccbb781d7b0 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -215,6 +215,12 @@ Those configuration options are documented below: onFailure Callback to be invoked upon a failed request. +.. describe:: timeout + + By default, Raven.js requests do not time out. By setting this options, you can enforce that the request + is aborted after a number of milliseconds, e.g. set ``timeout: 500`` to abort after half a second during + configuration. Once the timeout occurs, the ``ravenTimeout`` event is triggered. + .. describe:: allowSecretKey By default, Raven.js will throw an error if configured with a Sentry DSN that contains a secret key. diff --git a/src/raven.js b/src/raven.js index 2bbd1fde08f9..159d6ea40190 100644 --- a/src/raven.js +++ b/src/raven.js @@ -1039,9 +1039,10 @@ Raven.prototype = { }, _makeXhrRequest: function(opts) { - var request; + var request, + self = this, + url = opts.url; - var url = opts.url; function handler() { if (request.status === 200) { if (opts.onSuccess) { @@ -1070,6 +1071,13 @@ Raven.prototype = { request.onload = handler; } + // Allow limiting request duration to ensure that logging something will always reach a finished state. By + // default, the timeout is 0, which is unlimited. + request.timeout = opts.options.timeout || 0; + request.ontimeout = function () { + self._triggerEvent('timeout'); + }; + // NOTE: auth is intentionally sent as part of query string (NOT as custom // HTTP header) so as to avoid preflight CORS requests request.open('POST', url + '?' + urlencode(opts.auth)); @@ -1081,7 +1089,7 @@ Raven.prototype = { 'withCredentials' in new XMLHttpRequest() || typeof XDomainRequest !== 'undefined'; - return (hasCORS ? this._makeXhrRequest : this._makeImageRequest)(opts); + return (hasCORS ? this._makeXhrRequest : this._makeImageRequest).call(this, opts); }, // Note: this is shitty, but I can't figure out how to get diff --git a/test/raven.test.js b/test/raven.test.js index 57aa7ece29d6..920c6664af76 100644 --- a/test/raven.test.js +++ b/test/raven.test.js @@ -1249,6 +1249,50 @@ describe('globals', function() { assert.equal(lastXhr.requestBody, '{"foo":"bar"}'); assert.equal(lastXhr.url, 'http://localhost/?a=1&b=2'); }); + + it('should set the request timeout if configured', function() { + var optionsWithTimeout = JSON.parse(JSON.stringify(Raven._globalOptions)), + expectedTimeout = 500; + + optionsWithTimeout.timeout = expectedTimeout; + Raven._makeXhrRequest({ + url: 'http://localhost/', + auth: {a: '1', b: '2'}, + data: {foo: 'bar'}, + options: optionsWithTimeout + }); + + var lastXhr = this.requests[this.requests.length - 1]; + assert.equal(lastXhr.timeout, expectedTimeout); + }); + + it('should trigger a timeout event once timeout occurs', function () { + Raven._makeXhrRequest({ + url: 'http://localhost/', + auth: {a: '1', b: '2'}, + data: {foo: 'bar'}, + options: Raven._globalOptions + }); + this.sinon.stub(Raven, '_triggerEvent'); + + var lastXhr = this.requests[this.requests.length - 1]; + assert.typeOf(lastXhr.ontimeout, 'function'); + + lastXhr.ontimeout(); + assert.isTrue(Raven._triggerEvent.calledWith('timeout')); + }); + + it('should use an infinite request timeout by default', function() { + Raven._makeXhrRequest({ + url: 'http://localhost/', + auth: {a: '1', b: '2'}, + data: {foo: 'bar'}, + options: Raven._globalOptions + }); + + var lastXhr = this.requests[this.requests.length - 1]; + assert.equal(lastXhr.timeout, 0); + }); }); describe('makeImageRequest', function() {