mirror of
https://github.com/appium/appium.git
synced 2026-10-17 04:41:24 +08:00
feat(base-driver): add httpServer.frontRouter for global extension middleware (#22632)
This commit is contained in:
@@ -477,9 +477,9 @@ capability.
|
||||
You probably don't normally need to update the Appium server object (which is an
|
||||
[Express](https://expressjs.com/) server having already been
|
||||
[configured](https://github.com/appium/appium/blob/master/packages/base-driver/lib/express/server.js)
|
||||
in a variety of ways). But, for example, you could add new Express middleware to the server to
|
||||
support your plugin's requirements. To update the server you must implement the `static async
|
||||
updateServer` method in your class. This method takes three parameters:
|
||||
in a variety of ways). But, for example, you could add new routes to the server to support your
|
||||
plugin's requirements. To update the server you must implement the `static async updateServer`
|
||||
method in your class. This method takes three parameters:
|
||||
|
||||
* `expressApp`: the Express app object
|
||||
* `httpServer`: the Node HTTP server object
|
||||
@@ -492,6 +492,25 @@ results you'll need to test! Warning: this should be considered an advanced feat
|
||||
knowledge of Express, as well as the care not to do anything that could affect the operation of
|
||||
other parts of the Appium server!
|
||||
|
||||
`updateServer` runs *after* Appium's own routes are registered, so `expressApp.use(...)` here
|
||||
won't see requests to paths Appium already owns — Express matches middleware and routes in
|
||||
registration order. For middleware that must see requests to those routes too (e.g. logging or
|
||||
auth), use `httpServer.frontRouter` instead: Appium mounts this
|
||||
[Router](https://expressjs.com/en/5x/api/router/) before any route is registered, so anything
|
||||
attached to it — even from inside `updateServer` — still runs ahead of route matching. Note this
|
||||
is still *after* Appium's own baseline middleware (CORS, WebSocket upgrade handling, body
|
||||
parsing, etc.), so it won't see requests Appium itself has already rejected, e.g. a CORS
|
||||
preflight failure or a body that failed to parse:
|
||||
|
||||
```js
|
||||
static async updateServer(expressApp, httpServer, cliArgs) {
|
||||
httpServer.frontRouter.use((req, res, next) => {
|
||||
console.log(`Incoming request: ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Handle unexpected session shutdown
|
||||
|
||||
When developing a plugin you may want to add some cleanup logic for when a session ends. You would
|
||||
|
||||
@@ -159,6 +159,11 @@ describe('FakePlugin w/ FakeDriver via HTTP', function () {
|
||||
// arg got through.
|
||||
assert.deepStrictEqual((await axios.post(`http://${TEST_HOST}:${port}/cliArgs`)).data.usePlugins, res);
|
||||
});
|
||||
it('should let updateServer intercept requests to routes Appium itself owns via httpServer.frontRouter', async function () {
|
||||
// /status is a built-in route, registered before updateServer runs
|
||||
const res = await axios.get(`http://${TEST_HOST}:${port}/status`);
|
||||
assert.strictEqual(res.headers['x-fake-plugin-pre-server'], 'true');
|
||||
});
|
||||
it('should modify the method map with new commands', async function () {
|
||||
const driver = await wdio(wdOpts as any);
|
||||
const {sessionId} = driver;
|
||||
|
||||
@@ -15,7 +15,7 @@ import type {
|
||||
} from '@appium/types';
|
||||
import bodyParser from 'body-parser';
|
||||
import express from 'express';
|
||||
import type {Express, RequestHandler} from 'express';
|
||||
import type {Express, RequestHandler, Router} from 'express';
|
||||
import methodOverride from 'method-override';
|
||||
|
||||
import {DEFAULT_BASE_PATH} from '../constants';
|
||||
@@ -74,6 +74,8 @@ export interface ConfigureServerOpts {
|
||||
extraMethodMap?: MethodMap<ExternalDriver>;
|
||||
webSocketsMapping?: StringRecord;
|
||||
useLegacyUpgradeHandler?: boolean;
|
||||
/** Router mounted before any route is registered; see {@linkcode AppiumServer.frontRouter}. */
|
||||
frontRouter: Router;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@@ -147,6 +149,7 @@ export async function server(opts: ServerOpts): Promise<AppiumServer> {
|
||||
webSocketsMapping: appiumServer.webSocketsMapping,
|
||||
useLegacyUpgradeHandler,
|
||||
registerTestPages,
|
||||
frontRouter: appiumServer.frontRouter,
|
||||
} as ConfigureServerInternalOpts);
|
||||
// allow extensions to update the app and http server objects
|
||||
for (const updater of serverUpdaters) {
|
||||
@@ -188,6 +191,7 @@ export function configureServer(opts: ConfigureServerOpts): void {
|
||||
extraMethodMap = {},
|
||||
webSocketsMapping = {},
|
||||
useLegacyUpgradeHandler = true,
|
||||
frontRouter,
|
||||
} = opts;
|
||||
const {registerTestPages} = opts as ConfigureServerInternalOpts;
|
||||
const basePath = normalizeBasePath(rawBasePath);
|
||||
@@ -221,6 +225,9 @@ export function configureServer(opts: ConfigureServerOpts): void {
|
||||
// set up start logging (which depends on bodyParser doing its thing)
|
||||
app.use(startLogFormatter);
|
||||
|
||||
// mounted before any route, so middleware added later (e.g. via `updateServer`) still runs first
|
||||
app.use(frontRouter);
|
||||
|
||||
addRoutes(app, {basePath, extraMethodMap});
|
||||
|
||||
// Express only passes errors to an error handler registered after the middleware
|
||||
@@ -307,6 +314,7 @@ function configureHttp({
|
||||
}: ConfigureHttpOpts): AppiumServer {
|
||||
const appiumServer = httpServer as unknown as AppiumServer;
|
||||
appiumServer.webSocketsMapping = {};
|
||||
appiumServer.frontRouter = express.Router();
|
||||
appiumServer.addWebSocketHandler = addWebSocketHandler;
|
||||
appiumServer.removeWebSocketHandler = removeWebSocketHandler;
|
||||
appiumServer.removeAllWebSocketHandlers = removeAllWebSocketHandlers;
|
||||
|
||||
@@ -9,6 +9,9 @@ import {configureServer, normalizeBasePath, server} from '../../../lib/express/s
|
||||
import {routeConfiguringFunction} from '../../../lib/protocol/protocol';
|
||||
import {registerTestPages} from '../../../lib/test-pages';
|
||||
|
||||
// stand-in for the router `configureHttp` normally creates
|
||||
const fakeFrontRouter = {} as any;
|
||||
|
||||
const newMethodMap = {
|
||||
'/session/:sessionId/fake': {
|
||||
GET: {command: 'fakeGet'},
|
||||
@@ -57,8 +60,8 @@ describe('server configuration', function () {
|
||||
it('should actually use the middleware', function () {
|
||||
const app = fakeApp() as any;
|
||||
const configureRoutes = () => {};
|
||||
configureServer({app, addRoutes: configureRoutes});
|
||||
assert.strictEqual(app.use.callCount, 11);
|
||||
configureServer({app, addRoutes: configureRoutes, frontRouter: fakeFrontRouter});
|
||||
assert.strictEqual(app.use.callCount, 12);
|
||||
assert.strictEqual(app.all.callCount, 0);
|
||||
});
|
||||
|
||||
@@ -66,8 +69,8 @@ describe('server configuration', function () {
|
||||
const app = fakeApp() as any;
|
||||
const configureRoutes = () => {};
|
||||
// @ts-expect-error registerTestPages is not normally used in this way
|
||||
configureServer({app, addRoutes: configureRoutes, registerTestPages});
|
||||
assert.strictEqual(app.use.callCount, 15);
|
||||
configureServer({app, addRoutes: configureRoutes, frontRouter: fakeFrontRouter, registerTestPages});
|
||||
assert.strictEqual(app.use.callCount, 16);
|
||||
assert.strictEqual(app.all.callCount, 4);
|
||||
});
|
||||
|
||||
@@ -76,8 +79,8 @@ describe('server configuration', function () {
|
||||
const app2 = fakeApp() as any;
|
||||
const driver = fakeDriver();
|
||||
const addRoutes = routeConfiguringFunction(driver as any);
|
||||
configureServer({app: app1, addRoutes});
|
||||
configureServer({app: app2, addRoutes, extraMethodMap: newMethodMap});
|
||||
configureServer({app: app1, addRoutes, frontRouter: fakeFrontRouter});
|
||||
configureServer({app: app2, addRoutes, frontRouter: fakeFrontRouter, extraMethodMap: newMethodMap});
|
||||
assert.strictEqual(app2.totalCount(), app1.totalCount() + 2);
|
||||
});
|
||||
|
||||
@@ -86,11 +89,47 @@ describe('server configuration', function () {
|
||||
const app2 = fakeApp() as any;
|
||||
const driver = fakeDriver();
|
||||
const addRoutes = routeConfiguringFunction(driver as any);
|
||||
configureServer({app: app1, addRoutes});
|
||||
configureServer({app: app2, addRoutes, extraMethodMap: [] as any});
|
||||
configureServer({app: app1, addRoutes, frontRouter: fakeFrontRouter});
|
||||
configureServer({app: app2, addRoutes, frontRouter: fakeFrontRouter, extraMethodMap: [] as any});
|
||||
assert.strictEqual(app2.totalCount(), app1.totalCount());
|
||||
});
|
||||
|
||||
it('should mount the front router before routes are registered', function () {
|
||||
const callOrder: string[] = [];
|
||||
const app = fakeApp() as any;
|
||||
app.use = sandbox.spy((mw: any) => {
|
||||
if (mw === fakeFrontRouter) {
|
||||
callOrder.push('frontRouter');
|
||||
}
|
||||
});
|
||||
const configureRoutes = () => callOrder.push('route');
|
||||
configureServer({app, addRoutes: configureRoutes, frontRouter: fakeFrontRouter});
|
||||
assert.deepStrictEqual(callOrder, ['frontRouter', 'route']);
|
||||
});
|
||||
|
||||
it('should let updateServer intercept requests to routes Appium owns via httpServer.frontRouter', async function () {
|
||||
const driver = fakeDriver();
|
||||
const addRoutes = routeConfiguringFunction(driver as any);
|
||||
// reaches httpServer.frontRouter from the existing updateServer hook
|
||||
const interceptingUpdater = async (_app: any, httpServer: any) => {
|
||||
httpServer.frontRouter.use((_req: any, res: any, next: any) => {
|
||||
res.set('x-pre-server', 'true');
|
||||
next();
|
||||
});
|
||||
};
|
||||
const _server = await server({
|
||||
routeConfiguringFunction: addRoutes,
|
||||
port,
|
||||
serverUpdaters: [interceptingUpdater],
|
||||
});
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/status`);
|
||||
assert.strictEqual(res.headers.get('x-pre-server'), 'true');
|
||||
} finally {
|
||||
await _server.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow plugins to update the server', async function () {
|
||||
const driver = fakeDriver();
|
||||
const _server = await server({
|
||||
|
||||
@@ -94,7 +94,7 @@ export class FakePlugin extends BasePlugin {
|
||||
|
||||
static async updateServer(
|
||||
expressApp: Application,
|
||||
_httpServer: AppiumServer,
|
||||
httpServer: AppiumServer,
|
||||
cliArgs: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
expressApp.all('/fake', FakePlugin.fakeRoute);
|
||||
@@ -102,6 +102,11 @@ export class FakePlugin extends BasePlugin {
|
||||
expressApp.all('/cliArgs', (req, res) => {
|
||||
res.send(JSON.stringify(cliArgs));
|
||||
});
|
||||
// global middleware, exercised via httpServer.frontRouter
|
||||
httpServer.frontRouter.use((_req, res, next) => {
|
||||
res.set('x-fake-plugin-pre-server', 'true');
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
async onIpcInit() {
|
||||
|
||||
@@ -41,8 +41,9 @@ describe('fake plugin', function () {
|
||||
|
||||
it('should update an express app with a fake route', async function () {
|
||||
const app = new FakeExpress();
|
||||
const httpServer = {frontRouter: {use: () => {}}};
|
||||
assert.rejects(() => app.get('/fake'));
|
||||
await FakePlugin.updateServer(app as any, {} as any, {});
|
||||
await FakePlugin.updateServer(app as any, httpServer as any, {});
|
||||
assert.equal(await app.get('/fake'), JSON.stringify({fake: 'fakeResponse'}));
|
||||
});
|
||||
|
||||
|
||||
@@ -323,6 +323,11 @@ export interface ExternalDriver<
|
||||
*/
|
||||
export interface DriverStatic<T extends Driver> {
|
||||
baseVersion: string;
|
||||
/**
|
||||
* Allows a driver to modify the Appium server instance. Routes are already registered by the
|
||||
* time this runs; for middleware that must see every request, use `httpServer.frontRouter`
|
||||
* (see {@linkcode AppiumServer.frontRouter}).
|
||||
*/
|
||||
updateServer?: UpdateServerCallback;
|
||||
newMethodMap?: MethodMap<T>;
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,9 @@ import type {Class, StringRecord} from './util';
|
||||
*/
|
||||
export interface PluginStatic<P extends Plugin> {
|
||||
/**
|
||||
* Allows a plugin to modify the Appium server instance.
|
||||
* Allows a plugin to modify the Appium server instance. Routes are already registered by the
|
||||
* time this runs; for middleware that must see every request, use `httpServer.frontRouter`
|
||||
* (`AppiumServerExtension` in `@appium/types`).
|
||||
*/
|
||||
updateServer?: UpdateServerCallback;
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {Server as HTTPServer} from 'node:http';
|
||||
|
||||
import type {Express} from 'express';
|
||||
import type {Express, Router} from 'express';
|
||||
import type {Server as WSServer} from 'ws';
|
||||
|
||||
import type {ServerArgs} from './config';
|
||||
@@ -41,6 +41,12 @@ export interface AppiumServerExtension {
|
||||
webSocketsMapping: Record<string, WSServer>;
|
||||
/** Returns true if the server operates via HTTPS protocol */
|
||||
isSecure(): boolean;
|
||||
/**
|
||||
* A {@linkcode Router} mounted before any route is registered. Use it from
|
||||
* {@linkcode UpdateServerCallback} to add middleware that must see every request, including
|
||||
* ones matched by routes registered elsewhere. See https://github.com/appium/appium/issues/17411
|
||||
*/
|
||||
frontRouter: Router;
|
||||
}
|
||||
|
||||
export type {WSServer};
|
||||
|
||||
Reference in New Issue
Block a user