fix(webdriver): re-enable undici global dispatcher (#14623)
This commit is contained in:
parent
ea9d91050e
commit
80cfd4a3d4
3 changed files with 183 additions and 7 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import dns from 'node:dns'
|
||||
import { fetch, Agent, type RequestInit as UndiciRequestInit, ProxyAgent, type Dispatcher } from 'undici'
|
||||
import { fetch, Agent, type RequestInit as UndiciRequestInit, ProxyAgent, type Dispatcher, getGlobalDispatcher } from 'undici'
|
||||
|
||||
import { environment } from '../environment.js'
|
||||
import { WebDriverRequest } from './request.js'
|
||||
|
|
@ -35,6 +35,28 @@ export class FetchRequest extends WebDriverRequest {
|
|||
}
|
||||
|
||||
/**
|
||||
* First check if a global dispatcher has been set (e.g. via setGlobalDispatcher)
|
||||
*/
|
||||
try {
|
||||
const globalDispatcher = getGlobalDispatcher()
|
||||
|
||||
// Check if the global dispatcher appears to be customized
|
||||
// The default dispatcher is typically an Agent, but we check if it's
|
||||
// a ProxyAgent or has proxy-specific properties
|
||||
if (globalDispatcher && (
|
||||
globalDispatcher.constructor.name === 'ProxyAgent' ||
|
||||
'proxy' in globalDispatcher ||
|
||||
// Also check for other custom dispatcher types
|
||||
(globalDispatcher.constructor.name !== 'Agent' && globalDispatcher.constructor.name !== 'MockAgent')
|
||||
)) {
|
||||
return globalDispatcher
|
||||
}
|
||||
} catch {
|
||||
// If getGlobalDispatcher fails for any reason, fall back to env vars
|
||||
}
|
||||
|
||||
/**
|
||||
* Fall back to creating a dispatcher based on environment variables
|
||||
* Use a proxy agent if we have a proxy url set
|
||||
*/
|
||||
const { PROXY_URL, NO_PROXY } = environment.value.variables
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import path from 'node:path'
|
|||
|
||||
import logger from '@wdio/logger'
|
||||
import type { Options } from '@wdio/types'
|
||||
import { getGlobalDispatcher, ProxyAgent, Agent } from 'undici'
|
||||
|
||||
import '../src/browser.js'
|
||||
import { FetchRequest as WebFetchRequest } from '../src/request/web.js'
|
||||
import { FetchRequest, SESSION_DISPATCHERS } from '../src/request/node.js'
|
||||
import { environment } from '../src/environment.js'
|
||||
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
vi.mock('fetch')
|
||||
|
|
@ -16,6 +18,8 @@ vi.mock('undici', () => {
|
|||
fetch: vi.fn(async () => ({ ok: true, status: 200, json: async () => ({}) })),
|
||||
Agent: vi.fn().mockImplementation(() => ({ close: vi.fn() })),
|
||||
ProxyAgent: vi.fn().mockImplementation(() => ({ close: vi.fn() })),
|
||||
getGlobalDispatcher: vi.fn(),
|
||||
setGlobalDispatcher: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -25,14 +29,23 @@ const webdriverPath = '/session'
|
|||
const defaultOptions = {
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
port: 4444
|
||||
port: 4444,
|
||||
connectionRetryTimeout: 10000
|
||||
}
|
||||
const baseUrl = `${defaultOptions.protocol}://${defaultOptions.hostname}:${defaultOptions.port}`
|
||||
|
||||
describe('webdriver request', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(fetch).mockClear()
|
||||
SESSION_DISPATCHERS.clear()
|
||||
// Reset environment variables
|
||||
environment.value.variables.PROXY_URL = undefined
|
||||
environment.value.variables.NO_PROXY = []
|
||||
// Clear all mocks from undici
|
||||
const { getGlobalDispatcher, ProxyAgent, Agent } = vi.mocked(await import('undici'))
|
||||
vi.mocked(getGlobalDispatcher).mockClear()
|
||||
vi.mocked(ProxyAgent).mockClear()
|
||||
vi.mocked(Agent).mockClear()
|
||||
})
|
||||
|
||||
it('should have some default options', () => {
|
||||
|
|
@ -513,6 +526,117 @@ describe('webdriver request', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('proxy configuration', () => {
|
||||
beforeEach(() => {
|
||||
// Reset environment variables before each test
|
||||
environment.value.variables.PROXY_URL = undefined
|
||||
environment.value.variables.NO_PROXY = []
|
||||
vi.mocked(getGlobalDispatcher).mockReturnValue({
|
||||
close: vi.fn(),
|
||||
constructor: { name: 'ProxyAgent' }
|
||||
} as any)
|
||||
})
|
||||
|
||||
it('should use global dispatcher if set', async () => {
|
||||
const { getGlobalDispatcher, ProxyAgent } = await import('undici')
|
||||
const customDispatcher = { type: 'custom-proxy', close: vi.fn() }
|
||||
|
||||
// Mock getGlobalDispatcher to return a custom dispatcher
|
||||
vi.mocked(getGlobalDispatcher).mockReturnValue(customDispatcher as any)
|
||||
|
||||
const req = new FetchRequest('GET', '/test', {})
|
||||
const { requestOptions } = await req.createOptions(defaultOptions) as { requestOptions: any }
|
||||
|
||||
expect(requestOptions.dispatcher).toBe(customDispatcher)
|
||||
expect(ProxyAgent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should fall back to environment variables if no global dispatcher is set', async () => {
|
||||
// Mock getGlobalDispatcher to return a default Agent (meaning no custom global dispatcher)
|
||||
const defaultAgent = { type: 'default-agent', close: vi.fn(), constructor: { name: 'Agent' } }
|
||||
vi.mocked(Agent).mockReturnValue(defaultAgent as any)
|
||||
vi.mocked(getGlobalDispatcher).mockReturnValue(defaultAgent as any)
|
||||
|
||||
// Set proxy environment variable
|
||||
environment.value.variables.PROXY_URL = 'http://proxy.example.com:8080'
|
||||
|
||||
const req = new FetchRequest('GET', '/test', {})
|
||||
await req.createOptions(defaultOptions)
|
||||
|
||||
expect(ProxyAgent).toHaveBeenCalledWith({
|
||||
uri: 'http://proxy.example.com:8080',
|
||||
connectTimeout: defaultOptions.connectionRetryTimeout,
|
||||
headersTimeout: defaultOptions.connectionRetryTimeout,
|
||||
bodyTimeout: defaultOptions.connectionRetryTimeout,
|
||||
})
|
||||
})
|
||||
|
||||
it('should use environment proxy unless excluded by NO_PROXY', async () => {
|
||||
// Mock getGlobalDispatcher to return a default Agent
|
||||
const defaultAgent = { type: 'default-agent', close: vi.fn(), constructor: { name: 'Agent' } }
|
||||
vi.mocked(Agent).mockReturnValue(defaultAgent as any)
|
||||
vi.mocked(getGlobalDispatcher).mockReturnValue(defaultAgent as any)
|
||||
|
||||
environment.value.variables.PROXY_URL = 'http://proxy.example.com:8080'
|
||||
environment.value.variables.NO_PROXY = ['localhost', '.internal.com']
|
||||
|
||||
const req = new FetchRequest('GET', '/test', {})
|
||||
|
||||
// Reset mocks before tests
|
||||
vi.mocked(ProxyAgent).mockClear()
|
||||
vi.mocked(Agent).mockClear()
|
||||
|
||||
// Should use proxy for external host
|
||||
await req.createOptions({ ...defaultOptions, hostname: 'external.com' })
|
||||
expect(ProxyAgent).toHaveBeenCalledTimes(1)
|
||||
expect(Agent).toHaveBeenCalledTimes(0)
|
||||
|
||||
vi.mocked(ProxyAgent).mockClear()
|
||||
vi.mocked(Agent).mockClear()
|
||||
|
||||
// Should not use proxy for excluded host
|
||||
await req.createOptions({ ...defaultOptions, hostname: 'api.internal.com' })
|
||||
expect(ProxyAgent).not.toHaveBeenCalled()
|
||||
expect(Agent).toHaveBeenCalledTimes(1) // Once for global dispatcher mock, once for actual dispatcher
|
||||
})
|
||||
|
||||
it('should handle getGlobalDispatcher errors gracefully', async () => {
|
||||
// Mock getGlobalDispatcher to throw an error
|
||||
vi.mocked(getGlobalDispatcher).mockImplementation(() => {
|
||||
throw new Error('getGlobalDispatcher not available')
|
||||
})
|
||||
|
||||
environment.value.variables.PROXY_URL = 'http://proxy.example.com:8080'
|
||||
|
||||
const req = new FetchRequest('GET', '/test', {})
|
||||
await req.createOptions(defaultOptions)
|
||||
|
||||
// Should fall back to environment variables
|
||||
expect(ProxyAgent).toHaveBeenCalledWith({
|
||||
uri: 'http://proxy.example.com:8080',
|
||||
connectTimeout: defaultOptions.connectionRetryTimeout,
|
||||
headersTimeout: defaultOptions.connectionRetryTimeout,
|
||||
bodyTimeout: defaultOptions.connectionRetryTimeout,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not use proxy if neither global dispatcher nor env vars are set', async () => {
|
||||
// Mock getGlobalDispatcher to return a default Agent
|
||||
const defaultAgent = { type: 'default-agent', close: vi.fn(), constructor: { name: 'Agent' } }
|
||||
vi.mocked(Agent).mockReturnValue(defaultAgent as any)
|
||||
vi.mocked(getGlobalDispatcher).mockReturnValue(defaultAgent as any)
|
||||
|
||||
// Ensure no proxy environment variables are set
|
||||
environment.value.variables.PROXY_URL = undefined
|
||||
|
||||
const req = new FetchRequest('GET', '/test', {})
|
||||
await req.createOptions(defaultOptions)
|
||||
|
||||
expect(ProxyAgent).not.toHaveBeenCalled()
|
||||
expect(Agent).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-ignore
|
||||
vi.mocked(fetch).retryCnt = 0
|
||||
|
|
@ -522,5 +646,9 @@ describe('webdriver request', () => {
|
|||
vi.mocked(error).mockClear()
|
||||
|
||||
SESSION_DISPATCHERS.clear()
|
||||
|
||||
// Reset environment variables
|
||||
environment.value.variables.PROXY_URL = undefined
|
||||
environment.value.variables.NO_PROXY = []
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,22 +10,39 @@ You can tunnel two different types of request through a proxy:
|
|||
|
||||
## Proxy Between Driver And Test
|
||||
|
||||
If your company has a corporate proxy (e.g. on `http://my.corp.proxy.com:9090`) for all outgoing requests, follow the below steps to install and configure [undici](https://github.com/nodejs/undici).
|
||||
If your company has a corporate proxy (e.g. on `http://my.corp.proxy.com:9090`) for all outgoing requests, you have two options to configure WebdriverIO to use the proxy:
|
||||
|
||||
### Install undici
|
||||
### Option 1: Using Environment Variables (Recommended)
|
||||
|
||||
Starting from WebdriverIO v9.12.0, you can simply set the standard proxy environment variables:
|
||||
|
||||
```bash
|
||||
export HTTP_PROXY=http://my.corp.proxy.com:9090
|
||||
export HTTPS_PROXY=http://my.corp.proxy.com:9090
|
||||
# Optional: bypass proxy for certain hosts
|
||||
export NO_PROXY=localhost,127.0.0.1,.internal.domain
|
||||
```
|
||||
|
||||
Then run your tests as usual. WebdriverIO will automatically use these environment variables for proxy configuration.
|
||||
|
||||
### Option 2: Using undici's setGlobalDispatcher
|
||||
|
||||
For more advanced proxy configurations or if you need programmatic control, you can use undici's `setGlobalDispatcher` method:
|
||||
|
||||
#### Install undici
|
||||
|
||||
```bash npm2yarn
|
||||
npm install undici --save-dev
|
||||
```
|
||||
|
||||
### Add undici setGlobalDispatcher to your config file
|
||||
#### Add undici setGlobalDispatcher to your config file
|
||||
|
||||
Add the following require statement to the top of your config file.
|
||||
|
||||
```js title="wdio.conf.js"
|
||||
import { setGlobalDispatcher, ProxyAgent } from 'undici';
|
||||
|
||||
const dispatcher = new ProxyAgent({ uri: new URL(process.env.https_proxy).toString() });
|
||||
const dispatcher = new ProxyAgent({ uri: new URL(process.env.https_proxy || 'http://my.corp.proxy.com:9090').toString() });
|
||||
setGlobalDispatcher(dispatcher);
|
||||
|
||||
export const config = {
|
||||
|
|
@ -35,6 +52,15 @@ export const config = {
|
|||
|
||||
Additional information about configuring the proxy can be located [here](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md).
|
||||
|
||||
### Which Method Should I Use?
|
||||
|
||||
- **Use environment variables** if you want a simple, standard approach that works across different tools and doesn't require code changes.
|
||||
- **Use setGlobalDispatcher** if you need advanced proxy features like custom authentication, different proxy configurations per environment, or want to programmatically control proxy behavior.
|
||||
|
||||
Both methods are fully supported and WebdriverIO will check for a global dispatcher first before falling back to environment variables.
|
||||
|
||||
### Sauce Connect Proxy
|
||||
|
||||
If you use [Sauce Connect Proxy](https://docs.saucelabs.com/secure-connections/sauce-connect-5), start it via:
|
||||
|
||||
```sh
|
||||
|
|
|
|||
Loading…
Reference in a new issue