Skip to content

Commit e62c713

Browse files
authored
Fix an assertion if a request with timeout receives 2 callbacks from the Browser/OS. (#37)
This resulted in _respond being called twice, which (if the correct retry policy was applied) would result in the request being enqueued twice & this two timeout timers were setup.
1 parent 3a80fcd commit e62c713

3 files changed

Lines changed: 139 additions & 9 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "simplerestclients",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "A library of components for accessing RESTful services with javascript/typescript.",
55
"author": "David de Regt <David.de.Regt@microsoft.com>",
66
"scripts": {

src/SimpleWebRequest.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ const enum FeatureSupportStatus {
173173
// List of pending requests, sorted from most important to least important (numerically descending)
174174
let requestQueue: SimpleWebRequestBase[] = [];
175175

176+
// List of requests blocked on _blockUNtil promises
177+
let blockedList: SimpleWebRequestBase[] = [];
178+
176179
// List of executing (non-finished) requests -- only to keep track of number of requests to compare to the max
177180
let executingList: SimpleWebRequestBase[] = [];
178181

@@ -215,8 +218,11 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
215218
protected static checkQueueProcessing() {
216219
while (requestQueue.length > 0 && executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests) {
217220
const req = requestQueue.shift()!!!;
221+
blockedList.push(req);
218222
const blockPromise = (req._blockRequestUntil && req._blockRequestUntil()) || SyncTasks.Resolved();
219-
blockPromise.then(() => {
223+
blockPromise.finally(() => {
224+
_.remove(blockedList, req);
225+
}).then(() => {
220226
if (executingList.length < SimpleWebRequestOptions.MaxSimultaneousRequests) {
221227
executingList.push(req);
222228
req._fire();
@@ -231,12 +237,10 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
231237
}
232238

233239
protected _removeFromQueue(): void {
234-
// Pull it out of whichever queue it's sitting in
235-
if (this._xhr) {
236-
_.pull(executingList, this);
237-
} else {
238-
_.pull(requestQueue, this);
239-
}
240+
// Only pull from request queue and executing queue here - pulling from the blocked queue can result in requests
241+
// being queued up multiple times if _respond fires more than once (it shouldn't, but does happen in the wild)
242+
_.remove(executingList, this);
243+
_.remove(requestQueue, this);
240244
}
241245

242246
protected _assertAndClean(expression: any, message: string): void {
@@ -526,6 +530,11 @@ export abstract class SimpleWebRequestBase<TOptions extends WebRequestOptions =
526530
return;
527531
}
528532

533+
// Check if the current queues, if the request is already in there, nothing to enqueue
534+
if (_.includes(executingList, this) || _.includes(blockedList, this) || _.includes(requestQueue, this)) {
535+
return;
536+
}
537+
529538
// Throw it on the queue
530539
const index = _.findIndex(requestQueue, request =>
531540
// find a request with the same priority, but newer

test/GenericRestClient.spec.ts

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as faker from 'faker';
2+
import { ErrorHandlingType, SimpleWebRequestBase, WebErrorResponse } from '../src/SimpleWebRequest';
23
import { GenericRestClient, ApiCallOptions } from '../src/GenericRestClient';
34
import { DETAILED_RESPONSE, REQUEST_OPTIONS } from './helpers';
45
import * as SyncTasks from 'synctasks';
@@ -8,6 +9,22 @@ const BASE_URL = faker.internet.url();
89
const http = new RestClient(BASE_URL);
910

1011
describe('GenericRestClient', () => {
12+
beforeAll(() => {
13+
jasmine.Ajax.install();
14+
// Run an initial request to finish feature detection - this is needed so we can directly call onLoad
15+
const statusCode = 200;
16+
const onSuccess = jasmine.createSpy('onSuccess');
17+
const path = '/auth';
18+
19+
http.performApiGet(path)
20+
.then(onSuccess);
21+
22+
const request = jasmine.Ajax.requests.mostRecent();
23+
request.respondWith({ status: statusCode });
24+
expect(onSuccess).toHaveBeenCalled();
25+
jasmine.Ajax.uninstall();
26+
});
27+
1128
beforeEach(() => jasmine.Ajax.install());
1229
afterEach(() => jasmine.Ajax.uninstall());
1330

@@ -372,7 +389,7 @@ describe('GenericRestClient', () => {
372389
expect(onSuccess).toHaveBeenCalledWith(body.map((str: string) => str.trim()));
373390
});
374391

375-
it('blocks the request with custom method', () => {
392+
it('blocks the request with custom method', () => {
376393
const blockDefer = SyncTasks.Defer<void>();
377394

378395
class Http extends GenericRestClient {
@@ -398,4 +415,108 @@ describe('GenericRestClient', () => {
398415
request.respondWith({ status: statusCode });
399416
expect(onSuccess).toHaveBeenCalled();
400417
});
418+
419+
it('aborting request after failure w/retry', () => {
420+
let blockDefer = SyncTasks.Defer<void>();
421+
422+
class Http extends GenericRestClient {
423+
constructor(endpointUrl: string) {
424+
super(endpointUrl);
425+
this._defaultOptions.customErrorHandler = this._customErrorHandler;
426+
this._defaultOptions.timeout = 1;
427+
}
428+
protected _blockRequestUntil() {
429+
return blockDefer.promise();
430+
}
431+
432+
protected _customErrorHandler = (webRequest: SimpleWebRequestBase, errorResponse: WebErrorResponse) => {
433+
if (errorResponse.canceled) {
434+
return ErrorHandlingType.DoNotRetry;
435+
}
436+
return ErrorHandlingType.RetryUncountedImmediately;
437+
}
438+
}
439+
440+
const statusCode = 400;
441+
const onSuccess = jasmine.createSpy('onSuccess');
442+
const onFailure = jasmine.createSpy('onFailure');
443+
const http = new Http(BASE_URL);
444+
const path = '/auth';
445+
446+
const req = http.performApiGet(path)
447+
.then(onSuccess)
448+
.catch(onFailure);
449+
450+
blockDefer.resolve(void 0);
451+
const request1 = jasmine.Ajax.requests.mostRecent();
452+
453+
// Reset blockuntil so retries may block
454+
blockDefer = SyncTasks.Defer<void>();
455+
456+
request1.respondWith({ status: statusCode });
457+
expect(onSuccess).not.toHaveBeenCalled();
458+
expect(onFailure).not.toHaveBeenCalled();
459+
460+
// Calls abort function
461+
req.cancel();
462+
463+
expect(onSuccess).not.toHaveBeenCalled();
464+
expect(onFailure).toHaveBeenCalled();
465+
});
466+
467+
describe('Timing related tests' , () => {
468+
beforeEach(() => {
469+
jasmine.clock().install();
470+
});
471+
472+
afterEach(() => {
473+
jasmine.clock().uninstall();
474+
});
475+
476+
it('failed request with retry handles multiple _respond calls', () => {
477+
let blockDefer = SyncTasks.Defer<void>();
478+
479+
class Http extends GenericRestClient {
480+
constructor(endpointUrl: string) {
481+
super(endpointUrl);
482+
this._defaultOptions.customErrorHandler = this._customErrorHandler;
483+
this._defaultOptions.timeout = 1;
484+
}
485+
protected _blockRequestUntil() {
486+
return blockDefer.promise();
487+
}
488+
489+
protected _customErrorHandler = () => {
490+
return ErrorHandlingType.RetryUncountedImmediately;
491+
}
492+
}
493+
494+
const statusCode = 400;
495+
const onSuccess = jasmine.createSpy('onSuccess');
496+
const http = new Http(BASE_URL);
497+
const path = '/auth';
498+
499+
http.performApiGet(path)
500+
.then(onSuccess);
501+
502+
blockDefer.resolve(void 0);
503+
const request1 = jasmine.Ajax.requests.mostRecent();
504+
505+
// Reset blockuntil so retries may block
506+
blockDefer = SyncTasks.Defer<void>();
507+
508+
// Store this so we're able to emulate double-request callbacks
509+
const onloadToCall = request1.onload as any;
510+
request1.respondWith({ status: statusCode });
511+
onloadToCall(undefined);
512+
expect(onSuccess).not.toHaveBeenCalled();
513+
blockDefer.resolve(void 0);
514+
515+
jasmine.clock().tick(100);
516+
517+
const request2 = jasmine.Ajax.requests.mostRecent();
518+
request2.respondWith({ status: 200 });
519+
expect(onSuccess).toHaveBeenCalled();
520+
});
521+
});
401522
});

0 commit comments

Comments
 (0)