From fc7a8ca102bd756747bf9727233337ae8bd5de2f Mon Sep 17 00:00:00 2001 From: Andrii Varlamov Date: Fri, 7 Nov 2025 16:24:55 +0100 Subject: [PATCH] - add abort signal support --- src/client-browser.js | 3 +- src/client-node.js | 9 +-- src/index.js | 4 +- src/request.js | 15 ++++- tests/specs/node-client.test.js | 104 ++++++++++++++++++++++++-------- 5 files changed, 101 insertions(+), 34 deletions(-) diff --git a/src/client-browser.js b/src/client-browser.js index 5bdb475..540040a 100644 --- a/src/client-browser.js +++ b/src/client-browser.js @@ -1,9 +1,10 @@ import { Request } from './request' -export function sendXmlHttpRequest(path, method, headers, body, encoding, timeout, withCredentials) { +export function sendXmlHttpRequest(path, method, headers, body, encoding, timeout, withCredentials, abortSignal) { return new Promise(function sendRequest(resolve, reject) { let request = new Request.XMLHttpRequest() + request.signal = abortSignal request.timeout = timeout if (!encoding) { diff --git a/src/client-node.js b/src/client-node.js index d199d7f..b902c72 100644 --- a/src/client-node.js +++ b/src/client-node.js @@ -1,15 +1,16 @@ import { isFormData, isStream, normalizeTrailingSlashInPath } from './utils' -export function sendNodeAPIRequest(path, method, headers, body, encoding, timeout, withCredentials) { +export function sendNodeAPIRequest(path, method, headers, body, encoding, timeout, withCredentials, abortSignal) { return new Promise((resolve, reject) => { const u = require('url').parse(path) const form = isFormData(body) && body const https = u.protocol === 'https:' const options = { - host: u.hostname, - port: u.port || (https ? 443 : 80), - path: normalizeTrailingSlashInPath(path, u) + (u.search || ''), + signal: abortSignal, + host : u.hostname, + port : u.port || (https ? 443 : 80), + path : normalizeTrailingSlashInPath(path, u) + (u.search || ''), method, headers, timeout, diff --git a/src/index.js b/src/index.js index 6f660e4..e68c35f 100644 --- a/src/index.js +++ b/src/index.js @@ -15,12 +15,12 @@ Object.defineProperty(Request, 'FormData', { Request.XMLHttpRequest = typeof XMLHttpRequest !== 'undefined' ? XMLHttpRequest : undefined -Request.send = (path, method, headers, body, encoding, timeout, withCredentials) => { +Request.send = (path, method, headers, body, encoding, timeout, withCredentials, abortSignal) => { const sender = typeof Request.XMLHttpRequest !== 'undefined' ? sendXmlHttpRequest : sendNodeAPIRequest - return sender(path, method, headers, body, encoding, timeout, withCredentials) + return sender(path, method, headers, body, encoding, timeout, withCredentials, abortSignal) } Request.verbose = false diff --git a/src/request.js b/src/request.js index a25bfa3..5ee2dac 100644 --- a/src/request.js +++ b/src/request.js @@ -27,6 +27,7 @@ export class Request extends EventEmitter { this.encoding = 'utf8' this.timeout = 0 this.withCredentials = null + this.abortSignal = null } /** @@ -185,6 +186,17 @@ export class Request extends EventEmitter { return this } + /** + * Sets abort signal + * @param abortSignal + * @returns {Request} + */ + setAbortSignal(abortSignal) { + this.abortSignal = abortSignal + + return this + } + /** * Sends the request * @@ -264,7 +276,8 @@ export class Request extends EventEmitter { body, this.encoding, this.timeout, - withCredentials + withCredentials, + this.abortSignal, ) .then(parseBody) .then(checkStatus) diff --git a/tests/specs/node-client.test.js b/tests/specs/node-client.test.js index 3ecc4a5..6a16b72 100644 --- a/tests/specs/node-client.test.js +++ b/tests/specs/node-client.test.js @@ -40,6 +40,7 @@ describe('Node Client', () => { 'port' : '9898', 'timeout' : 0, 'withCredentials': false, + 'signal' : null, }) }) @@ -57,7 +58,8 @@ describe('Node Client', () => { 'path' : 'foo.bar/path/to/api', 'port' : 80, 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -75,7 +77,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : 80, 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -93,7 +96,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : 443, 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -113,7 +117,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -131,7 +136,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -266,7 +272,8 @@ describe('Node Client', () => { 'path' : '/v1/documents/10psXGc-EW3vkeGXP0qG3v66Q-uo:batchUpdate', 'port' : 443, 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -301,7 +308,8 @@ describe('Node Client', () => { 'path' : '/path/to/api?str=str&num1=0&num2=123&bool1=true&bool2=false', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -320,7 +328,8 @@ describe('Node Client', () => { 'path' : '/path/to/api?numArr=1&numArr=2&numArr=3&strArr=a&strArr=b&strArr=c', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -339,7 +348,8 @@ describe('Node Client', () => { 'path' : '/path/to/api?str=%D0%B0%D0%B1%D0%B2&space=%20&percent=%25&at=%40&strArr=%D0%B0%D0%B1%D0%B2&strArr=%20&strArr=%25&strArr=%40', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) }) @@ -361,7 +371,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) }) @@ -382,7 +393,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -403,7 +415,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) expect(transaction2.requestBody).toEqual('[1,2,3]') @@ -416,7 +429,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -436,7 +450,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -488,7 +503,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -511,7 +527,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -540,7 +557,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) }) @@ -559,7 +577,33 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 1200, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, + }) + }) + }) + + describe('Request Abort Signal', () => { + it('runs a request with abort signal', async () => { + const transaction = registerNodeTransaction() + + const abortController = new AbortController() + + await Request.get('http://foo.bar:9898/path/to/api') + .setAbortSignal(abortController.signal) + + const { signal, ...restOptions } = transaction.options + + expect(signal).toBe(abortController.signal) + + expect(restOptions).toEqual({ + 'headers' : {}, + 'host' : 'foo.bar', + 'method' : 'GET', + 'path' : '/path/to/api', + 'port' : '9898', + 'timeout' : 0, + 'withCredentials': false, }) }) }) @@ -577,7 +621,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -596,7 +641,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -615,7 +661,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -634,7 +681,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) }) @@ -758,7 +806,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -781,7 +830,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -805,7 +855,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) }) @@ -838,7 +889,8 @@ describe('Node Client', () => { 'path' : '/path/to/api', 'port' : '9898', 'timeout' : 0, - 'withCredentials': false + 'withCredentials': false, + 'signal' : null, }) })