fix(webdriver): re-enable undici global dispatcher (#14623)

This commit is contained in:
Christian Bromann 2025-07-08 16:22:09 -07:00 committed by GitHub
parent ea9d91050e
commit 80cfd4a3d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 183 additions and 7 deletions

View file

@ -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

View file

@ -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 = []
})
})

View file

@ -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