fix(appium): fix interaction of plugins with proxying
Previously, you could not 'await next()' in a plugin and trigger the default proxy behaviour.
Now this is possible.
MODIFY
packages/appium/lib/appium.js
packages/appium/lib/appium.js
@@ -451,6 +451,15 @@ class AppiumDriver extends BaseDriver {
const isUmbrellaCmd = !isGetStatus && isAppiumDriverCommand(cmd);
const isSessionCmd = !isGetStatus && !isUmbrellaCmd;
+ // if a plugin override proxying for this command and that is why we are here instead of just
+ // letting the protocol proxy the command entirely, determine that, get the request object for
+ // use later on, then clean up the args
+ const reqForProxy = _.last(args)?.reqForProxy;
+ if (reqForProxy) {
+ args.pop();
+ }
+
+
// first do some error checking. If we're requesting a session command execution, then make
// sure that session actually exists on the session driver, and set the session driver itself
let sessionId = null;
@@ -492,6 +501,19 @@ class AppiumDriver extends BaseDriver {
// if we make it here, we know that the default behavior is handled
cmdHandledBy.default = true;
+ if (reqForProxy) {
+ // we would have proxied this command had a plugin not handled it, so the default behavior
+ // is to do the proxy and retrieve the result internally so it can be passed to the plugin
+ // in case it calls 'await next()'. This requires that the driver have defined
+ // 'proxyCommand' and not just 'proxyReqRes'.
+ if (!dstSession.proxyCommand) {
+ throw new Error(`The default behavior for this command was to proxy, but the driver ` +
+ `did not have the 'proxyCommand' method defined. To fully support ` +
+ `plugins, drivers should have 'proxyCommand' set to a jwpProxy object's ` +
+ `'command()' method, in addition to the normal 'proxyReqRes'`);
+ }
+ return await dstSession.proxyCommand(reqForProxy.originalUrl, reqForProxy.method, reqForProxy.body);
+ }
if (isGetStatus) {
return await this.getStatus();
@@ -596,26 +618,6 @@ class AppiumDriver extends BaseDriver {
return res;
}
- // We want to override basedriver's proxy avoidance detection since plugins might want to avoid
- // proxying in order to handle a command, so we use this function to make an extra check whether
- // plugins might want to do this.
- proxyRouteIsAvoided (sessionId, method, url, body) {
- if (super.proxyRouteIsAvoided(sessionId, method, url, body)) {
- return true;
- }
- // if even just one plugin needs to avoid proxying, do so
- for (const plugin of this.pluginsForSession(sessionId)) {
- if (plugin.shouldAvoidProxy?.(method, url, body)) {
- log.info(`Request to ${url} would normally be proxied, but plugin ${plugin.name} wants ` +
- `to handle it internally. We'll avoid proxying in this case.`);
- return true;
- }
- }
- return false;
- }
-
-
-
proxyActive (sessionId) {
const dstSession = this.sessions[sessionId];
return dstSession && _.isFunction(dstSession.proxyActive) && dstSession.proxyActive(sessionId);

MODIFY
packages/appium/test/plugin-e2e-specs.js
packages/appium/test/plugin-e2e-specs.js
@@ -60,7 +60,7 @@ describe('FakePlugin', function () {
installType: INSTALL_TYPE_LOCAL,
}, PLUGIN_TYPE);
}
- baseArgs = {port: testPort, host: TEST_HOST, appiumHome, usePlugins: ['fake']};
+ baseArgs = {port: testPort, host: TEST_HOST, appiumHome, usePlugins: ['fake'], useDrivers: ['fake']};
});
describe('without plugin registered', function () {
@@ -105,7 +105,7 @@ describe('FakePlugin', function () {
before(async function () {
// then start server if we need to
const usePlugins = registrationType === 'explicit' ? ['fake', 'p2', 'p3'] : ['all'];
- const args = {port: testPort, host: TEST_HOST, appiumHome, usePlugins};
+ const args = {port: testPort, host: TEST_HOST, appiumHome, usePlugins, useDrivers: ['fake']};
server = await appiumServer(args);
});
after(async function () {
@@ -149,6 +149,19 @@ describe('FakePlugin', function () {
await driver.deleteSession();
}
});
+
+ it('should allow original command to be proxied if supported', async function () {
+ const driver = await wdio(wdOpts);
+ const {sessionId} = driver;
+ try {
+ await axios.post(`${baseUrl}/${sessionId}/context`, {name: 'PROXY'});
+ const handle = (await axios.get(`${baseUrl}/${sessionId}/window/handle`)).data.value;
+ handle.should.eql('<<proxied via proxyCommand>>');
+ } finally {
+ await axios.post(`${baseUrl}/${sessionId}/context`, {name: 'NATIVE_APP'});
+ await driver.deleteSession();
+ }
+ });
});
}
describe('cli args handling for plugin args', function () {

MODIFY
packages/base-driver/lib/basedriver/driver.js
packages/base-driver/lib/basedriver/driver.js
@@ -306,6 +306,7 @@ class BaseDriver extends Protocol {
async executeCommand (cmd, ...args) {
// get start time for this command, and log in special cases
let startTime = Date.now();
+
if (cmd === 'createSession') {
// If creating a session determine if W3C or MJSONWP protocol was requested and remember the choice
this.protocol = determineProtocol(...args);

MODIFY
packages/base-driver/lib/protocol/protocol.js
packages/base-driver/lib/protocol/protocol.js
@@ -239,10 +239,18 @@ function buildHandler (app, method, path, spec, driver, isSessCmd) {
// this command should never be proxied (which is useful for plugin developers who add
// commands and generally would not want that command to be proxied instead of handled by the
// plugin)
+ let pluginOverrodeProxy = false;
if (isSessCmd && !spec.neverProxy && driverShouldDoJwpProxy(driver, req, spec.command)) {
+ if (!driver.pluginsToHandleCmd ||
+ driver.pluginsToHandleCmd(spec.command, req.params.sessionId).length === 0) {
await doJwpProxy(driver, req, res);
return;
}
+ SESSIONS_CACHE.getLogger(req.params.sessionId, currentProtocol).debug(`Would have proxied ` +
+ `command directly, but a plugin exists which might require its value, so will let ` +
+ `its value be collected internally and made part of plugin chain`);
+ pluginOverrodeProxy = true;
+ }
// if a command is not in our method map, it's because we
// have no plans to ever implement it
@@ -283,6 +291,13 @@ function buildHandler (app, method, path, spec, driver, isSessCmd) {
`${driver.constructor.name}.${spec.command}() with args: ` +
_.truncate(JSON.stringify(args), {length: MAX_LOG_BODY_LENGTH}));
+ if (pluginOverrodeProxy) {
+ // TODO for now we add this information on the args list, but that's mixing purposes here.
+ // We really should add another 'options' parameter to 'executeCommand', but this would be
+ // a breaking change for all drivers so would need to be handled carefully.
+ args.push({reqForProxy: req});
+ }
+
driverRes = await driver.executeCommand(spec.command, ...args);
// Get the protocol after executeCommand

fix(appium): fix interaction of plugins with proxying
Previously, you could not 'await next()' in a plugin and trigger the default proxy behaviour.
Now this is possible.