feat(route): explicitly fall back to the next handler (#14834)
This commit is contained in:
parent
05c56f5942
commit
dcdd3c3cdb
|
|
@ -114,6 +114,120 @@ If set changes the post data of request
|
|||
|
||||
If set changes the request HTTP headers. Header values will be converted to a string.
|
||||
|
||||
## async method: Route.fallback
|
||||
|
||||
Proceeds to the next registered route in the route chain. If no more routes are
|
||||
registered, continues the request as is. This allows registering multiple routes
|
||||
with the same mask and falling back from one to another.
|
||||
|
||||
```js
|
||||
// Handle GET requests.
|
||||
await page.route('**/*', route => {
|
||||
if (route.request().method() !== 'GET') {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
// Handling GET only.
|
||||
// ...
|
||||
});
|
||||
|
||||
// Handle POST requests.
|
||||
await page.route('**/*', route => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
// Handling POST only.
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
// Handle GET requests.
|
||||
page.route("**/*", route -> {
|
||||
if (!route.request().method().equals("GET")) {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
// Handling GET only.
|
||||
// ...
|
||||
});
|
||||
|
||||
// Handle POST requests.
|
||||
page.route("**/*", route -> {
|
||||
if (!route.request().method().equals("POST")) {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
// Handling POST only.
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
```python async
|
||||
# Handle GET requests.
|
||||
def handle_post(route):
|
||||
if route.request.method != "GET":
|
||||
route.fallback()
|
||||
return
|
||||
# Handling GET only.
|
||||
# ...
|
||||
|
||||
# Handle POST requests.
|
||||
def handle_post(route):
|
||||
if route.request.method != "POST":
|
||||
route.fallback()
|
||||
return
|
||||
# Handling POST only.
|
||||
# ...
|
||||
|
||||
await page.route("**/*", handle_get)
|
||||
await page.route("**/*", handle_post)
|
||||
```
|
||||
|
||||
```python sync
|
||||
# Handle GET requests.
|
||||
def handle_post(route):
|
||||
if route.request.method != "GET":
|
||||
route.fallback()
|
||||
return
|
||||
# Handling GET only.
|
||||
# ...
|
||||
|
||||
# Handle POST requests.
|
||||
def handle_post(route):
|
||||
if route.request.method != "POST":
|
||||
route.fallback()
|
||||
return
|
||||
# Handling POST only.
|
||||
# ...
|
||||
|
||||
page.route("**/*", handle_get)
|
||||
page.route("**/*", handle_post)
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Handle GET requests.
|
||||
await page.RouteAsync("**/*", route => {
|
||||
if (route.Request.Method != "GET") {
|
||||
await route.FallbackAsync();
|
||||
return;
|
||||
}
|
||||
// Handling GET only.
|
||||
// ...
|
||||
});
|
||||
|
||||
// Handle POST requests.
|
||||
await page.RouteAsync("**/*", route => {
|
||||
if (route.Request.Method != "POST") {
|
||||
await route.FallbackAsync();
|
||||
return;
|
||||
}
|
||||
// Handling POST only.
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## async method: Route.fulfill
|
||||
|
||||
Fulfills route's request with given response.
|
||||
|
|
|
|||
|
|
@ -144,31 +144,17 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async _onRoute(route: network.Route, request: network.Request) {
|
||||
const routes = this._routes.filter(r => r.matches(request.url()));
|
||||
|
||||
const nextRoute = async () => {
|
||||
const routeHandler = routes.shift();
|
||||
if (!routeHandler) {
|
||||
await route._finalContinue();
|
||||
return;
|
||||
}
|
||||
|
||||
const routeHandlers = this._routes.filter(r => r.matches(request.url()));
|
||||
for (const routeHandler of routeHandlers) {
|
||||
if (routeHandler.willExpire())
|
||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
||||
|
||||
await new Promise<void>(f => {
|
||||
routeHandler.handle(route, request, async done => {
|
||||
if (!done)
|
||||
await nextRoute();
|
||||
f();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
await nextRoute();
|
||||
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
const handled = await routeHandler.handle(route, request);
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
await route._innerContinue({}, true);
|
||||
}
|
||||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
|
|
|
|||
|
|
@ -229,8 +229,7 @@ type OverridesForContinue = {
|
|||
};
|
||||
|
||||
export class Route extends ChannelOwner<channels.RouteChannel> implements api.Route {
|
||||
private _pendingContinueOverrides: OverridesForContinue | undefined;
|
||||
private _routeChain: ((done: boolean) => Promise<void>) | null = null;
|
||||
private _handlingPromise: ManualPromise<boolean> | null = null;
|
||||
|
||||
static from(route: channels.RouteChannel): Route {
|
||||
return (route as any)._object;
|
||||
|
|
@ -255,22 +254,30 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
]);
|
||||
}
|
||||
|
||||
_startHandling(routeChain: (done: boolean) => Promise<void>) {
|
||||
this._routeChain = routeChain;
|
||||
_startHandling(): Promise<boolean> {
|
||||
this._handlingPromise = new ManualPromise();
|
||||
return this._handlingPromise;
|
||||
}
|
||||
|
||||
async fallback() {
|
||||
this._checkNotHandled();
|
||||
this._reportHandled(false);
|
||||
}
|
||||
|
||||
async abort(errorCode?: string) {
|
||||
this._checkNotHandled();
|
||||
await this._raceWithPageClose(this._channel.abort({ errorCode }));
|
||||
await this._followChain(true);
|
||||
this._reportHandled(true);
|
||||
}
|
||||
|
||||
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}) {
|
||||
this._checkNotHandled();
|
||||
await this._wrapApiCall(async () => {
|
||||
const fallback = await this._innerFulfill(options);
|
||||
switch (fallback) {
|
||||
case 'abort': await this.abort(); break;
|
||||
case 'continue': await this.continue(); break;
|
||||
case 'done': await this._followChain(true); break;
|
||||
case 'done': this._reportHandled(true); break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -354,20 +361,23 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
}
|
||||
|
||||
async continue(options: OverridesForContinue = {}) {
|
||||
if (!this._routeChain)
|
||||
this._checkNotHandled();
|
||||
await this._innerContinue(options);
|
||||
this._reportHandled(true);
|
||||
}
|
||||
|
||||
_checkNotHandled() {
|
||||
if (!this._handlingPromise)
|
||||
throw new Error('Route is already handled!');
|
||||
this._pendingContinueOverrides = { ...this._pendingContinueOverrides, ...options };
|
||||
await this._followChain(false);
|
||||
}
|
||||
|
||||
async _followChain(done: boolean) {
|
||||
const chain = this._routeChain!;
|
||||
this._routeChain = null;
|
||||
await chain(done);
|
||||
_reportHandled(done: boolean) {
|
||||
const chain = this._handlingPromise!;
|
||||
this._handlingPromise = null;
|
||||
chain.resolve(done);
|
||||
}
|
||||
|
||||
async _finalContinue() {
|
||||
const options = this._pendingContinueOverrides || {};
|
||||
async _innerContinue(options: OverridesForContinue = {}, internal = false) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
|
||||
await this._raceWithPageClose(this._channel.continue({
|
||||
|
|
@ -376,7 +386,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
|
||||
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
|
||||
}));
|
||||
}, !this._pendingContinueOverrides);
|
||||
}, !!internal);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -593,12 +603,16 @@ export class RouteHandler {
|
|||
return urlMatches(this._baseURL, requestURL, this.url);
|
||||
}
|
||||
|
||||
public handle(route: Route, request: Request, routeChain: (done: boolean) => Promise<void>) {
|
||||
public async handle(route: Route, request: Request): Promise<boolean> {
|
||||
++this.handledCount;
|
||||
route._startHandling(routeChain);
|
||||
const handledPromise = route._startHandling();
|
||||
// Extract handler into a variable to avoid [RouteHandler.handler] in the stack.
|
||||
const handler = this.handler;
|
||||
handler(route, request);
|
||||
const [handled] = await Promise.all([
|
||||
handledPromise,
|
||||
handler(route, request),
|
||||
]);
|
||||
return handled;
|
||||
}
|
||||
|
||||
public willExpire(): boolean {
|
||||
|
|
|
|||
|
|
@ -180,30 +180,17 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
private async _onRoute(route: Route, request: Request) {
|
||||
const routes = this._routes.filter(r => r.matches(request.url()));
|
||||
|
||||
const nextRoute = async () => {
|
||||
const routeHandler = routes.shift();
|
||||
if (!routeHandler) {
|
||||
await this._browserContext._onRoute(route, request);
|
||||
return;
|
||||
}
|
||||
|
||||
const routeHandlers = this._routes.filter(r => r.matches(request.url()));
|
||||
for (const routeHandler of routeHandlers) {
|
||||
if (routeHandler.willExpire())
|
||||
this._routes.splice(this._routes.indexOf(routeHandler), 1);
|
||||
|
||||
await new Promise<void>(f => {
|
||||
routeHandler.handle(route, request, async done => {
|
||||
if (!done)
|
||||
await nextRoute();
|
||||
f();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
await nextRoute();
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
const handled = await routeHandler.handle(route, request);
|
||||
if (!this._routes.length)
|
||||
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
await this._browserContext._onRoute(route, request);
|
||||
}
|
||||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
|
|
|
|||
29
packages/playwright-core/types/types.d.ts
vendored
29
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -14833,6 +14833,35 @@ export interface Route {
|
|||
url?: string;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Proceeds to the next registered route in the route chain. If no more routes are registered, continues the request as is.
|
||||
* This allows registering multiple routes with the same mask and falling back from one to another.
|
||||
*
|
||||
* ```js
|
||||
* // Handle GET requests.
|
||||
* await page.route('**\/*', route => {
|
||||
* if (route.request().method() !== 'GET') {
|
||||
* route.fallback();
|
||||
* return;
|
||||
* }
|
||||
* // Handling GET only.
|
||||
* // ...
|
||||
* });
|
||||
*
|
||||
* // Handle POST requests.
|
||||
* await page.route('**\/*', route => {
|
||||
* if (route.request().method() !== 'POST') {
|
||||
* route.fallback();
|
||||
* return;
|
||||
* }
|
||||
* // Handling POST only.
|
||||
* // ...
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
fallback(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Fulfills route's request with given response.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -47,19 +47,19 @@ it('should unroute', async ({ browser, server }) => {
|
|||
let intercepted = [];
|
||||
await context.route('**/*', route => {
|
||||
intercepted.push(1);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(2);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(3);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
const handler4 = route => {
|
||||
intercepted.push(4);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
};
|
||||
await context.route('**/empty.html', handler4);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -250,19 +250,19 @@ it('should overwrite post body with empty string', async ({ context, server, pag
|
|||
expect(body).toBe('');
|
||||
});
|
||||
|
||||
it('should chain continue', async ({ context, page, server }) => {
|
||||
it('should chain fallback', async ({ context, page, server }) => {
|
||||
const intercepted = [];
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(1);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(2);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(3);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(intercepted).toEqual([3, 2, 1]);
|
||||
|
|
@ -277,7 +277,7 @@ it('should not chain fulfill', async ({ context, page, server }) => {
|
|||
route.fulfill({ status: 200, body: 'fulfilled' });
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
const body = await response.body();
|
||||
|
|
@ -294,38 +294,38 @@ it('should not chain abort', async ({ context, page, server }) => {
|
|||
route.abort();
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
const e = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||
expect(e).toBeTruthy();
|
||||
expect(failed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should chain continue into page', async ({ context, page, server }) => {
|
||||
it('should chain fallback into page', async ({ context, page, server }) => {
|
||||
const intercepted = [];
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(1);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(2);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await context.route('**/empty.html', route => {
|
||||
intercepted.push(3);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(4);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(5);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(6);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(intercepted).toEqual([6, 5, 4, 3, 2, 1]);
|
||||
|
|
|
|||
|
|
@ -43,19 +43,19 @@ it('should unroute', async ({ page, server }) => {
|
|||
let intercepted = [];
|
||||
await page.route('**/*', route => {
|
||||
intercepted.push(1);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(2);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(3);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
const handler4 = route => {
|
||||
intercepted.push(4);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
};
|
||||
await page.route('**/empty.html', handler4);
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -825,10 +825,10 @@ it('should contain raw response header after fulfill', async ({ page, server })
|
|||
expect(headers['content-type']).toBeTruthy();
|
||||
});
|
||||
|
||||
for (const method of ['fulfill', 'continue', 'abort'] as const) {
|
||||
for (const method of ['fulfill', 'continue', 'fallback', 'abort'] as const) {
|
||||
it(`route.${method} should throw if called twice`, async ({ page, server }) => {
|
||||
const routePromise = new Promise<Route>(async resove => {
|
||||
await page.route('**/*', resove);
|
||||
const routePromise = new Promise<Route>(async resolve => {
|
||||
await page.route('**/*', resolve);
|
||||
});
|
||||
page.goto(server.PREFIX + '/empty.html').catch(() => {});
|
||||
const route = await routePromise;
|
||||
|
|
@ -838,19 +838,19 @@ for (const method of ['fulfill', 'continue', 'abort'] as const) {
|
|||
});
|
||||
}
|
||||
|
||||
it('should chain continue', async ({ page, server }) => {
|
||||
it('should fall back', async ({ page, server }) => {
|
||||
const intercepted = [];
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(1);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(2);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
intercepted.push(3);
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(intercepted).toEqual([3, 2, 1]);
|
||||
|
|
@ -865,7 +865,7 @@ it('should not chain fulfill', async ({ page, server }) => {
|
|||
route.fulfill({ status: 200, body: 'fulfilled' });
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
const body = await response.body();
|
||||
|
|
@ -882,14 +882,14 @@ it('should not chain abort', async ({ page, server }) => {
|
|||
route.abort();
|
||||
});
|
||||
await page.route('**/empty.html', route => {
|
||||
route.continue();
|
||||
route.fallback();
|
||||
});
|
||||
const e = await page.goto(server.EMPTY_PAGE).catch(e => e);
|
||||
expect(e).toBeTruthy();
|
||||
expect(failed).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should continue after exception', async ({ page, server }) => {
|
||||
it('should fall back after exception', async ({ page, server }) => {
|
||||
await page.route('**/empty.html', route => {
|
||||
route.continue();
|
||||
});
|
||||
|
|
@ -897,7 +897,7 @@ it('should continue after exception', async ({ page, server }) => {
|
|||
try {
|
||||
await route.fulfill({ har: { path: 'file' }, response: {} as any });
|
||||
} catch (e) {
|
||||
route.continue();
|
||||
route.fallback();
|
||||
}
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -908,7 +908,7 @@ it('should chain once', async ({ page, server }) => {
|
|||
route.fulfill({ status: 200, body: 'fulfilled one' });
|
||||
}, { times: 1 });
|
||||
await page.route('**/empty.html', route => {
|
||||
route.continue();
|
||||
route.fallback();
|
||||
}, { times: 1 });
|
||||
const response = await page.goto(server.EMPTY_PAGE);
|
||||
const body = await response.body();
|
||||
|
|
|
|||
Loading…
Reference in a new issue