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() {