diff --git a/src/request/interfaces.d.ts b/src/request/interfaces.d.ts index c38fd22e..7c5e2ef3 100644 --- a/src/request/interfaces.d.ts +++ b/src/request/interfaces.d.ts @@ -1,3 +1,4 @@ +import { AbortSignal } from '@dojo/shim/AbortController'; import { IterableIterator } from '@dojo/shim/iterator'; import Task from '../async/Task'; import UrlSearchParams, { ParamList } from '../UrlSearchParams'; @@ -57,6 +58,10 @@ export interface RequestOptions { * Password for HTTP authentication */ password?: string; + /** + * Abort signal, allowing for cancelable requests + */ + signal?: AbortSignal; /** * Number of milliseconds before the request times out and is canceled */ diff --git a/src/request/providers/node.ts b/src/request/providers/node.ts index 94b127d9..01d78c92 100644 --- a/src/request/providers/node.ts +++ b/src/request/providers/node.ts @@ -654,6 +654,15 @@ export default function node(url: string, options: NodeRequestOptions = {}): Upl timeoutReject && timeoutReject(new TimeoutError('The request timed out')); }, options.timeout); } + + if (options.signal) { + options.signal.addEventListener('abort', () => { + const abortError = new Error('Aborted'); + abortError.name = 'AbortError'; + reject(abortError); + }); + } + }, () => { request.abort(); diff --git a/src/request/providers/xhr.ts b/src/request/providers/xhr.ts index 333bb767..34661fa2 100644 --- a/src/request/providers/xhr.ts +++ b/src/request/providers/xhr.ts @@ -205,6 +205,19 @@ export default function xhr(url: string, options: XhrRequestOptions = {}): Uploa const task = >new Task((resolve, reject) => { timeoutReject = reject; + if (options.signal) { + options.signal.addEventListener('abort', () => { + let abortError: Error; + try { + abortError = new DOMException('Aborted', 'AbortError'); + } catch { + abortError = new Error('Aborted'); + abortError.name = 'AbortError'; + } + reject(abortError); + }); + } + request.onreadystatechange = function() { if (isAborted) { return; diff --git a/tests/unit/request/node.ts b/tests/unit/request/node.ts index c7548bea..a4f79658 100644 --- a/tests/unit/request/node.ts +++ b/tests/unit/request/node.ts @@ -7,6 +7,7 @@ import * as zlib from 'zlib'; import { Response } from '../../../src/request/interfaces'; import { default as nodeRequest, NodeResponse } from '../../../src/request/providers/node'; import TimeoutError from '../../../src/request/TimeoutError'; +import AbortController from '@dojo/shim/AbortController'; const serverPort = 8124; const serverUrl = 'http://localhost:' + serverPort; @@ -463,6 +464,20 @@ registerSuite('request/node', { ); }, + signal(this: any) { + const dfd = this.async(); + const url = getRequestUrl('foo.json'); + const controller = new AbortController(); + const { signal } = controller; + const request = nodeRequest(url, { signal }); + request.catch( + dfd.callback((error: Error) => { + assert.strictEqual(error.name, 'AbortError'); + }) + ); + controller.abort(); + }, + 'user and password': { both(this: any): void { const user = 'user name'; diff --git a/tests/unit/request/xhr.ts b/tests/unit/request/xhr.ts index 1b575b0d..5f22c87f 100644 --- a/tests/unit/request/xhr.ts +++ b/tests/unit/request/xhr.ts @@ -1,6 +1,7 @@ const { registerSuite } = intern.getInterface('object'); const { assert } = intern.getPlugin('chai'); +import AbortController from '@dojo/shim/AbortController'; import xhrRequest, { XhrResponse } from '../../../src/request/providers/xhr'; import { Response } from '../../../src/request/interfaces'; import UrlSearchParams from '../../../src/UrlSearchParams'; @@ -94,6 +95,23 @@ registerSuite('request/providers/xhr', { ); }, + '"signal"'(this: any) { + if (!echoServerAvailable) { + this.skip('No echo server available'); + } + const dfd = this.async(); + const controller = new AbortController(); + const { signal } = controller; + const request = xhrRequest('/__echo/foo.json', { signal, timeout: 100 }); + request.catch( + dfd.callback((error: Error) => { + assert.strictEqual(error.name, 'AbortError'); + }) + ); + + controller.abort(); + }, + 'user and password'(this: any) { if (!echoServerAvailable) { this.skip('No echo server available');