Skip to content

Commit 42cae8a

Browse files
authored
Merge pull request #236 from reportportal/develop
Release 5.5.0
2 parents a0fad3e + b8440ad commit 42cae8a

12 files changed

Lines changed: 954 additions & 19 deletions

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
### Added
2+
- Full http/https proxy support with `noProxy` configuration, check [Proxy configuration options](https://github.com/reportportal/client-javascript?tab=readme-ov-file#proxy-configuration-options) for more details.
13

24
## [5.4.3] - 2025-10-20
35
### Added
4-
- OAuth 2.0 Password Grant authentication, check [Authentication Options](https://github.com/reportportal/client-javascript?tab=readme-ov-file#authentication-options) for more details.
6+
- OAuth 2.0 Password Grant authentication, check [Authentication options](https://github.com/reportportal/client-javascript?tab=readme-ov-file#authentication-options) for more details.
57

68
## [5.4.2] - 2025-10-02
79
### Added

README.md

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ rpClient.checkConnect().then(() => {
5252

5353
When creating a client instance, you need to specify the following options.
5454

55-
### Authentication Options
55+
### Authentication options
5656

5757
The client supports two authentication methods:
5858
1. **API Key Authentication** (default)
@@ -67,7 +67,7 @@ Either API key or complete OAuth 2.0 configuration is required to connect to Rep
6767
| apiKey | Conditional | | User's ReportPortal API key from which you want to send requests. It can be found on the profile page of this user. *Required only if OAuth is not configured. |
6868
| oauth | Conditional | | OAuth 2.0 configuration object. When provided, OAuth authentication will be used instead of API key. See OAuth Configuration below. |
6969

70-
#### OAuth Configuration
70+
#### OAuth configuration
7171

7272
The `oauth` object supports the following properties:
7373

@@ -109,7 +109,7 @@ rpClient.checkConnect().then(() => {
109109
});
110110
```
111111

112-
### General Options
112+
### General options
113113

114114
| Option | Necessity | Default | Description |
115115
|-----------------------|------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -119,7 +119,7 @@ rpClient.checkConnect().then(() => {
119119
| headers | Optional | {} | The object with custom headers for internal http client. |
120120
| debug | Optional | false | This flag allows seeing the logs of the client. Useful for debugging. |
121121
| isLaunchMergeRequired | Optional | false | Allows client to merge launches into one at the end of the run via saving their UUIDs to the temp files at filesystem. At the end of the run launches can be merged using `mergeLaunches` method. Temp file format: `rplaunch-${launch_uuid}.tmp`. |
122-
| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. Use the `retry` property (number or [`axios-retry`](https://github.com/softonic/axios-retry#options) config) to customise automatic retries. |
122+
| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). Supports `proxy` and `noProxy` for proxy configuration (see [Proxy configuration](#proxy-configuration)), `agent` property for [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client options, `timeout`, `debug: true` for debugging, and `retry` property (number or [`axios-retry`](https://github.com/softonic/axios-retry#options) config) for automatic retries. |
123123
| launchUuidPrint | Optional | false | Whether to print the current launch UUID. |
124124
| launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR', 'FILE', 'ENVIRONMENT'. Works only if `launchUuidPrint` set to `true`. File format: `rp-launch-uuid-${launch_uuid}.tmp`. Env variable: `RP_LAUNCH_UUID`. |
125125
| token | Deprecated | Not set | Use `apiKey` or `oauth` instead. |
@@ -166,6 +166,169 @@ const client = new RPClient({
166166

167167
Setting `retry: 0` disables automatic retries.
168168

169+
### Proxy configuration
170+
171+
The client supports comprehensive proxy configuration for both HTTP and HTTPS requests, including ReportPortal API calls and OAuth token requests. Proxy settings can be configured via `restClientConfig` or environment variables.
172+
173+
#### Basic proxy configuration
174+
175+
##### Via configuration object
176+
177+
```javascript
178+
const RPClient = require('@reportportal/client-javascript');
179+
180+
const rpClient = new RPClient({
181+
apiKey: 'your_api_key',
182+
endpoint: 'http://your-instance.com:8080/api/v1',
183+
launch: 'LAUNCH_NAME',
184+
project: 'PROJECT_NAME',
185+
restClientConfig: {
186+
proxy: {
187+
protocol: 'https', // 'http' or 'https'
188+
host: '127.0.0.1',
189+
port: 8080,
190+
// Optional authentication
191+
auth: {
192+
username: 'proxy-user',
193+
password: 'proxy-password'
194+
}
195+
}
196+
}
197+
});
198+
```
199+
200+
##### Via proxy URL string
201+
202+
```javascript
203+
const rpClient = new RPClient({
204+
// ... other options
205+
restClientConfig: {
206+
proxy: 'https://127.0.0.1:8080'
207+
}
208+
});
209+
```
210+
211+
##### Via environment variables
212+
213+
The client automatically detects and uses proxy environment variables:
214+
215+
```bash
216+
export HTTPS_PROXY=https://127.0.0.1:8080
217+
export HTTP_PROXY=http://127.0.0.1:8080
218+
export NO_PROXY=localhost,127.0.0.1,.local
219+
```
220+
221+
#### Bypassing proxy for specific domains (noProxy)
222+
223+
Use the `noProxy` option to exclude specific domains from being proxied. This is useful when some services are accessible directly while others require a proxy.
224+
225+
```javascript
226+
const rpClient = new RPClient({
227+
// ... other options
228+
restClientConfig: {
229+
proxy: {
230+
protocol: 'https',
231+
host: '127.0.0.1',
232+
port: 8080
233+
},
234+
// Bypass proxy for these domains
235+
noProxy: 'localhost,127.0.0.1,internal.company.com,.local.domain'
236+
}
237+
});
238+
```
239+
240+
**noProxy format:**
241+
- Exact hostname: `example.com` - matches `example.com` and `sub.example.com`
242+
- Leading dot: `.example.com` - matches only subdomains like `sub.example.com` (not `example.com` itself)
243+
- Wildcard: `*` - bypass proxy for all requests
244+
- Multiple entries: Comma-separated list
245+
246+
**Priority:** Configuration `noProxy` takes precedence over `NO_PROXY` environment variable.
247+
248+
#### Proxy with OAuth authentication
249+
250+
When using OAuth authentication, the proxy configuration is automatically applied to both:
251+
- OAuth token endpoint requests
252+
- ReportPortal API requests
253+
254+
```javascript
255+
const rpClient = new RPClient({
256+
endpoint: 'http://your-instance.com:8080/api/v1',
257+
project: 'PROJECT_NAME',
258+
oauth: {
259+
tokenEndpoint: 'https://login.microsoftonline.com/.../oauth2/v2.0/token',
260+
username: 'your-username',
261+
password: 'your-password',
262+
clientId: 'your-client-id'
263+
},
264+
restClientConfig: {
265+
proxy: {
266+
protocol: 'https',
267+
host: '127.0.0.1',
268+
port: 8080
269+
},
270+
// Example: Use proxy for OAuth, bypass for ReportPortal
271+
noProxy: 'your-instance.com'
272+
}
273+
});
274+
```
275+
276+
#### Advanced proxy scenarios
277+
278+
##### Disable proxy explicitly
279+
280+
```javascript
281+
restClientConfig: {
282+
proxy: false // Disable proxy even if environment variables are set
283+
}
284+
```
285+
286+
##### Debug proxy configuration
287+
288+
Enable debug mode to see detailed proxy decision logs:
289+
290+
```javascript
291+
restClientConfig: {
292+
proxy: { /* ... */ },
293+
noProxy: 'localhost,.local',
294+
debug: true // See proxy-related logs
295+
}
296+
```
297+
298+
Debug output example:
299+
```
300+
[ProxyHelper] getProxyConfig called:
301+
URL: https://login.microsoftonline.com/oauth2/v2.0/token
302+
Hostname: login.microsoftonline.com
303+
noProxy from config: localhost,.local
304+
Should bypass proxy: false
305+
[ProxyHelper] Creating proxy agent:
306+
URL: https://login.microsoftonline.com/oauth2/v2.0/token
307+
Protocol: https:
308+
Proxy URL: https://127.0.0.1:8080
309+
```
310+
311+
#### Proxy configuration options
312+
313+
| Option | Type | Description |
314+
|---------------------|------------------------------|-------------------------------------------------------------------------------------------------|
315+
| `proxy` | `false \| string \| object` | Proxy configuration. Can be `false` (disable), URL string, or configuration object (see below) |
316+
| `proxy.protocol` | `string` | Proxy protocol: `'http'` or `'https'` |
317+
| `proxy.host` | `string` | Proxy host address |
318+
| `proxy.port` | `number` | Proxy port number |
319+
| `proxy.auth` | `object` | Optional proxy authentication |
320+
| `proxy.auth.username` | `string` | Proxy username |
321+
| `proxy.auth.password` | `string` | Proxy password |
322+
| `noProxy` | `string` | Comma-separated list of domains to bypass proxy |
323+
324+
#### How proxy handling works
325+
326+
1. **Per-request proxy decision:** Each request (API or OAuth) determines its proxy configuration based on the target URL
327+
2. **noProxy checking:** URLs matching `noProxy` patterns bypass the proxy and connect directly
328+
3. **Default agents for bypassed URLs:** When a URL bypasses proxy, a default HTTP/HTTPS agent is used to prevent automatic proxy detection
329+
4. **Environment variable support:** `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are automatically detected and used if no explicit configuration is provided
330+
5. **Priority:** Explicit configuration takes precedence over environment variables
331+
169332
### checkConnect
170333

171334
`checkConnect` - asynchronous method for verifying the correctness of the client connection

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
5.4.3
1+
5.4.4-SNAPSHOT

__tests__/oauth.spec.js

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const axios = require('axios');
2+
const { HttpsProxyAgent } = require('https-proxy-agent');
23
const OAuthInterceptor = require('../lib/oauth');
34

45
jest.mock('axios', () => ({
@@ -47,9 +48,8 @@ describe('OAuthInterceptor', () => {
4748
expect(params.get('client_id')).toBe(baseConfig.clientId);
4849
expect(params.get('client_secret')).toBe(baseConfig.clientSecret);
4950
expect(params.get('scope')).toBe(baseConfig.scope);
50-
expect(config).toEqual({
51-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
52-
});
51+
expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' });
52+
expect(config.httpsAgent).toBeDefined(); // Default agent added
5353
expect(oauthInterceptor.refreshToken).toBe('refresh-123');
5454
expect(oauthInterceptor.tokenExpiresAt).toBe(baseTime + 120000);
5555

@@ -152,7 +152,10 @@ describe('OAuthInterceptor', () => {
152152

153153
it('logs debug messages only when debug mode is enabled', () => {
154154
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
155-
const oauthInterceptor = new OAuthInterceptor({ ...baseConfig, debug: true });
155+
const oauthInterceptor = new OAuthInterceptor({
156+
...baseConfig,
157+
restClientConfig: { debug: true },
158+
});
156159
oauthInterceptor.logDebug('message', { foo: 'bar' });
157160

158161
expect(consoleSpy).toHaveBeenCalledWith('[OAuth] message', { foo: 'bar' });
@@ -309,4 +312,75 @@ describe('OAuthInterceptor', () => {
309312
consoleErrorSpy.mockRestore();
310313
consoleWarnSpy.mockRestore();
311314
});
315+
316+
it('uses proxy configuration for token requests', async () => {
317+
const baseTime = 1700000700000;
318+
const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime);
319+
const configWithProxy = {
320+
...baseConfig,
321+
restClientConfig: {
322+
proxy: {
323+
protocol: 'https',
324+
host: '127.0.0.1',
325+
port: 9000,
326+
},
327+
},
328+
};
329+
const oauthInterceptor = new OAuthInterceptor(configWithProxy);
330+
axios.post.mockResolvedValue({
331+
data: {
332+
access_token: 'token-with-proxy',
333+
expires_in: 120,
334+
},
335+
});
336+
337+
const token = await oauthInterceptor.getAccessToken();
338+
339+
expect(token).toBe('token-with-proxy');
340+
expect(axios.post).toHaveBeenCalledTimes(1);
341+
const [url, , config] = axios.post.mock.calls[0];
342+
343+
expect(url).toBe(baseConfig.tokenEndpoint);
344+
expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' });
345+
expect(config.httpsAgent).toBeInstanceOf(HttpsProxyAgent);
346+
347+
nowSpy.mockRestore();
348+
});
349+
350+
it('bypasses proxy for token endpoint when in noProxy list', async () => {
351+
const baseTime = 1700000800000;
352+
const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime);
353+
const configWithNoProxy = {
354+
...baseConfig,
355+
restClientConfig: {
356+
proxy: {
357+
protocol: 'https',
358+
host: '127.0.0.1',
359+
port: 9000,
360+
},
361+
noProxy: 'auth.example.com',
362+
},
363+
};
364+
const oauthInterceptor = new OAuthInterceptor(configWithNoProxy);
365+
axios.post.mockResolvedValue({
366+
data: {
367+
access_token: 'token-no-proxy',
368+
expires_in: 120,
369+
},
370+
});
371+
372+
const token = await oauthInterceptor.getAccessToken();
373+
374+
expect(token).toBe('token-no-proxy');
375+
expect(axios.post).toHaveBeenCalledTimes(1);
376+
const [url, , config] = axios.post.mock.calls[0];
377+
378+
expect(url).toBe(baseConfig.tokenEndpoint);
379+
expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' });
380+
// Should use default agent, not proxy agent
381+
expect(config.httpsAgent).toBeDefined();
382+
expect(config.httpsAgent.constructor.name).toBe('Agent');
383+
384+
nowSpy.mockRestore();
385+
});
312386
});

0 commit comments

Comments
 (0)