Skip to content

Commit

Permalink
Reject request on XHR timeout (#2646)
Browse files Browse the repository at this point in the history
Co-authored-by: Zachary Hamm <hamm.zachary@gmail.com>
  • Loading branch information
kevinfiol and zacharyhamm authored Dec 23, 2020
1 parent 2b5c2f0 commit f5b41aa
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 23 deletions.
34 changes: 27 additions & 7 deletions request/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ module.exports = function($window, Promise, oncompletion) {
var assumeJSON = (args.serialize == null || args.serialize === JSON.serialize) && !(body instanceof $window.FormData)
var responseType = args.responseType || (typeof args.extract === "function" ? "" : "json")

var xhr = new $window.XMLHttpRequest(), aborted = false
var xhr = new $window.XMLHttpRequest(), aborted = false, isTimeout = false
var original = xhr, replacedAbort
var abort = xhr.abort

Expand Down Expand Up @@ -141,12 +141,25 @@ module.exports = function($window, Promise, oncompletion) {
}
if (success) resolve(response)
else {
try { message = ev.target.responseText }
catch (e) { message = response }
var error = new Error(message)
error.code = ev.target.status
error.response = response
reject(error)
var completeErrorResponse = function() {
try { message = ev.target.responseText }
catch (e) { message = response }
var error = new Error(message)
error.code = ev.target.status
error.response = response
reject(error)
}

if (xhr.status === 0) {
// Use setTimeout to push this code block onto the event queue
// This allows `xhr.ontimeout` to run in the case that there is a timeout
// Without this setTimeout, `xhr.ontimeout` doesn't have a chance to reject
// as `xhr.onreadystatechange` will run before it
setTimeout(function() {
if (isTimeout) return
completeErrorResponse()
})
} else completeErrorResponse()
}
}
catch (e) {
Expand All @@ -155,6 +168,13 @@ module.exports = function($window, Promise, oncompletion) {
}
}

xhr.ontimeout = function (ev) {
isTimeout = true
var error = new Error("Request timed out")
error.code = ev.target.status
reject(error)
}

if (typeof args.config === "function") {
xhr = args.config(xhr, args, url) || xhr

Expand Down
51 changes: 51 additions & 0 deletions request/tests/test-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,57 @@ o.spec("request", function() {
o(e instanceof Error).equals(true)
}).then(done)
})
o("rejects on request timeout", function(done) {
var timeout = 50
var timeToGetItem = timeout + 1

mock.$defineRoutes({
"GET /item": function() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({status: 200})
}, timeToGetItem)
})
}
})

request({
method: "GET", url: "/item",
timeout: timeout
}).catch(function(e) {
o(e instanceof Error).equals(true)
o(e.message).equals("Request timed out")
o(e.code).equals(0)
}).then(function() {
done()
})
})
o("does not reject when time to request resource does not exceed timeout", function(done) {
var timeout = 50
var timeToGetItem = timeout - 1
var isRequestRejected = false

mock.$defineRoutes({
"GET /item": function() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve({status: 200})
}, timeToGetItem)
})
}
})

request({
method: "GET", url: "/item",
timeout: timeout
}).catch(function(e) {
isRequestRejected = true
o(e.message).notEquals("Request timed out")
}).then(function() {
o(isRequestRejected).equals(false)
done()
})
})
o("does not reject on status error code when extract provided", function(done) {
mock.$defineRoutes({
"GET /item": function() {
Expand Down
58 changes: 42 additions & 16 deletions test-utils/xhrMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ module.exports = function() {
}
this.responseType = ""
this.response = null
this.timeout = 0
Object.defineProperty(this, "responseText", {get: function() {
if (this.responseType === "" || this.responseType === "text") {
return this.response
Expand All @@ -55,25 +56,50 @@ module.exports = function() {
}})
this.send = function(body) {
var self = this
if(!aborted) {
var handler = routes[args.method + " " + args.pathname] || serverErrorHandler.bind(null, args.pathname)
var data = handler({rawUrl: args.rawUrl, url: args.pathname, query: args.search || {}, body: body || null})
self.status = data.status
// Match spec
if (self.responseType === "json") {
try { self.response = JSON.parse(data.responseText) }
catch (e) { /* ignore */ }

var completeResponse = function (data) {
self._responseCompleted = true
if(!aborted) {
self.status = data.status
// Match spec
if (self.responseType === "json") {
try { self.response = JSON.parse(data.responseText) }
catch (e) { /* ignore */ }
} else {
self.response = data.responseText
}
} else {
self.response = data.responseText
self.status = 0
}
self.readyState = 4
if (args.async === true) {
callAsync(function() {
if (typeof self.onreadystatechange === "function") self.onreadystatechange({target: self})
})
}
} else {
self.status = 0
}
self.readyState = 4
if (args.async === true) {
callAsync(function() {
if (typeof self.onreadystatechange === "function") self.onreadystatechange({target: self})
})

var data
if (!aborted) {
var handler = routes[args.method + " " + args.pathname] || serverErrorHandler.bind(null, args.pathname)
data = handler({rawUrl: args.rawUrl, url: args.pathname, query: args.search || {}, body: body || null})
}

if (typeof self.timeout === "number" && self.timeout > 0) {
setTimeout(function () {
if (self._responseCompleted) {
return
}

self.status = 0;
if (typeof self.ontimeout === "function") self.ontimeout({target: self, type:"timeout"})
}, self.timeout)
}

if (data instanceof Promise) {
data.then(completeResponse)
} else {
completeResponse(data)
}
}
this.abort = function() {
Expand Down

0 comments on commit f5b41aa

Please sign in to comment.