From 2247d736d4278536e3a4381bad2bfca6b102760f Mon Sep 17 00:00:00 2001 From: Tarrowren Date: Wed, 2 Nov 2022 11:16:35 +0800 Subject: [PATCH 1/5] browser suppert `CancellationToken` --- api.d.ts | 3 +- package-lock.json | 6 + package.json | 1 + src/browser/main.ts | 87 +++++---- src/node/main.ts | 430 ++++++++++++++++++++++---------------------- 5 files changed, 273 insertions(+), 254 deletions(-) diff --git a/api.d.ts b/api.d.ts index b680ffc..8076797 100644 --- a/api.d.ts +++ b/api.d.ts @@ -12,6 +12,7 @@ export interface XHROptions { data?: string; strictSSL?: boolean; followRedirects?: number; + token?: import("vscode-jsonrpc").CancellationToken; agent?: HttpProxyAgent | HttpsProxyAgent; } @@ -39,4 +40,4 @@ export type Headers = { [header: string]: string | string[] | undefined }; export declare const configure: XHRConfigure; export declare const xhr: XHRRequest; -export declare function getErrorStatusDescription(status: number): string; \ No newline at end of file +export declare function getErrorStatusDescription(status: number): string; diff --git a/package-lock.json b/package-lock.json index 26c124a..0e2be4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2330,6 +2330,12 @@ "punycode": "^2.1.0" } }, + "vscode-jsonrpc": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.0.tgz", + "integrity": "sha512-QeAniC/xTWauVQgyNgEqNJ0Qm/Jw8QySGRQhRFPwb8c4FPp9k6QNgJp0ayXWws5qhdaHkiXkGPlzjOPZFQQKLw==", + "dev": true + }, "vscode-nls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.0.0.tgz", diff --git a/package.json b/package.json index 8299ae0..5732158 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "vscode-nls": "^5.0.0", + "vscode-jsonrpc": "^5.0.0", "typescript": "^4.5.4", "@types/node": "13.7.6", "rimraf": "^3.0.2", diff --git a/src/browser/main.ts b/src/browser/main.ts index 11872ac..1e96ebb 100644 --- a/src/browser/main.ts +++ b/src/browser/main.ts @@ -8,47 +8,58 @@ import { XHRRequest, XHRConfigure, XHROptions, XHRResponse } from '../../api'; export const configure: XHRConfigure = (_proxyUrl: string, _strictSSL: boolean) => { }; export const xhr: XHRRequest = async (options: XHROptions): Promise => { - const requestHeaders = new Headers(); - if (options.headers) { - for (const key in options.headers) { - const value = options.headers[key]; - if (Array.isArray(value)) { - value.forEach(v => requestHeaders.set(key, v)) - } else { - requestHeaders.set(key, value); - } - } - } - if (options.user && options.password) { - requestHeaders.set('Authorization', 'Basic ' + btoa(options.user + ":" + options.password)); - } - const requestInit: RequestInit = { - method: options.type, - redirect: options.followRedirects > 0 ? 'follow' : 'manual', - mode: 'cors', - headers: requestHeaders - }; - if (options.data) { - requestInit.body = options.data; - } + const requestHeaders = new Headers(); + if (options.headers) { + for (const key in options.headers) { + const value = options.headers[key]; + if (Array.isArray(value)) { + value.forEach(v => requestHeaders.set(key, v)) + } else { + requestHeaders.set(key, value); + } + } + } + if (options.user && options.password) { + requestHeaders.set('Authorization', 'Basic ' + btoa(options.user + ":" + options.password)); + } + const requestInit: RequestInit = { + method: options.type, + redirect: options.followRedirects > 0 ? 'follow' : 'manual', + mode: 'cors', + headers: requestHeaders + }; + if (options.data) { + requestInit.body = options.data; + } + if (options.token) { + const controller = new AbortController(); + if (options.token.isCancellationRequested) { + // see https://github.com/microsoft/TypeScript/issues/49609 + (controller as any).abort(); + } + options.token.onCancellationRequested(() => { + (controller as any).abort(); + }); + requestInit.signal = controller.signal; + } - const requestInfo = new Request(options.url, requestInit); - const response = await fetch(requestInfo); - const resposeHeaders: any = {}; - response.headers.forEach((value, key) => { - resposeHeaders[key] = value; - }); + const requestInfo = new Request(options.url, requestInit); + const response = await fetch(requestInfo); + const resposeHeaders: any = {}; + response.headers.forEach((value, key) => { + resposeHeaders[key] = value; + }); - const buffer = await response.arrayBuffer(); + const buffer = await response.arrayBuffer(); - return new class { - get responseText() { return new TextDecoder().decode(buffer); }; - get body() { return new Uint8Array(buffer) }; - readonly status = response.status; - readonly headers = resposeHeaders; - } + return new class { + get responseText() { return new TextDecoder().decode(buffer); }; + get body() { return new Uint8Array(buffer) }; + readonly status = response.status; + readonly headers = resposeHeaders; + } } export function getErrorStatusDescription(status: number): string { - return String(status); -} \ No newline at end of file + return String(status); +} diff --git a/src/node/main.ts b/src/node/main.ts index c29346a..a129c72 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -14,8 +14,8 @@ import * as createHttpProxyAgent from 'http-proxy-agent'; import { XHRRequest, XHRConfigure, XHROptions, XHRResponse, HttpProxyAgent, HttpsProxyAgent } from '../../api'; if (process.env.VSCODE_NLS_CONFIG) { - const VSCODE_NLS_CONFIG = process.env.VSCODE_NLS_CONFIG; - nls.config(JSON.parse(VSCODE_NLS_CONFIG)); + const VSCODE_NLS_CONFIG = process.env.VSCODE_NLS_CONFIG; + nls.config(JSON.parse(VSCODE_NLS_CONFIG)); } const localize = nls.loadMessageBundle(); @@ -23,252 +23,252 @@ let proxyUrl: string | undefined = undefined; let strictSSL: boolean = true; export const configure: XHRConfigure = (_proxyUrl: string | undefined, _strictSSL: boolean) => { - proxyUrl = _proxyUrl; - strictSSL = _strictSSL; + proxyUrl = _proxyUrl; + strictSSL = _strictSSL; }; export const xhr: XHRRequest = (options: XHROptions): Promise => { - options = { ...options }; - - if (typeof options.strictSSL !== 'boolean') { - options.strictSSL = strictSSL; - } - if (!options.agent) { - options.agent = getProxyAgent(options.url, { proxyUrl, strictSSL }); - } - if (typeof options.followRedirects !== 'number') { - options.followRedirects = 5; - } - - return request(options).then(result => new Promise((c, e) => { - const res = result.res; - let readable: NodeJS.ReadableStream = res; - let isCompleted = false; - - const encoding = res.headers && res.headers['content-encoding']; - if (encoding && !hasNoBody(options.type, result.res.statusCode)) { - const zlibOptions = { - flush: zlib.constants.Z_SYNC_FLUSH, - finishFlush: zlib.constants.Z_SYNC_FLUSH - }; - if (encoding === 'gzip') { - const gunzip = zlib.createGunzip(zlibOptions); - res.pipe(gunzip); - readable = gunzip; - } else if (encoding === 'deflate') { - const inflate = zlib.createInflate(zlibOptions); - res.pipe(inflate); - readable = inflate; - } - } - const data: any = []; - readable.on('data', c => data.push(c)); - readable.on('end', () => { - if (isCompleted) { - return; - } - isCompleted = true; - if (options.followRedirects > 0 && (res.statusCode >= 300 && res.statusCode <= 303 || res.statusCode === 307)) { - let location = res.headers['location']; - if (location.startsWith('/')) { - const endpoint = parseUrl(options.url); - location = format({ - protocol: endpoint.protocol, - hostname: endpoint.hostname, - port: endpoint.port, - pathname: location - }); - } - if (location) { - const newOptions = { - type: options.type, url: location, user: options.user, password: options.password, headers: options.headers, - timeout: options.timeout, followRedirects: options.followRedirects - 1, data: options.data - }; - xhr(newOptions).then(c, e); - return; - } - } - - const buffer = Buffer.concat(data); - - const response: XHRResponse = { - responseText: buffer.toString(), - body: buffer, - status: res.statusCode, - headers: res.headers || {} - }; - - if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 1223) { - c(response); - } else { - e(response); - } - }); - readable.on('error', (err) => { - const response: XHRResponse = { - responseText: localize('error', 'Unable to access {0}. Error: {1}', options.url, err.message), - body: Buffer.concat(data), - status: 500, - headers: {} - }; - isCompleted = true; - e(response); - }); - }), err => { - let message: string; - - if (options.agent) { - message = localize('error.cannot.connect.proxy', 'Unable to connect to {0} through a proxy. Error: {1}', options.url, err.message); - } else { - message = localize('error.cannot.connect', 'Unable to connect to {0}. Error: {1}', options.url, err.message); - } - - return Promise.reject({ - responseText: message, - body: Buffer.concat([]), - status: 404, - headers: {} - }); - }); + options = { ...options }; + + if (typeof options.strictSSL !== 'boolean') { + options.strictSSL = strictSSL; + } + if (!options.agent) { + options.agent = getProxyAgent(options.url, { proxyUrl, strictSSL }); + } + if (typeof options.followRedirects !== 'number') { + options.followRedirects = 5; + } + + return request(options).then(result => new Promise((c, e) => { + const res = result.res; + let readable: NodeJS.ReadableStream = res; + let isCompleted = false; + + const encoding = res.headers && res.headers['content-encoding']; + if (encoding && !hasNoBody(options.type, result.res.statusCode)) { + const zlibOptions = { + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + }; + if (encoding === 'gzip') { + const gunzip = zlib.createGunzip(zlibOptions); + res.pipe(gunzip); + readable = gunzip; + } else if (encoding === 'deflate') { + const inflate = zlib.createInflate(zlibOptions); + res.pipe(inflate); + readable = inflate; + } + } + const data: any = []; + readable.on('data', c => data.push(c)); + readable.on('end', () => { + if (isCompleted) { + return; + } + isCompleted = true; + if (options.followRedirects > 0 && (res.statusCode >= 300 && res.statusCode <= 303 || res.statusCode === 307)) { + let location = res.headers['location']; + if (location.startsWith('/')) { + const endpoint = parseUrl(options.url); + location = format({ + protocol: endpoint.protocol, + hostname: endpoint.hostname, + port: endpoint.port, + pathname: location + }); + } + if (location) { + const newOptions = { + type: options.type, url: location, user: options.user, password: options.password, headers: options.headers, + timeout: options.timeout, followRedirects: options.followRedirects - 1, data: options.data + }; + xhr(newOptions).then(c, e); + return; + } + } + + const buffer = Buffer.concat(data); + + const response: XHRResponse = { + responseText: buffer.toString(), + body: buffer, + status: res.statusCode, + headers: res.headers || {} + }; + + if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 1223) { + c(response); + } else { + e(response); + } + }); + readable.on('error', (err) => { + const response: XHRResponse = { + responseText: localize('error', 'Unable to access {0}. Error: {1}', options.url, err.message), + body: Buffer.concat(data), + status: 500, + headers: {} + }; + isCompleted = true; + e(response); + }); + }), err => { + let message: string; + + if (options.agent) { + message = localize('error.cannot.connect.proxy', 'Unable to connect to {0} through a proxy. Error: {1}', options.url, err.message); + } else { + message = localize('error.cannot.connect', 'Unable to connect to {0}. Error: {1}', options.url, err.message); + } + + return Promise.reject({ + responseText: message, + body: Buffer.concat([]), + status: 404, + headers: {} + }); + }); } function assign(destination: any, ...sources: any[]): any { - sources.forEach(source => Object.keys(source).forEach((key) => destination[key] = source[key])); - return destination; + sources.forEach(source => Object.keys(source).forEach((key) => destination[key] = source[key])); + return destination; } function hasNoBody(method: string, code: number) { - return method === 'HEAD' || /* Informational */ (code >= 100 && code < 200) || /* No Content */ code === 204 || /* Not Modified */ code === 304; + return method === 'HEAD' || /* Informational */ (code >= 100 && code < 200) || /* No Content */ code === 204 || /* Not Modified */ code === 304; } interface RequestResult { - req: http.ClientRequest; - res: http.IncomingMessage; + req: http.ClientRequest; + res: http.IncomingMessage; } function request(options: XHROptions): Promise { - let req: http.ClientRequest; - - return new Promise((c, e) => { - const endpoint = parseUrl(options.url); - - const opts: https.RequestOptions = { - hostname: endpoint.hostname, - agent: options.agent ? options.agent : false, - port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80), - path: endpoint.path, - method: options.type || 'GET', - headers: options.headers, - rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true - }; - - if (options.user && options.password) { - opts.auth = options.user + ':' + options.password; - } - - const handler = (res: http.IncomingMessage) => { - if (res.statusCode >= 300 && res.statusCode < 400 && options.followRedirects && options.followRedirects > 0 && res.headers['location']) { - let location = res.headers['location']; - if (location.startsWith('/')) { - location = format({ - protocol: endpoint.protocol, - hostname: endpoint.hostname, - port: endpoint.port, - pathname: location - }); - } - c(request(assign({}, options, { - url: location, - followRedirects: options.followRedirects - 1 - }))); - } else { - c({ req, res }); - } - } - if (endpoint.protocol === 'https:') { - req = https.request(opts, handler); - } else { - req = http.request(opts, handler); - } - - req.on('error', e); - - if (options.timeout) { - req.setTimeout(options.timeout); - } - if (options.data) { - req.write(options.data); - } - - req.end(); - }); + let req: http.ClientRequest; + + return new Promise((c, e) => { + const endpoint = parseUrl(options.url); + + const opts: https.RequestOptions = { + hostname: endpoint.hostname, + agent: options.agent ? options.agent : false, + port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80), + path: endpoint.path, + method: options.type || 'GET', + headers: options.headers, + rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true + }; + + if (options.user && options.password) { + opts.auth = options.user + ':' + options.password; + } + + const handler = (res: http.IncomingMessage) => { + if (res.statusCode >= 300 && res.statusCode < 400 && options.followRedirects && options.followRedirects > 0 && res.headers['location']) { + let location = res.headers['location']; + if (location.startsWith('/')) { + location = format({ + protocol: endpoint.protocol, + hostname: endpoint.hostname, + port: endpoint.port, + pathname: location + }); + } + c(request(assign({}, options, { + url: location, + followRedirects: options.followRedirects - 1 + }))); + } else { + c({ req, res }); + } + } + if (endpoint.protocol === 'https:') { + req = https.request(opts, handler); + } else { + req = http.request(opts, handler); + } + + req.on('error', e); + + if (options.timeout) { + req.setTimeout(options.timeout); + } + if (options.data) { + req.write(options.data); + } + + req.end(); + }); } export function getErrorStatusDescription(status: number): string { - if (status < 400) { - return void 0; - } - switch (status) { - case 400: return localize('status.400', 'Bad request. The request cannot be fulfilled due to bad syntax.'); - case 401: return localize('status.401', 'Unauthorized. The server is refusing to respond.'); - case 403: return localize('status.403', 'Forbidden. The server is refusing to respond.'); - case 404: return localize('status.404', 'Not Found. The requested location could not be found.'); - case 405: return localize('status.405', 'Method not allowed. A request was made using a request method not supported by that location.'); - case 406: return localize('status.406', 'Not Acceptable. The server can only generate a response that is not accepted by the client.'); - case 407: return localize('status.407', 'Proxy Authentication Required. The client must first authenticate itself with the proxy.'); - case 408: return localize('status.408', 'Request Timeout. The server timed out waiting for the request.'); - case 409: return localize('status.409', 'Conflict. The request could not be completed because of a conflict in the request.'); - case 410: return localize('status.410', 'Gone. The requested page is no longer available.'); - case 411: return localize('status.411', 'Length Required. The "Content-Length" is not defined.'); - case 412: return localize('status.412', 'Precondition Failed. The precondition given in the request evaluated to false by the server.'); - case 413: return localize('status.413', 'Request Entity Too Large. The server will not accept the request, because the request entity is too large.'); - case 414: return localize('status.414', 'Request-URI Too Long. The server will not accept the request, because the URL is too long.'); - case 415: return localize('status.415', 'Unsupported Media Type. The server will not accept the request, because the media type is not supported.'); - case 500: return localize('status.500', 'Internal Server Error.'); - case 501: return localize('status.501', 'Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request.'); - case 503: return localize('status.503', 'Service Unavailable. The server is currently unavailable (overloaded or down).'); - default: return localize('status.416', 'HTTP status code {0}', status); - } + if (status < 400) { + return void 0; + } + switch (status) { + case 400: return localize('status.400', 'Bad request. The request cannot be fulfilled due to bad syntax.'); + case 401: return localize('status.401', 'Unauthorized. The server is refusing to respond.'); + case 403: return localize('status.403', 'Forbidden. The server is refusing to respond.'); + case 404: return localize('status.404', 'Not Found. The requested location could not be found.'); + case 405: return localize('status.405', 'Method not allowed. A request was made using a request method not supported by that location.'); + case 406: return localize('status.406', 'Not Acceptable. The server can only generate a response that is not accepted by the client.'); + case 407: return localize('status.407', 'Proxy Authentication Required. The client must first authenticate itself with the proxy.'); + case 408: return localize('status.408', 'Request Timeout. The server timed out waiting for the request.'); + case 409: return localize('status.409', 'Conflict. The request could not be completed because of a conflict in the request.'); + case 410: return localize('status.410', 'Gone. The requested page is no longer available.'); + case 411: return localize('status.411', 'Length Required. The "Content-Length" is not defined.'); + case 412: return localize('status.412', 'Precondition Failed. The precondition given in the request evaluated to false by the server.'); + case 413: return localize('status.413', 'Request Entity Too Large. The server will not accept the request, because the request entity is too large.'); + case 414: return localize('status.414', 'Request-URI Too Long. The server will not accept the request, because the URL is too long.'); + case 415: return localize('status.415', 'Unsupported Media Type. The server will not accept the request, because the media type is not supported.'); + case 500: return localize('status.500', 'Internal Server Error.'); + case 501: return localize('status.501', 'Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request.'); + case 503: return localize('status.503', 'Service Unavailable. The server is currently unavailable (overloaded or down).'); + default: return localize('status.416', 'HTTP status code {0}', status); + } } // proxy handling function getSystemProxyURI(requestURL: Url): string { - if (requestURL.protocol === 'http:') { - return process.env.HTTP_PROXY || process.env.http_proxy || null; - } else if (requestURL.protocol === 'https:') { - return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null; - } + if (requestURL.protocol === 'http:') { + return process.env.HTTP_PROXY || process.env.http_proxy || null; + } else if (requestURL.protocol === 'https:') { + return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null; + } - return null; + return null; } interface ProxyOptions { - proxyUrl?: string; - strictSSL?: boolean; + proxyUrl?: string; + strictSSL?: boolean; } function getProxyAgent(rawRequestURL: string, options: ProxyOptions = {}): HttpProxyAgent | HttpsProxyAgent | undefined { - const requestURL = parseUrl(rawRequestURL); - const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL); + const requestURL = parseUrl(rawRequestURL); + const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL); - if (!proxyURL) { - return null; - } + if (!proxyURL) { + return null; + } - const proxyEndpoint = parseUrl(proxyURL); + const proxyEndpoint = parseUrl(proxyURL); - if (!/^https?:$/.test(proxyEndpoint.protocol)) { - return null; - } + if (!/^https?:$/.test(proxyEndpoint.protocol)) { + return null; + } - const opts = { - host: proxyEndpoint.hostname, - port: Number(proxyEndpoint.port), - auth: proxyEndpoint.auth, - rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true, - protocol: proxyEndpoint.protocol - }; + const opts = { + host: proxyEndpoint.hostname, + port: Number(proxyEndpoint.port), + auth: proxyEndpoint.auth, + rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true, + protocol: proxyEndpoint.protocol + }; - return requestURL.protocol === 'http:' ? createHttpProxyAgent(opts) : createHttpsProxyAgent(opts); + return requestURL.protocol === 'http:' ? createHttpProxyAgent(opts) : createHttpsProxyAgent(opts); } From 64d2221060a727726d6f32a2bb05b4f55a6d03d2 Mon Sep 17 00:00:00 2001 From: Tarrowren Date: Wed, 2 Nov 2022 15:33:54 +0800 Subject: [PATCH 2/5] node suppert `CancellationToken` --- src/node/main.ts | 81 ++++++++++++++++++++++++++++++++++++------------ src/test/test.ts | 23 ++++++++++++++ 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/node/main.ts b/src/node/main.ts index a129c72..618f8d5 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -42,7 +42,7 @@ export const xhr: XHRRequest = (options: XHROptions): Promise => { return request(options).then(result => new Promise((c, e) => { const res = result.res; - let readable: NodeJS.ReadableStream = res; + let readable: import('stream').Readable = res; let isCompleted = false; const encoding = res.headers && res.headers['content-encoding']; @@ -80,9 +80,9 @@ export const xhr: XHRRequest = (options: XHROptions): Promise => { }); } if (location) { - const newOptions = { + const newOptions: XHROptions = { type: options.type, url: location, user: options.user, password: options.password, headers: options.headers, - timeout: options.timeout, followRedirects: options.followRedirects - 1, data: options.data + timeout: options.timeout, followRedirects: options.followRedirects - 1, data: options.data, token: options.token }; xhr(newOptions).then(c, e); return; @@ -105,30 +105,51 @@ export const xhr: XHRRequest = (options: XHROptions): Promise => { } }); readable.on('error', (err) => { - const response: XHRResponse = { - responseText: localize('error', 'Unable to access {0}. Error: {1}', options.url, err.message), - body: Buffer.concat(data), - status: 500, - headers: {} - }; + let response: XHRResponse | Error; + if (isAbortError(err)) { + response = err; + } else { + response = { + responseText: localize('error', 'Unable to access {0}. Error: {1}', options.url, err.message), + body: Buffer.concat(data), + status: 500, + headers: {} + }; + } isCompleted = true; e(response); }); - }), err => { - let message: string; - if (options.agent) { - message = localize('error.cannot.connect.proxy', 'Unable to connect to {0} through a proxy. Error: {1}', options.url, err.message); + if (options.token) { + if (options.token.isCancellationRequested) { + readable.destroy(getAbortError()); + } + options.token.onCancellationRequested(() => { + readable.destroy(getAbortError()); + }); + } + }), err => { + let response: XHRResponse | Error; + if (isAbortError(err)) { + response = err; } else { - message = localize('error.cannot.connect', 'Unable to connect to {0}. Error: {1}', options.url, err.message); + let message: string; + + if (options.agent) { + message = localize('error.cannot.connect.proxy', 'Unable to connect to {0} through a proxy. Error: {1}', options.url, err.message); + } else { + message = localize('error.cannot.connect', 'Unable to connect to {0}. Error: {1}', options.url, err.message); + } + + response = { + responseText: message, + body: Buffer.concat([]), + status: 404, + headers: {} + }; } - return Promise.reject({ - responseText: message, - body: Buffer.concat([]), - status: 404, - headers: {} - }); + return Promise.reject(response); }); } @@ -201,6 +222,15 @@ function request(options: XHROptions): Promise { } req.end(); + + if (options.token) { + if (options.token.isCancellationRequested) { + req.destroy(getAbortError()); + } + options.token.onCancellationRequested(() => { + req.destroy(getAbortError()); + }); + } }); } @@ -272,3 +302,14 @@ function getProxyAgent(rawRequestURL: string, options: ProxyOptions = {}): HttpP return requestURL.protocol === 'http:' ? createHttpProxyAgent(opts) : createHttpsProxyAgent(opts); } + +function getAbortError(): Error { + const err: any = new Error('The user aborted a request'); + err.code = 20 + err.name = 'AbortError'; + return err; +} + +function isAbortError(value: any): boolean { + return value && value.code === 20 && value.name === 'AbortError'; +} diff --git a/src/test/test.ts b/src/test/test.ts index 079049e..74dd9d4 100644 --- a/src/test/test.ts +++ b/src/test/test.ts @@ -8,6 +8,7 @@ import { AddressInfo } from 'net'; import { promises as fs } from 'fs'; import { join } from 'path'; import { IncomingMessage, ServerResponse } from 'http'; +import { CancellationTokenSource } from 'vscode-jsonrpc' test('text content', async t => { const testContent = JSON.stringify({ hello: 1, world: true }) @@ -149,3 +150,25 @@ test('relative redirect', async t => { server.close(); }); + +test('cancellation token', async t => { + const server = await createServer(); + + server.on('request', (req, res) => { + res.writeHead(200, { 'Content-Encoding': 'gzip' }); + res.end(); + }); + + const serverAddress = server.address() as AddressInfo; + const cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.cancel(); + try { + await xhr({ url: `http://${serverAddress.address}:${serverAddress.port}`, token: cancellationTokenSource.token }); + t.fail('not aborted') + } catch (e) { + t.is(e.code, 20); + t.is(e.name, 'AbortError'); + } + + server.close(); +}) From e06463ca25c86ba61482f9445d9bdbce0ef54387 Mon Sep 17 00:00:00 2001 From: Tarrowren Date: Wed, 2 Nov 2022 15:34:03 +0800 Subject: [PATCH 3/5] 0.6.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e2be4d..9be11d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "request-light", - "version": "0.5.8", + "version": "0.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5732158..7cc970f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "request-light", - "version": "0.5.8", + "version": "0.6.0", "description": "Lightweight request library. Promise based, with proxy support.", "main": "./lib/node/main.js", "browser": { From 552f78ebd7e4078c2ad0a7fc5f8a01598683684f Mon Sep 17 00:00:00 2001 From: Tarrowren Date: Thu, 3 Nov 2022 10:37:10 +0800 Subject: [PATCH 4/5] use tab --- src/browser/main.ts | 96 ++++---- src/node/main.ts | 519 ++++++++++++++++++++++---------------------- src/test/test.ts | 1 - 3 files changed, 309 insertions(+), 307 deletions(-) diff --git a/src/browser/main.ts b/src/browser/main.ts index 1e96ebb..8950ecf 100644 --- a/src/browser/main.ts +++ b/src/browser/main.ts @@ -8,58 +8,58 @@ import { XHRRequest, XHRConfigure, XHROptions, XHRResponse } from '../../api'; export const configure: XHRConfigure = (_proxyUrl: string, _strictSSL: boolean) => { }; export const xhr: XHRRequest = async (options: XHROptions): Promise => { - const requestHeaders = new Headers(); - if (options.headers) { - for (const key in options.headers) { - const value = options.headers[key]; - if (Array.isArray(value)) { - value.forEach(v => requestHeaders.set(key, v)) - } else { - requestHeaders.set(key, value); - } - } - } - if (options.user && options.password) { - requestHeaders.set('Authorization', 'Basic ' + btoa(options.user + ":" + options.password)); - } - const requestInit: RequestInit = { - method: options.type, - redirect: options.followRedirects > 0 ? 'follow' : 'manual', - mode: 'cors', - headers: requestHeaders - }; - if (options.data) { - requestInit.body = options.data; - } - if (options.token) { - const controller = new AbortController(); - if (options.token.isCancellationRequested) { - // see https://github.com/microsoft/TypeScript/issues/49609 - (controller as any).abort(); - } - options.token.onCancellationRequested(() => { - (controller as any).abort(); - }); - requestInit.signal = controller.signal; - } + const requestHeaders = new Headers(); + if (options.headers) { + for (const key in options.headers) { + const value = options.headers[key]; + if (Array.isArray(value)) { + value.forEach(v => requestHeaders.set(key, v)) + } else { + requestHeaders.set(key, value); + } + } + } + if (options.user && options.password) { + requestHeaders.set('Authorization', 'Basic ' + btoa(options.user + ":" + options.password)); + } + const requestInit: RequestInit = { + method: options.type, + redirect: options.followRedirects > 0 ? 'follow' : 'manual', + mode: 'cors', + headers: requestHeaders + }; + if (options.data) { + requestInit.body = options.data; + } + if (options.token) { + const controller = new AbortController(); + if (options.token.isCancellationRequested) { + // see https://github.com/microsoft/TypeScript/issues/49609 + (controller as any).abort(); + } + options.token.onCancellationRequested(() => { + (controller as any).abort(); + }); + requestInit.signal = controller.signal; + } - const requestInfo = new Request(options.url, requestInit); - const response = await fetch(requestInfo); - const resposeHeaders: any = {}; - response.headers.forEach((value, key) => { - resposeHeaders[key] = value; - }); + const requestInfo = new Request(options.url, requestInit); + const response = await fetch(requestInfo); + const resposeHeaders: any = {}; + response.headers.forEach((value, key) => { + resposeHeaders[key] = value; + }); - const buffer = await response.arrayBuffer(); + const buffer = await response.arrayBuffer(); - return new class { - get responseText() { return new TextDecoder().decode(buffer); }; - get body() { return new Uint8Array(buffer) }; - readonly status = response.status; - readonly headers = resposeHeaders; - } + return new class { + get responseText() { return new TextDecoder().decode(buffer); }; + get body() { return new Uint8Array(buffer) }; + readonly status = response.status; + readonly headers = resposeHeaders; + } } export function getErrorStatusDescription(status: number): string { - return String(status); + return String(status); } diff --git a/src/node/main.ts b/src/node/main.ts index 618f8d5..7b6bc76 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -2,20 +2,20 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Url, parse as parseUrl, format } from 'url'; -import * as https from 'https'; import * as http from 'http'; -import * as zlib from 'zlib'; +import * as https from 'https'; +import { format, parse as parseUrl, Url } from 'url'; import * as nls from 'vscode-nls'; +import * as zlib from 'zlib'; -import * as createHttpsProxyAgent from 'https-proxy-agent'; import * as createHttpProxyAgent from 'http-proxy-agent'; +import * as createHttpsProxyAgent from 'https-proxy-agent'; -import { XHRRequest, XHRConfigure, XHROptions, XHRResponse, HttpProxyAgent, HttpsProxyAgent } from '../../api'; +import { HttpProxyAgent, HttpsProxyAgent, XHRConfigure, XHROptions, XHRRequest, XHRResponse } from '../../api'; if (process.env.VSCODE_NLS_CONFIG) { - const VSCODE_NLS_CONFIG = process.env.VSCODE_NLS_CONFIG; - nls.config(JSON.parse(VSCODE_NLS_CONFIG)); + const VSCODE_NLS_CONFIG = process.env.VSCODE_NLS_CONFIG; + nls.config(JSON.parse(VSCODE_NLS_CONFIG)); } const localize = nls.loadMessageBundle(); @@ -23,293 +23,296 @@ let proxyUrl: string | undefined = undefined; let strictSSL: boolean = true; export const configure: XHRConfigure = (_proxyUrl: string | undefined, _strictSSL: boolean) => { - proxyUrl = _proxyUrl; - strictSSL = _strictSSL; + proxyUrl = _proxyUrl; + strictSSL = _strictSSL; }; export const xhr: XHRRequest = (options: XHROptions): Promise => { - options = { ...options }; - - if (typeof options.strictSSL !== 'boolean') { - options.strictSSL = strictSSL; - } - if (!options.agent) { - options.agent = getProxyAgent(options.url, { proxyUrl, strictSSL }); - } - if (typeof options.followRedirects !== 'number') { - options.followRedirects = 5; - } - - return request(options).then(result => new Promise((c, e) => { - const res = result.res; - let readable: import('stream').Readable = res; - let isCompleted = false; - - const encoding = res.headers && res.headers['content-encoding']; - if (encoding && !hasNoBody(options.type, result.res.statusCode)) { - const zlibOptions = { - flush: zlib.constants.Z_SYNC_FLUSH, - finishFlush: zlib.constants.Z_SYNC_FLUSH - }; - if (encoding === 'gzip') { - const gunzip = zlib.createGunzip(zlibOptions); - res.pipe(gunzip); - readable = gunzip; - } else if (encoding === 'deflate') { - const inflate = zlib.createInflate(zlibOptions); - res.pipe(inflate); - readable = inflate; - } - } - const data: any = []; - readable.on('data', c => data.push(c)); - readable.on('end', () => { - if (isCompleted) { - return; - } - isCompleted = true; - if (options.followRedirects > 0 && (res.statusCode >= 300 && res.statusCode <= 303 || res.statusCode === 307)) { - let location = res.headers['location']; - if (location.startsWith('/')) { - const endpoint = parseUrl(options.url); - location = format({ - protocol: endpoint.protocol, - hostname: endpoint.hostname, - port: endpoint.port, - pathname: location - }); - } - if (location) { - const newOptions: XHROptions = { - type: options.type, url: location, user: options.user, password: options.password, headers: options.headers, - timeout: options.timeout, followRedirects: options.followRedirects - 1, data: options.data, token: options.token - }; - xhr(newOptions).then(c, e); - return; - } - } - - const buffer = Buffer.concat(data); - - const response: XHRResponse = { - responseText: buffer.toString(), - body: buffer, - status: res.statusCode, - headers: res.headers || {} - }; - - if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 1223) { - c(response); - } else { - e(response); - } - }); - readable.on('error', (err) => { - let response: XHRResponse | Error; - if (isAbortError(err)) { - response = err; - } else { - response = { - responseText: localize('error', 'Unable to access {0}. Error: {1}', options.url, err.message), - body: Buffer.concat(data), - status: 500, - headers: {} - }; - } - isCompleted = true; - e(response); - }); - - if (options.token) { - if (options.token.isCancellationRequested) { - readable.destroy(getAbortError()); - } - options.token.onCancellationRequested(() => { - readable.destroy(getAbortError()); - }); - } - }), err => { - let response: XHRResponse | Error; - if (isAbortError(err)) { - response = err; - } else { - let message: string; - - if (options.agent) { - message = localize('error.cannot.connect.proxy', 'Unable to connect to {0} through a proxy. Error: {1}', options.url, err.message); - } else { - message = localize('error.cannot.connect', 'Unable to connect to {0}. Error: {1}', options.url, err.message); - } - - response = { - responseText: message, - body: Buffer.concat([]), - status: 404, - headers: {} - }; - } - - return Promise.reject(response); - }); + options = { ...options }; + + if (typeof options.strictSSL !== 'boolean') { + options.strictSSL = strictSSL; + } + if (!options.agent) { + options.agent = getProxyAgent(options.url, { proxyUrl, strictSSL }); + } + if (typeof options.followRedirects !== 'number') { + options.followRedirects = 5; + } + + return request(options).then(result => new Promise((c, e) => { + const res = result.res; + let readable: import('stream').Readable = res; + let isCompleted = false; + + const encoding = res.headers && res.headers['content-encoding']; + if (encoding && !hasNoBody(options.type, result.res.statusCode)) { + const zlibOptions = { + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + }; + if (encoding === 'gzip') { + const gunzip = zlib.createGunzip(zlibOptions); + res.pipe(gunzip); + readable = gunzip; + } else if (encoding === 'deflate') { + const inflate = zlib.createInflate(zlibOptions); + res.pipe(inflate); + readable = inflate; + } + } + const data: any = []; + readable.on('data', c => data.push(c)); + readable.on('end', () => { + if (isCompleted) { + return; + } + isCompleted = true; + if (options.followRedirects > 0 && (res.statusCode >= 300 && res.statusCode <= 303 || res.statusCode === 307)) { + let location = res.headers['location']; + if (location.startsWith('/')) { + const endpoint = parseUrl(options.url); + location = format({ + protocol: endpoint.protocol, + hostname: endpoint.hostname, + port: endpoint.port, + pathname: location + }); + } + if (location) { + const newOptions: XHROptions = { + type: options.type, url: location, user: options.user, password: options.password, headers: options.headers, + timeout: options.timeout, followRedirects: options.followRedirects - 1, data: options.data, token: options.token + }; + xhr(newOptions).then(c, e); + return; + } + } + + const buffer = Buffer.concat(data); + + const response: XHRResponse = { + responseText: buffer.toString(), + body: buffer, + status: res.statusCode, + headers: res.headers || {} + }; + + if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 1223) { + c(response); + } else { + e(response); + } + }); + readable.on('error', (err) => { + let response: XHRResponse | Error; + if (AbortError.is(err)) { + response = err; + } else { + response = { + responseText: localize('error', 'Unable to access {0}. Error: {1}', options.url, err.message), + body: Buffer.concat(data), + status: 500, + headers: {} + }; + } + isCompleted = true; + e(response); + }); + + if (options.token) { + if (options.token.isCancellationRequested) { + readable.destroy(new AbortError()); + } + options.token.onCancellationRequested(() => { + readable.destroy(new AbortError()); + }); + } + }), err => { + let response: XHRResponse | Error; + if (AbortError.is(err)) { + response = err; + } else { + let message: string; + + if (options.agent) { + message = localize('error.cannot.connect.proxy', 'Unable to connect to {0} through a proxy. Error: {1}', options.url, err.message); + } else { + message = localize('error.cannot.connect', 'Unable to connect to {0}. Error: {1}', options.url, err.message); + } + + response = { + responseText: message, + body: Buffer.concat([]), + status: 404, + headers: {} + }; + } + + return Promise.reject(response); + }); } function assign(destination: any, ...sources: any[]): any { - sources.forEach(source => Object.keys(source).forEach((key) => destination[key] = source[key])); - return destination; + sources.forEach(source => Object.keys(source).forEach((key) => destination[key] = source[key])); + return destination; } function hasNoBody(method: string, code: number) { - return method === 'HEAD' || /* Informational */ (code >= 100 && code < 200) || /* No Content */ code === 204 || /* Not Modified */ code === 304; + return method === 'HEAD' || /* Informational */ (code >= 100 && code < 200) || /* No Content */ code === 204 || /* Not Modified */ code === 304; } interface RequestResult { - req: http.ClientRequest; - res: http.IncomingMessage; + req: http.ClientRequest; + res: http.IncomingMessage; } function request(options: XHROptions): Promise { - let req: http.ClientRequest; - - return new Promise((c, e) => { - const endpoint = parseUrl(options.url); - - const opts: https.RequestOptions = { - hostname: endpoint.hostname, - agent: options.agent ? options.agent : false, - port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80), - path: endpoint.path, - method: options.type || 'GET', - headers: options.headers, - rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true - }; - - if (options.user && options.password) { - opts.auth = options.user + ':' + options.password; - } - - const handler = (res: http.IncomingMessage) => { - if (res.statusCode >= 300 && res.statusCode < 400 && options.followRedirects && options.followRedirects > 0 && res.headers['location']) { - let location = res.headers['location']; - if (location.startsWith('/')) { - location = format({ - protocol: endpoint.protocol, - hostname: endpoint.hostname, - port: endpoint.port, - pathname: location - }); - } - c(request(assign({}, options, { - url: location, - followRedirects: options.followRedirects - 1 - }))); - } else { - c({ req, res }); - } - } - if (endpoint.protocol === 'https:') { - req = https.request(opts, handler); - } else { - req = http.request(opts, handler); - } - - req.on('error', e); - - if (options.timeout) { - req.setTimeout(options.timeout); - } - if (options.data) { - req.write(options.data); - } - - req.end(); - - if (options.token) { - if (options.token.isCancellationRequested) { - req.destroy(getAbortError()); - } - options.token.onCancellationRequested(() => { - req.destroy(getAbortError()); - }); - } - }); + let req: http.ClientRequest; + + return new Promise((c, e) => { + const endpoint = parseUrl(options.url); + + const opts: https.RequestOptions = { + hostname: endpoint.hostname, + agent: options.agent ? options.agent : false, + port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80), + path: endpoint.path, + method: options.type || 'GET', + headers: options.headers, + rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true + }; + + if (options.user && options.password) { + opts.auth = options.user + ':' + options.password; + } + + const handler = (res: http.IncomingMessage) => { + if (res.statusCode >= 300 && res.statusCode < 400 && options.followRedirects && options.followRedirects > 0 && res.headers['location']) { + let location = res.headers['location']; + if (location.startsWith('/')) { + location = format({ + protocol: endpoint.protocol, + hostname: endpoint.hostname, + port: endpoint.port, + pathname: location + }); + } + c(request(assign({}, options, { + url: location, + followRedirects: options.followRedirects - 1 + }))); + } else { + c({ req, res }); + } + } + if (endpoint.protocol === 'https:') { + req = https.request(opts, handler); + } else { + req = http.request(opts, handler); + } + + req.on('error', e); + + if (options.timeout) { + req.setTimeout(options.timeout); + } + if (options.data) { + req.write(options.data); + } + + req.end(); + + if (options.token) { + if (options.token.isCancellationRequested) { + req.destroy(new AbortError()); + } + options.token.onCancellationRequested(() => { + req.destroy(new AbortError()); + }); + } + }); } export function getErrorStatusDescription(status: number): string { - if (status < 400) { - return void 0; - } - switch (status) { - case 400: return localize('status.400', 'Bad request. The request cannot be fulfilled due to bad syntax.'); - case 401: return localize('status.401', 'Unauthorized. The server is refusing to respond.'); - case 403: return localize('status.403', 'Forbidden. The server is refusing to respond.'); - case 404: return localize('status.404', 'Not Found. The requested location could not be found.'); - case 405: return localize('status.405', 'Method not allowed. A request was made using a request method not supported by that location.'); - case 406: return localize('status.406', 'Not Acceptable. The server can only generate a response that is not accepted by the client.'); - case 407: return localize('status.407', 'Proxy Authentication Required. The client must first authenticate itself with the proxy.'); - case 408: return localize('status.408', 'Request Timeout. The server timed out waiting for the request.'); - case 409: return localize('status.409', 'Conflict. The request could not be completed because of a conflict in the request.'); - case 410: return localize('status.410', 'Gone. The requested page is no longer available.'); - case 411: return localize('status.411', 'Length Required. The "Content-Length" is not defined.'); - case 412: return localize('status.412', 'Precondition Failed. The precondition given in the request evaluated to false by the server.'); - case 413: return localize('status.413', 'Request Entity Too Large. The server will not accept the request, because the request entity is too large.'); - case 414: return localize('status.414', 'Request-URI Too Long. The server will not accept the request, because the URL is too long.'); - case 415: return localize('status.415', 'Unsupported Media Type. The server will not accept the request, because the media type is not supported.'); - case 500: return localize('status.500', 'Internal Server Error.'); - case 501: return localize('status.501', 'Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request.'); - case 503: return localize('status.503', 'Service Unavailable. The server is currently unavailable (overloaded or down).'); - default: return localize('status.416', 'HTTP status code {0}', status); - } + if (status < 400) { + return void 0; + } + switch (status) { + case 400: return localize('status.400', 'Bad request. The request cannot be fulfilled due to bad syntax.'); + case 401: return localize('status.401', 'Unauthorized. The server is refusing to respond.'); + case 403: return localize('status.403', 'Forbidden. The server is refusing to respond.'); + case 404: return localize('status.404', 'Not Found. The requested location could not be found.'); + case 405: return localize('status.405', 'Method not allowed. A request was made using a request method not supported by that location.'); + case 406: return localize('status.406', 'Not Acceptable. The server can only generate a response that is not accepted by the client.'); + case 407: return localize('status.407', 'Proxy Authentication Required. The client must first authenticate itself with the proxy.'); + case 408: return localize('status.408', 'Request Timeout. The server timed out waiting for the request.'); + case 409: return localize('status.409', 'Conflict. The request could not be completed because of a conflict in the request.'); + case 410: return localize('status.410', 'Gone. The requested page is no longer available.'); + case 411: return localize('status.411', 'Length Required. The "Content-Length" is not defined.'); + case 412: return localize('status.412', 'Precondition Failed. The precondition given in the request evaluated to false by the server.'); + case 413: return localize('status.413', 'Request Entity Too Large. The server will not accept the request, because the request entity is too large.'); + case 414: return localize('status.414', 'Request-URI Too Long. The server will not accept the request, because the URL is too long.'); + case 415: return localize('status.415', 'Unsupported Media Type. The server will not accept the request, because the media type is not supported.'); + case 500: return localize('status.500', 'Internal Server Error.'); + case 501: return localize('status.501', 'Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request.'); + case 503: return localize('status.503', 'Service Unavailable. The server is currently unavailable (overloaded or down).'); + default: return localize('status.416', 'HTTP status code {0}', status); + } } // proxy handling function getSystemProxyURI(requestURL: Url): string { - if (requestURL.protocol === 'http:') { - return process.env.HTTP_PROXY || process.env.http_proxy || null; - } else if (requestURL.protocol === 'https:') { - return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null; - } + if (requestURL.protocol === 'http:') { + return process.env.HTTP_PROXY || process.env.http_proxy || null; + } else if (requestURL.protocol === 'https:') { + return process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null; + } - return null; + return null; } interface ProxyOptions { - proxyUrl?: string; - strictSSL?: boolean; + proxyUrl?: string; + strictSSL?: boolean; } function getProxyAgent(rawRequestURL: string, options: ProxyOptions = {}): HttpProxyAgent | HttpsProxyAgent | undefined { - const requestURL = parseUrl(rawRequestURL); - const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL); + const requestURL = parseUrl(rawRequestURL); + const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL); - if (!proxyURL) { - return null; - } + if (!proxyURL) { + return null; + } - const proxyEndpoint = parseUrl(proxyURL); + const proxyEndpoint = parseUrl(proxyURL); - if (!/^https?:$/.test(proxyEndpoint.protocol)) { - return null; - } + if (!/^https?:$/.test(proxyEndpoint.protocol)) { + return null; + } - const opts = { - host: proxyEndpoint.hostname, - port: Number(proxyEndpoint.port), - auth: proxyEndpoint.auth, - rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true, - protocol: proxyEndpoint.protocol - }; + const opts = { + host: proxyEndpoint.hostname, + port: Number(proxyEndpoint.port), + auth: proxyEndpoint.auth, + rejectUnauthorized: (typeof options.strictSSL === 'boolean') ? options.strictSSL : true, + protocol: proxyEndpoint.protocol + }; - return requestURL.protocol === 'http:' ? createHttpProxyAgent(opts) : createHttpsProxyAgent(opts); + return requestURL.protocol === 'http:' ? createHttpProxyAgent(opts) : createHttpsProxyAgent(opts); } -function getAbortError(): Error { - const err: any = new Error('The user aborted a request'); - err.code = 20 - err.name = 'AbortError'; - return err; -} +class AbortError extends Error { + constructor() { + super('The user aborted a request'); + this.name = 'AbortError'; + + // see https://github.com/microsoft/TypeScript/issues/13965 + Object.setPrototypeOf(this, AbortError.prototype); + } -function isAbortError(value: any): boolean { - return value && value.code === 20 && value.name === 'AbortError'; + static is(value: any): boolean { + return value instanceof AbortError; + } } diff --git a/src/test/test.ts b/src/test/test.ts index 74dd9d4..ac09741 100644 --- a/src/test/test.ts +++ b/src/test/test.ts @@ -166,7 +166,6 @@ test('cancellation token', async t => { await xhr({ url: `http://${serverAddress.address}:${serverAddress.port}`, token: cancellationTokenSource.token }); t.fail('not aborted') } catch (e) { - t.is(e.code, 20); t.is(e.name, 'AbortError'); } From f4e91ed470dbb8f8063faddada9e0cefec909c94 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 3 Nov 2022 11:40:44 +0100 Subject: [PATCH 5/5] inline CancellationToken --- api.d.ts | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/api.d.ts b/api.d.ts index 8076797..d64e4fc 100644 --- a/api.d.ts +++ b/api.d.ts @@ -12,7 +12,7 @@ export interface XHROptions { data?: string; strictSSL?: boolean; followRedirects?: number; - token?: import("vscode-jsonrpc").CancellationToken; + token?: CancellationToken; agent?: HttpProxyAgent | HttpsProxyAgent; } @@ -31,6 +31,41 @@ export interface XHRConfigure { (proxyUrl: string | undefined, strictSSL: boolean): void; } +export interface Disposable { + /** + * Dispose this object. + */ + dispose(): void; +} +/** + * Represents a typed event. + */ + export interface Event { + /** + * + * @param listener The listener function will be call when the event happens. + * @param thisArgs The 'this' which will be used when calling the event listener. + * @param disposables An array to which a {{IDisposable}} will be added. The + * @return + */ + (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable; +} +/** + * Defines a CancellationToken. This interface is not + * intended to be implemented. A CancellationToken must + * be created via a CancellationTokenSource. + */ + export interface CancellationToken { + /** + * Is `true` when the token has been cancelled, `false` otherwise. + */ + readonly isCancellationRequested: boolean; + /** + * An [event](#Event) which fires upon cancellation. + */ + readonly onCancellationRequested: Event; +} + export type HttpProxyAgent = import('http-proxy-agent').HttpProxyAgent; export type HttpsProxyAgent = import('https-proxy-agent').HttpsProxyAgent;