Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

fix($http): correct xhrStatus of different timeout values #16262

Merged
merged 2 commits into from
Mar 7, 2018

Conversation

Narretz
Copy link
Contributor

@Narretz Narretz commented Oct 9, 2017

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
bugfix

What is the current behavior? (You can also link to an open issue here)
Incorrect xhrStatus

What is the new behavior (if this is a feature change)?
Correct xhrStatus

Does this PR introduce a breaking change?
No

Please check if the PR fulfills these requirements

Other information:

@gkalpak @frederikprijck this is something I'd like your review / opinion on

While trying to find out if #14021 was (partially) fixed by 5e2bc5b, I noticed there's a problem with the http tests and the actual behavior:

In

expect(response.xhrStatus).toBe('timeout');

we expect that the xhrStatus is 'timeout', however it actually is 'abort' in real $http usage. This is not caught by our tests because the tests run against the mocked $httpBackend which must manually be kept in sync with the real $http implementation.

What's more, our implementation of abort / timeout doesn't follow the way normal xhr abort / timeout works:

If you use xhr.abort() then the onabort handler is called.
If you set xhr.timeout and the timeout is exceeded then the ontimeout handler is callded.

(In the browser, both events are marked as "canceled" in the devtools, at least in Chrome).

In $http, in both cases you would get xhrStatus 'abort', because we abort the xhrRequest, but
don't distinguish between these cases.

In the current implementation, the "timeout" handler would only be called in the case of a real
network timeout (presumably, because I couldn't find a way to test this).

What's more, if you pass a $timeout as the timeout parameter, it will currently set xhrStatus "abort" if the time runs out (promise resolved). This case can only be fixed by reading the internal $$timeoutId property on the promise returned by $timeout.

This PR fixes the code in both the mock httpBackend and the regular http to follow the same logic as xhr does:

xhr http option xhr result current http xhrStatus new http xhrStatus
xhr.abort() resolving promise onabort callback is called abort abort
xhr.timeout numerical value ontimeout callback abort timeout
---- $timeout ----------- abort timeout

Here's a plunker that shows the difference, with a patched angular.js file (to swit

http://plnkr.co/edit/nsviFV7YyF4TiXCyISMN?p=preview

@petebacondarwin
Copy link
Contributor

Not sure why a promise passed as timeout parameter would lead to an abort result rather than a timeout result?

@Narretz
Copy link
Contributor Author

Narretz commented Oct 9, 2017

When you (manually) resolve the promise, the request is aborted. This is our equivalent to calling xhr.abort(). If we don't do this, then there's no way to distinguish a manual abort from a timeout (which was the point of 5e2bc5b afaik)

The API is not that great in that respect.

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what the xhrStatus should be in case of a timeout config. The name suggests it is a timeout, but I guess many people use it as a way to abort a request (e.g. as we use it in $resource) in case a new request (with updated params) needs to be sent. In such cases, it is not that the request timed out or took too long or something; it's just that we don't care about the response any more.

One idea might be to report xhrStatus as timeout for numeric timeouts and as abort for deferreds.

💯 for keeping the mocked service behavior consistent with the actual one.

(The commit message could be more descriptive btw 😛)

// The timeout behavior follows standard XMLHttpRequest logic.
// xhr.timeout = ontimeout (numerical timeout or $timeout)
// xhr.abort() = onabort (resolving a promise)
xhr.onabort = timeout > 0 || (timeout && timeout.$$timeoutId >= 0) ? requestTimeout : requestAborted;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, this will call the wrong method if the request is aborted while there is a timeout set (even if the "abortion" was not related to the timeout), right?

I think we should set a flag in requestTimeout() and adjust the xhrStatus argument in requestAborted() based on the value of that flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understand you correctly, but as it goes, at the moment you can only have a numerical timeout and the ability to cancel the request before that if you pass a promise into timeout and wire up your own timeout behind that promise (Because you cannot short-circuit a $timeout). And in that case, you cannot get the correct xhrStatus, because the xhrStatus is based on the value that is passed into the timeout, that is correct.
Based on the resolve value of a promise-like, we could set the correct xhrStatus, that is right.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that if something other than the timeout (numeric or promise) causes the request to be aborted (e.g. someone calls xhr.abort() manually or the browser aborts it - not sure if browsers do that), then your current implementation will call requestTimeout(), even if the abort event was not related to a timeout.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see.
As we don't expose the xhr object (or do we via $xhrFactory?), I don't think it can happen that a user aborts the request.
I think when the browser unloads a page (because of navigation), then onabort gets called, but in most cases it's too late for the code to react.

But I guess it's safer if the default is abort for onabort.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, with $xhrFactory() the user can get access to the xhr object 😁

Copy link
Member

@gkalpak gkalpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of minor comments.
LGTM otherwise 👍

@@ -1378,9 +1378,13 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
function wrapResponse(wrapped) {
if (!$browser && timeout) {
if (timeout.then) {
timeout.then(handleTimeout);
timeout.then(function() {
handlePrematureEnd(timeout.$$timeoutId >= 0 ? 'timeout' : 'abort');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not keep in-line with the actual implementation and use isDefined(timeout.$$timeoutId)? Does it make a difference?

var onRejected = jasmine.createSpy('onRejected');
var onRejected = jasmine.createSpy('onRejected').and.callFake(function(response) {
expect(response.xhrStatus).toBe('timeout');
callback();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need this. You can just assert that onRejected was called.

var onFulfilled = jasmine.createSpy('onFulfilled');
var onRejected = jasmine.createSpy('onRejected').and.callFake(function(response) {
expect(response.xhrStatus).toBe('timeout');
callback();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need this. You can just assert that onRejected was called.

@Narretz
Copy link
Contributor Author

Narretz commented Mar 6, 2018

thanks @gkalpak , I've incorporated your suggestions, and also updated the docs a bit - technically unrelated to the changes, but actually just a bit of re-ordering of info.

Narretz added 2 commits March 6, 2018 19:10
This correctly sets "timeout" if a request fails because the timeout
(numerical or $timeout) is exceeded,
and "abort" if the request is aborted by resolving a promise that was passed in.
@Narretz Narretz merged commit 64c23e4 into angular:master Mar 7, 2018
@Narretz Narretz deleted the fix-http-status branch March 7, 2018 09:40
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants