or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

base-driver.mddevice-settings.mdexpress-server.mdindex.mdjsonwp-proxy.mdprotocol-errors.md
tile.json

jsonwp-proxy.mddocs/

JSONWP Proxy

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.

Core Imports

import { JWProxy } from "appium-base-driver";

Basic Usage

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);
  }
}

Constructor and Configuration

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 requests

Properties:

// 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 ID

Example 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
});

Core Proxy Methods

Command Execution

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 requests

Returns: 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');

Direct Proxy

async proxy(url, method, body);

Lower-level proxy method that forwards requests directly to the target server.

Parameters:

  • url (string): Full or relative URL
  • method (string): HTTP method
  • body (object, optional): Request body

Returns: Promise<any> - Raw response from target server

Express Request/Response Proxy

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 object
  • res (express.Response): Express response object

Example:

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);
});

Request Management

Active Request Tracking

getActiveRequestsCount();
cancelActiveRequests();
  • getActiveRequestsCount() - Returns the number of currently active requests
  • cancelActiveRequests() - Cancels all pending requests

Example:

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()}`); // 0

URL and Session Management

URL Construction

getUrlForProxy(url);

Constructs the full URL for proxying based on the proxy configuration.

Parameters:

  • url (string): Relative URL or endpoint

Returns: 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'

Session ID Handling

endpointRequiresSessionId(endpoint);
getSessionIdFromUrl(url);
  • endpointRequiresSessionId() - Checks if an endpoint requires a session ID
  • getSessionIdFromUrl() - Extracts session ID from a URL

Example:

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'

Advanced Proxy Patterns

Selective Command Proxying

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);
  }
}

Proxy with Middleware

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}`);
  }
}

Multi-Server Proxy Setup

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();
  }
}

Error Handling

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.