@theia/request 1.71.0-next.8 → 1.72.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,33 +14,20 @@
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
16
 
17
- import * as http from 'http';
18
- import * as https from 'https';
19
- import { getProxyAgent, ProxyAgent } from './proxy';
17
+ import type { Dispatcher } from 'undici';
18
+ import { getProxyAgent } from './proxy';
20
19
  import { Headers, RequestConfiguration, RequestContext, RequestOptions, RequestService, CancellationToken } from './common-request-service';
21
- import { createGunzip } from 'zlib';
22
-
23
- export interface RawRequestFunction {
24
- (options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void): http.ClientRequest;
25
- }
26
20
 
27
21
  export interface NodeRequestOptions extends RequestOptions {
28
- agent?: ProxyAgent | http.Agent | https.Agent | boolean;
29
22
  strictSSL?: boolean;
30
- getRawRequest?(options: NodeRequestOptions): RawRequestFunction;
31
- };
23
+ dispatcher?: Dispatcher;
24
+ }
32
25
 
33
26
  export class NodeRequestService implements RequestService {
34
27
  protected proxyUrl?: string;
35
28
  protected strictSSL?: boolean;
36
29
  protected authorization?: string;
37
30
 
38
- protected getNodeRequest(options: RequestOptions): RawRequestFunction {
39
- const endpoint = new URL(options.url);
40
- const module = endpoint.protocol === 'https:' ? https : http;
41
- return module.request;
42
- }
43
-
44
31
  protected async getProxyUrl(url: string): Promise<string | undefined> {
45
32
  return this.proxyUrl;
46
33
  }
@@ -60,11 +47,12 @@ export class NodeRequestService implements RequestService {
60
47
  protected async processOptions(options: NodeRequestOptions): Promise<NodeRequestOptions> {
61
48
  const { strictSSL } = this;
62
49
  options.strictSSL = options.strictSSL ?? strictSSL;
63
- const agent = options.agent ? options.agent : getProxyAgent(options.url || '', process.env, {
64
- proxyUrl: await this.getProxyUrl(options.url),
65
- strictSSL: options.strictSSL
66
- });
67
- options.agent = agent;
50
+ if (!options.dispatcher) {
51
+ options.dispatcher = getProxyAgent(options.url || '', process.env, {
52
+ proxyUrl: await this.getProxyUrl(options.url),
53
+ strictSSL: options.strictSSL
54
+ });
55
+ }
68
56
 
69
57
  const authorization = options.proxyAuthorization || this.authorization;
70
58
  if (authorization) {
@@ -74,100 +62,75 @@ export class NodeRequestService implements RequestService {
74
62
  };
75
63
  }
76
64
 
77
- options.headers = {
78
- 'Accept-Encoding': 'gzip',
79
- ...(options.headers || {}),
80
- };
81
-
82
65
  return options;
83
66
  }
84
67
 
85
- request(options: NodeRequestOptions, token?: CancellationToken): Promise<RequestContext> {
86
- return new Promise(async (resolve, reject) => {
87
- options = await this.processOptions(options);
88
-
89
- const endpoint = new URL(options.url);
90
- const rawRequest = options.getRawRequest
91
- ? options.getRawRequest(options)
92
- : this.getNodeRequest(options);
93
-
94
- const opts: https.RequestOptions = {
95
- hostname: endpoint.hostname,
96
- port: endpoint.port ? parseInt(endpoint.port) : (endpoint.protocol === 'https:' ? 443 : 80),
97
- protocol: endpoint.protocol,
98
- path: endpoint.pathname + endpoint.search,
99
- method: options.type || 'GET',
100
- headers: options.headers,
101
- agent: options.agent as https.Agent,
102
- rejectUnauthorized: !!options.strictSSL
103
- };
68
+ async request(options: NodeRequestOptions, token?: CancellationToken): Promise<RequestContext> {
69
+ options = await this.processOptions(options);
104
70
 
105
- if (options.user && options.password) {
106
- opts.auth = options.user + ':' + options.password;
107
- }
71
+ const headers: Record<string, string> = { ...(options.headers || {}) };
108
72
 
109
- const timeoutHandler = () => {
110
- reject('timeout');
111
- };
73
+ if (options.user && options.password) {
74
+ headers['Authorization'] = 'Basic ' + Buffer.from(options.user + ':' + options.password).toString('base64');
75
+ }
112
76
 
113
- const req = rawRequest(opts, async res => {
114
- const followRedirects = options.followRedirects ?? 3;
115
- if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && followRedirects > 0 && res.headers.location) {
116
- req.off('timeout', timeoutHandler);
117
- this.request({
118
- ...options,
119
- url: res.headers.location,
120
- followRedirects: followRedirects - 1
121
- }, token).then(resolve, reject);
122
- } else {
123
- const chunks: Uint8Array[] = [];
124
-
125
- const stream = res.headers['content-encoding'] === 'gzip' ? res.pipe(createGunzip()) : res;
126
-
127
- stream.on('data', chunk => {
128
- chunks.push(chunk);
129
- });
130
-
131
- stream.on('end', () => {
132
- req.off('timeout', timeoutHandler);
133
- const buffer = Buffer.concat(chunks);
134
- resolve({
135
- url: options.url,
136
- res: {
137
- headers: res.headers as Headers,
138
- statusCode: res.statusCode
139
- },
140
- buffer
141
- });
142
- });
143
-
144
- stream.on('error', err => {
145
- reject(err);
146
- });
147
- }
148
- });
77
+ const signals: AbortSignal[] = [];
78
+ if (options.timeout) {
79
+ signals.push(AbortSignal.timeout(options.timeout));
80
+ }
149
81
 
150
- req.on('error', err => {
151
- reject(err);
82
+ let tokenAbortController: AbortController | undefined;
83
+ let cancellationListener: void | { dispose(): void } | undefined;
84
+ if (token) {
85
+ tokenAbortController = new AbortController();
86
+ signals.push(tokenAbortController.signal);
87
+ cancellationListener = token.onCancellationRequested(() => {
88
+ tokenAbortController!.abort();
152
89
  });
90
+ }
153
91
 
154
- req.on('timeout', timeoutHandler);
92
+ const signal = signals.length > 0
93
+ ? (signals.length === 1 ? signals[0] : AbortSignal.any(signals))
94
+ : undefined;
155
95
 
156
- if (options.timeout) {
157
- req.setTimeout(options.timeout);
158
- }
96
+ const fetchOptions: RequestInit & { dispatcher?: Dispatcher } = {
97
+ method: options.type || 'GET',
98
+ headers,
99
+ redirect: options.followRedirects === 0 ? 'manual' : 'follow',
100
+ signal,
101
+ };
159
102
 
160
- if (options.data) {
161
- req.write(options.data);
162
- }
103
+ if (options.dispatcher) {
104
+ fetchOptions.dispatcher = options.dispatcher;
105
+ }
163
106
 
164
- req.end();
107
+ if (options.data) {
108
+ fetchOptions.body = options.data;
109
+ }
110
+
111
+ try {
112
+ const response = await fetch(options.url, fetchOptions);
113
+ const arrayBuffer = await response.arrayBuffer();
114
+ const buffer = Buffer.from(arrayBuffer);
165
115
 
166
- token?.onCancellationRequested(() => {
167
- req.destroy();
168
- reject();
116
+ const responseHeaders: Headers = {};
117
+ response.headers.forEach((value, key) => {
118
+ responseHeaders[key] = value;
169
119
  });
170
- });
120
+
121
+ return {
122
+ url: options.url,
123
+ res: {
124
+ headers: responseHeaders,
125
+ statusCode: response.status
126
+ },
127
+ buffer
128
+ };
129
+ } finally {
130
+ if (cancellationListener) {
131
+ cancellationListener.dispose();
132
+ }
133
+ }
171
134
  }
172
135
 
173
136
  async resolveProxy(url: string): Promise<string | undefined> {
@@ -0,0 +1,94 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2026 EclipseSource GmbH.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the Eclipse Public License v. 2.0 which is available at
6
+ * http://www.eclipse.org/legal/epl-2.0.
7
+ *
8
+ * This Source Code may also be made available under the following Secondary
9
+ * Licenses when the conditions for such availability set forth in the Eclipse
10
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ * with the GNU Classpath Exception which is available at
12
+ * https://www.gnu.org/software/classpath/license.html.
13
+ *
14
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ ********************************************************************************/
16
+
17
+ import { expect } from 'chai';
18
+ import { getProxyAgent } from './proxy';
19
+
20
+ describe('proxy', () => {
21
+ describe('getProxyAgent', () => {
22
+ it('should return undefined when no proxy is configured', () => {
23
+ const agent = getProxyAgent('https://example.com', {});
24
+ expect(agent).to.be.undefined;
25
+ });
26
+
27
+ it('should return a dispatcher when proxy URL is provided via options', () => {
28
+ const agent = getProxyAgent('https://example.com', {}, {
29
+ proxyUrl: 'http://proxy.example.com:8080'
30
+ });
31
+ expect(agent).to.not.be.undefined;
32
+ });
33
+
34
+ it('should return a dispatcher when HTTP_PROXY env variable is set for http URLs', () => {
35
+ const env = { HTTP_PROXY: 'http://proxy.example.com:8080' };
36
+ const agent = getProxyAgent('http://example.com', env);
37
+ expect(agent).to.not.be.undefined;
38
+ });
39
+
40
+ it('should return a dispatcher when http_proxy env variable is set for http URLs', () => {
41
+ const env = { http_proxy: 'http://proxy.example.com:8080' };
42
+ const agent = getProxyAgent('http://example.com', env);
43
+ expect(agent).to.not.be.undefined;
44
+ });
45
+
46
+ it('should return a dispatcher when HTTPS_PROXY env variable is set for https URLs', () => {
47
+ const env = { HTTPS_PROXY: 'http://proxy.example.com:8080' };
48
+ const agent = getProxyAgent('https://example.com', env);
49
+ expect(agent).to.not.be.undefined;
50
+ });
51
+
52
+ it('should fall back to HTTP_PROXY for https URLs when HTTPS_PROXY is not set', () => {
53
+ const env = { HTTP_PROXY: 'http://proxy.example.com:8080' };
54
+ const agent = getProxyAgent('https://example.com', env);
55
+ expect(agent).to.not.be.undefined;
56
+ });
57
+
58
+ it('should return undefined for invalid request URLs', () => {
59
+ const agent = getProxyAgent('not-a-url', {}, {
60
+ proxyUrl: 'http://proxy.example.com:8080'
61
+ });
62
+ expect(agent).to.be.undefined;
63
+ });
64
+
65
+ it('should return undefined for non-http proxy protocols', () => {
66
+ const agent = getProxyAgent('https://example.com', {}, {
67
+ proxyUrl: 'socks5://proxy.example.com:1080'
68
+ });
69
+ expect(agent).to.be.undefined;
70
+ });
71
+
72
+ it('should return undefined for invalid proxy URLs', () => {
73
+ const agent = getProxyAgent('https://example.com', {}, {
74
+ proxyUrl: 'not-a-valid-url'
75
+ });
76
+ expect(agent).to.be.undefined;
77
+ });
78
+
79
+ it('should return an Agent (not ProxyAgent) when strictSSL is false and no proxy is set', () => {
80
+ const agent = getProxyAgent('https://example.com', {}, {
81
+ strictSSL: false
82
+ });
83
+ expect(agent).to.not.be.undefined;
84
+ });
85
+
86
+ it('should prefer proxyUrl option over environment variables', () => {
87
+ const env = { HTTP_PROXY: 'http://env-proxy.example.com:8080' };
88
+ const agent = getProxyAgent('http://example.com', env, {
89
+ proxyUrl: 'http://option-proxy.example.com:9090'
90
+ });
91
+ expect(agent).to.not.be.undefined;
92
+ });
93
+ });
94
+ });
package/src/proxy.ts CHANGED
@@ -14,13 +14,9 @@
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
16
 
17
- import { parse as parseUrl, Url } from 'url';
18
- import * as httpAgent from 'http-proxy-agent';
19
- import * as httpsAgent from 'https-proxy-agent';
17
+ import { ProxyAgent, Agent, Dispatcher } from 'undici';
20
18
 
21
- export type ProxyAgent = httpAgent.HttpProxyAgent | httpsAgent.HttpsProxyAgent;
22
-
23
- function getSystemProxyURI(requestURL: Url, env: typeof process.env): string | undefined {
19
+ function getSystemProxyURI(requestURL: URL, env: typeof process.env): string | undefined {
24
20
  if (requestURL.protocol === 'http:') {
25
21
  return env.HTTP_PROXY || env.http_proxy;
26
22
  } else if (requestURL.protocol === 'https:') {
@@ -35,27 +31,46 @@ export interface ProxySettings {
35
31
  strictSSL?: boolean;
36
32
  }
37
33
 
38
- export function getProxyAgent(rawRequestURL: string, env: typeof process.env, options: ProxySettings = {}): ProxyAgent | undefined {
39
- const requestURL = parseUrl(rawRequestURL);
34
+ export function getProxyAgent(rawRequestURL: string, env: typeof process.env, options: ProxySettings = {}): Dispatcher | undefined {
35
+ let requestURL: URL;
36
+ try {
37
+ requestURL = new URL(rawRequestURL);
38
+ } catch {
39
+ return undefined;
40
+ }
41
+
40
42
  const proxyURL = options.proxyUrl || getSystemProxyURI(requestURL, env);
41
43
 
42
44
  if (!proxyURL) {
45
+ if (options.strictSSL === false) {
46
+ return new Agent({
47
+ connect: { rejectUnauthorized: false }
48
+ });
49
+ }
43
50
  return undefined;
44
51
  }
45
52
 
46
- const proxyEndpoint = parseUrl(proxyURL);
53
+ let proxyEndpoint: URL;
54
+ try {
55
+ proxyEndpoint = new URL(proxyURL);
56
+ } catch {
57
+ return undefined;
58
+ }
47
59
 
48
- if (!/^https?:$/.test(proxyEndpoint.protocol || '')) {
60
+ if (!/^https?:$/.test(proxyEndpoint.protocol)) {
49
61
  return undefined;
50
62
  }
51
63
 
52
- const opts = {
53
- host: proxyEndpoint.hostname || '',
54
- port: proxyEndpoint.port || (proxyEndpoint.protocol === 'https' ? '443' : '80'),
55
- auth: proxyEndpoint.auth,
56
- rejectUnauthorized: !!options.strictSSL,
64
+ const proxyAgentOptions: ProxyAgent.Options = {
65
+ uri: proxyURL,
66
+ token: proxyEndpoint.username
67
+ ? `Basic ${Buffer.from(`${decodeURIComponent(proxyEndpoint.username)}:${decodeURIComponent(proxyEndpoint.password)}`).toString('base64')}`
68
+ : undefined,
57
69
  };
58
70
 
59
- const createAgent = requestURL.protocol === 'http:' ? httpAgent : httpsAgent;
60
- return createAgent(opts);
71
+ if (options.strictSSL === false) {
72
+ proxyAgentOptions.requestTls = { rejectUnauthorized: false };
73
+ }
74
+
75
+ return new ProxyAgent(proxyAgentOptions);
61
76
  }
@@ -1 +0,0 @@
1
- {"version":3,"file":"package.spec.d.ts","sourceRoot":"","sources":["../src/package.spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;kFAckF"}
@@ -1,26 +0,0 @@
1
- /********************************************************************************
2
- * Copyright (C) 2022 TypeFox and others.
3
- *
4
- * This program and the accompanying materials are made available under the
5
- * terms of the Eclipse Public License v. 2.0 which is available at
6
- * http://www.eclipse.org/legal/epl-2.0.
7
- *
8
- * This Source Code may also be made available under the following Secondary
9
- * Licenses when the conditions for such availability set forth in the Eclipse
10
- * Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- * with the GNU Classpath Exception which is available at
12
- * https://www.gnu.org/software/classpath/license.html.
13
- *
14
- * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- ********************************************************************************/
16
- /* note: this bogus test file is required so that
17
- we are able to run mocha unit tests on this
18
- package, without having any actual unit tests in it.
19
- This way a coverage report will be generated,
20
- showing 0% coverage, instead of no report.
21
- This file can be removed once we have real unit
22
- tests in place. */
23
- describe('request package', () => {
24
- it('should support code coverage statistics', () => true);
25
- });
26
- //# sourceMappingURL=package.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"package.spec.js","sourceRoot":"","sources":["../src/package.spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;kFAckF;AAElF;;;;;;qBAMqB;AAErB,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAE7B,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;AAC9D,CAAC,CAAC,CAAC"}