The JWProxy (JSON Wire Protocol Proxy) class provides functionality for forwarding WebDriver commands to other servers, enabling driver composition and command delegation. This is useful for hybrid drivers that need to proxy certain commands to other WebDriver implementations.
import { JWProxy } from "appium-base-driver";import { JWProxy, BaseDriver } from "appium-base-driver";
class HybridDriver extends BaseDriver {
constructor(opts) {
super(opts);
// Create proxy to forward web commands to ChromeDriver
this.chromeProxy = new JWProxy({
scheme: 'http',
server: 'localhost',
port: 9515,
base: ''
});
}
async createSession(jwpDesiredCaps, jwpRequiredCaps, w3cCapabilities) {
const caps = await super.createSession(jwpDesiredCaps, jwpRequiredCaps, w3cCapabilities);
// Start ChromeDriver session for web context
if (caps.browserName) {
const webCaps = { browserName: caps.browserName };
const proxyResponse = await this.chromeProxy.command('/session', 'POST', webCaps);
this.webSessionId = proxyResponse.sessionId;
}
return caps;
}
async findElement(strategy, selector) {
if (this.isWebContext && this.webSessionId) {
// Proxy web element finding to ChromeDriver
return await this.chromeProxy.command(
`/session/${this.webSessionId}/element`,
'POST',
{ using: strategy, value: selector }
);
}
// Handle native element finding
return await super.findElement(strategy, selector);
}
}class JWProxy {
constructor(opts = {});
}Parameters:
opts (object, optional): Proxy configuration options
scheme (string): Protocol scheme ('http' or 'https', default: 'http')server (string): Target server hostname (default: 'localhost')port (number): Target server port (default: 4444)base (string): Base path for requests (default: '/wd/hub')timeout (number): Request timeout in milliseconds (default: 240000)sessionId (string): Default session ID for requestsProperties:
// Configuration properties
proxy.scheme; // 'http' or 'https'
proxy.server; // Target hostname
proxy.port; // Target port number
proxy.base; // Base URL path
proxy.timeout; // Request timeout in ms
proxy.sessionId; // Current session IDExample Configuration:
// Proxy to Selenium Grid
const gridProxy = new JWProxy({
scheme: 'http',
server: 'selenium-hub.example.com',
port: 4444,
base: '/wd/hub',
timeout: 60000
});
// Proxy to local ChromeDriver
const chromeProxy = new JWProxy({
scheme: 'http',
server: '127.0.0.1',
port: 9515,
base: '',
timeout: 30000
});async command(url, method, body);Executes a WebDriver command via the proxy server.
Parameters:
url (string): Command endpoint URL (relative to base)method (string): HTTP method ('GET', 'POST', 'DELETE')body (object, optional): Request body for POST requestsReturns: Promise<any> - Command response from proxied server
Example:
const proxy = new JWProxy({ server: 'localhost', port: 9515 });
// Create session
const sessionResponse = await proxy.command('/session', 'POST', {
desiredCapabilities: { browserName: 'chrome' }
});
// Find element
const element = await proxy.command(
`/session/${sessionResponse.sessionId}/element`,
'POST',
{ using: 'id', value: 'my-button' }
);
// Click element
await proxy.command(
`/session/${sessionResponse.sessionId}/element/${element.ELEMENT}/click`,
'POST'
);
// Delete session
await proxy.command(`/session/${sessionResponse.sessionId}`, 'DELETE');async proxy(url, method, body);Lower-level proxy method that forwards requests directly to the target server.
Parameters:
url (string): Full or relative URLmethod (string): HTTP methodbody (object, optional): Request bodyReturns: Promise<any> - Raw response from target server
proxyReqRes(req, res);Proxies Express request and response objects directly to the target server. Useful for middleware-based proxying.
Parameters:
req (express.Request): Express request objectres (express.Response): Express response objectExample:
import express from 'express';
import { JWProxy } from "appium-base-driver";
const app = express();
const proxy = new JWProxy({ server: 'localhost', port: 9515 });
// Proxy all requests to /web/* to ChromeDriver
app.use('/web/*', (req, res) => {
// Modify URL to remove /web prefix
req.url = req.url.replace('/web', '');
proxy.proxyReqRes(req, res);
});getActiveRequestsCount();
cancelActiveRequests();getActiveRequestsCount() - Returns the number of currently active requestscancelActiveRequests() - Cancels all pending requestsExample:
const proxy = new JWProxy({ server: 'localhost', port: 9515 });
// Start multiple async operations
proxy.command('/session', 'POST', caps);
proxy.command('/status', 'GET');
proxy.command('/sessions', 'GET');
console.log(`Active requests: ${proxy.getActiveRequestsCount()}`); // 3
// Cancel all pending requests (e.g., during shutdown)
await proxy.cancelActiveRequests();
console.log(`Active requests: ${proxy.getActiveRequestsCount()}`); // 0getUrlForProxy(url);Constructs the full URL for proxying based on the proxy configuration.
Parameters:
url (string): Relative URL or endpointReturns: string - Full URL for the proxy request
Example:
const proxy = new JWProxy({
scheme: 'https',
server: 'remote-grid.com',
port: 443,
base: '/wd/hub'
});
const fullUrl = proxy.getUrlForProxy('/session');
// Returns: 'https://remote-grid.com:443/wd/hub/session'endpointRequiresSessionId(endpoint);
getSessionIdFromUrl(url);endpointRequiresSessionId() - Checks if an endpoint requires a session IDgetSessionIdFromUrl() - Extracts session ID from a URLExample:
const proxy = new JWProxy();
// Check if endpoint needs session ID
const needsSession = proxy.endpointRequiresSessionId('/session/abc123/element');
// Returns: true
const noSession = proxy.endpointRequiresSessionId('/status');
// Returns: false
// Extract session ID from URL
const sessionId = proxy.getSessionIdFromUrl('/session/abc123/element');
// Returns: 'abc123'import { BaseDriver, JWProxy } from "appium-base-driver";
class SelectiveProxyDriver extends BaseDriver {
constructor(opts) {
super(opts);
this.webProxy = new JWProxy({ port: 9515 }); // ChromeDriver
this.nativeProxy = new JWProxy({ port: 8200 }); // Native driver
}
// Commands to proxy to different servers
getProxyForCommand(cmd) {
const webCommands = ['findElement', 'click', 'sendKeys', 'executeScript'];
const nativeCommands = ['getScreenshot', 'shake', 'lock'];
if (webCommands.includes(cmd)) {
return this.webProxy;
} else if (nativeCommands.includes(cmd)) {
return this.nativeProxy;
}
return null; // Handle locally
}
async executeCommand(cmd, ...args) {
const proxy = this.getProxyForCommand(cmd);
if (proxy) {
// Build URL based on command and arguments
const url = this.buildCommandUrl(cmd, args);
const method = this.getMethodForCommand(cmd);
const body = this.buildCommandBody(cmd, args);
return await proxy.command(url, method, body);
}
// Execute locally
return await super.executeCommand(cmd, ...args);
}
}import { BaseDriver, JWProxy } from "appium-base-driver";
class MiddlewareProxyDriver extends BaseDriver {
constructor(opts) {
super(opts);
this.proxy = new JWProxy(opts.proxyConfig);
}
async proxyCommand(url, method, body) {
// Pre-proxy middleware
const modifiedBody = this.preprocessRequest(body);
try {
const response = await this.proxy.command(url, method, modifiedBody);
// Post-proxy middleware
return this.postprocessResponse(response);
} catch (err) {
// Error handling middleware
throw this.handleProxyError(err);
}
}
preprocessRequest(body) {
// Modify request before sending to proxy
if (body && body.desiredCapabilities) {
body.desiredCapabilities.proxyDriver = this.constructor.name;
}
return body;
}
postprocessResponse(response) {
// Modify response after receiving from proxy
if (response.value && response.value.message) {
response.value.message = `[Proxied] ${response.value.message}`;
}
return response;
}
handleProxyError(err) {
console.log('Proxy error:', err.message);
// Convert to appropriate protocol error
return new errors.UnknownError(`Proxy command failed: ${err.message}`);
}
}import { BaseDriver, JWProxy } from "appium-base-driver";
class MultiServerDriver extends BaseDriver {
constructor(opts) {
super(opts);
// Multiple proxy instances for different services
this.proxies = {
chrome: new JWProxy({ port: 9515 }),
firefox: new JWProxy({ port: 4444, base: '/wd/hub' }),
edge: new JWProxy({ port: 17556 }),
appium: new JWProxy({ port: 4723, base: '/wd/hub' })
};
this.currentProxy = null;
}
async createSession(jwpDesiredCaps, jwpRequiredCaps, w3cCapabilities) {
const caps = await super.createSession(jwpDesiredCaps, jwpRequiredCaps, w3cCapabilities);
// Select proxy based on capabilities
if (caps.browserName === 'chrome') {
this.currentProxy = this.proxies.chrome;
} else if (caps.browserName === 'firefox') {
this.currentProxy = this.proxies.firefox;
} else if (caps.platformName) {
this.currentProxy = this.proxies.appium;
}
// Create session on selected proxy
if (this.currentProxy) {
const proxyResponse = await this.currentProxy.command('/session', 'POST', {
desiredCapabilities: caps
});
this.proxySessionId = proxyResponse.sessionId;
}
return caps;
}
async deleteSession() {
if (this.currentProxy && this.proxySessionId) {
await this.currentProxy.command(`/session/${this.proxySessionId}`, 'DELETE');
}
return await super.deleteSession();
}
}The JWProxy class provides robust error handling for network issues, timeouts, and server errors:
import { JWProxy, errors } from "appium-base-driver";
const proxy = new JWProxy({ server: 'localhost', port: 9515, timeout: 30000 });
try {
const response = await proxy.command('/session', 'POST', capabilities);
} catch (err) {
if (err.code === 'ECONNREFUSED') {
throw new errors.SessionNotCreatedError('Cannot connect to proxy server');
} else if (err.code === 'ETIMEDOUT') {
throw new errors.TimeoutError('Proxy request timed out');
} else {
throw new errors.ProxyRequestError(`Proxy error: ${err.message}`);
}
}The JWProxy class enables powerful driver composition patterns, allowing Appium drivers to delegate specific functionality to specialized WebDriver implementations while maintaining a unified interface.