feat(fallback): allow falling back w/ overrides (#14849)

This commit is contained in:
Pavel Feldman 2022-06-13 16:56:16 -08:00 committed by GitHub
parent 324cdcd874
commit 9cf068ad06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 516 additions and 143 deletions

View file

@ -42,8 +42,8 @@ await page.route('**/*', (route, request) => {
// Override headers
const headers = {
...request.headers(),
foo: 'bar', // set "foo" header
origin: undefined, // remove "origin" header
foo: 'foo-value', // set "foo" header
bar: undefined, // remove "bar" header
};
route.continue({headers});
});
@ -53,8 +53,8 @@ await page.route('**/*', (route, request) => {
page.route("**/*", route -> {
// Override headers
Map<String, String> headers = new HashMap<>(route.request().headers());
headers.put("foo", "bar"); // set "foo" header
headers.remove("origin"); // remove "origin" header
headers.put("foo", "foo-value"); // set "foo" header
headers.remove("bar"); // remove "bar" header
route.resume(new Route.ResumeOptions().setHeaders(headers));
});
```
@ -64,8 +64,8 @@ async def handle(route, request):
# override headers
headers = {
**request.headers,
"foo": "bar" # set "foo" header
"origin": None # remove "origin" header
"foo": "foo-value" # set "foo" header
"bar": None # remove "bar" header
}
await route.continue_(headers=headers)
}
@ -77,8 +77,8 @@ def handle(route, request):
# override headers
headers = {
**request.headers,
"foo": "bar" # set "foo" header
"origin": None # remove "origin" header
"foo": "foo-value" # set "foo" header
"bar": None # remove "bar" header
}
route.continue_(headers=headers)
}
@ -116,9 +116,75 @@ If set changes the request HTTP headers. Header values will be converted to a st
## 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.
When several routes match the given pattern, they run in the order opposite to their registration.
That way the last registered route can always override all the previos ones. In the example below,
request will be handled by the bottom-most handler first, then it'll fall back to the previous one and
in the end will be aborted by the first registered route.
```js
await page.route('**/*', route => {
// Runs last.
route.abort();
});
await page.route('**/*', route => {
// Runs second.
route.fallback();
});
await page.route('**/*', route => {
// Runs first.
route.fallback();
});
```
```java
page.route("**/*", route -> {
// Runs last.
route.abort();
});
page.route("**/*", route -> {
// Runs second.
route.fallback();
});
page.route("**/*", route -> {
// Runs first.
route.fallback();
});
```
```python async
await page.route("**/*", lambda route: route.abort()) # Runs last.
await page.route("**/*", lambda route: route.fallback()) # Runs second.
await page.route("**/*", lambda route: route.fallback()) # Runs first.
```
```python sync
page.route("**/*", lambda route: route.abort()) # Runs last.
page.route("**/*", lambda route: route.fallback()) # Runs second.
page.route("**/*", lambda route: route.fallback()) # Runs first.
```
```csharp
await page.RouteAsync("**/*", route => {
// Runs last.
await route.AbortAsync();
});
await page.RouteAsync("**/*", route => {
// Runs second.
await route.FallbackAsync();
});
await page.RouteAsync("**/*", route => {
// Runs first.
await route.FallbackAsync();
});
```
Registering multiple routes is useful when you want separate handlers to
handle different kinds of requests, for example API calls vs page resources or
GET requests vs POST requests as in the example below.
```js
// Handle GET requests.
@ -228,6 +294,87 @@ await page.RouteAsync("**/*", route => {
});
```
One can also modify request while falling back to the subsequent handler, that way intermediate
route handler can modify url, method, headers and postData of the request.
```js
await page.route('**/*', (route, request) => {
// Override headers
const headers = {
...request.headers(),
foo: 'foo-value', // set "foo" header
bar: undefined, // remove "bar" header
};
route.fallback({headers});
});
```
```java
page.route("**/*", route -> {
// Override headers
Map<String, String> headers = new HashMap<>(route.request().headers());
headers.put("foo", "foo-value"); // set "foo" header
headers.remove("bar"); // remove "bar" header
route.fallback(new Route.ResumeOptions().setHeaders(headers));
});
```
```python async
async def handle(route, request):
# override headers
headers = {
**request.headers,
"foo": "foo-value" # set "foo" header
"bar": None # remove "bar" header
}
await route.fallback(headers=headers)
}
await page.route("**/*", handle)
```
```python sync
def handle(route, request):
# override headers
headers = {
**request.headers,
"foo": "foo-value" # set "foo" header
"bar": None # remove "bar" header
}
route.fallback(headers=headers)
}
page.route("**/*", handle)
```
```csharp
await page.RouteAsync("**/*", route =>
{
var headers = new Dictionary<string, string>(route.Request.Headers) { { "foo", "foo-value" } };
headers.Remove("bar");
route.FallbackAsync(headers);
});
```
### option: Route.fallback.url
- `url` <[string]>
If set changes the request URL. New URL must have same protocol as original one. Changing the URL won't
affect the route matching, all the routes are matched using the original request URL.
### option: Route.fallback.method
- `method` <[string]>
If set changes the request method (e.g. GET or POST)
### option: Route.fallback.postData
- `postData` <[string]|[Buffer]>
If set changes the post data of request
### option: Route.fallback.headers
- `headers` <[Object]<[string], [string]>>
If set changes the request HTTP headers. Header values will be converted to a string.
## async method: Route.fulfill
Fulfills route's request with given response.

View file

@ -154,7 +154,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
if (handled)
return;
}
await route._innerContinue({}, true);
await route._innerContinue(true);
}
async _onBinding(bindingCall: BindingCall) {

View file

@ -60,6 +60,13 @@ type RouteHAR = {
path: string;
};
type FallbackOverrides = {
url?: string;
method?: string;
headers?: Headers;
postData?: string | Buffer;
};
export class Request extends ChannelOwner<channels.RequestChannel> implements api.Request {
private _redirectedFrom: Request | null = null;
private _redirectedTo: Request | null = null;
@ -68,6 +75,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
private _actualHeadersPromise: Promise<RawHeaders> | undefined;
private _postData: Buffer | null;
_timing: ResourceTiming;
private _fallbackOverrides: FallbackOverrides = {};
static from(request: channels.RequestChannel): Request {
return (request as any)._object;
@ -98,7 +106,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
}
url(): string {
return this._initializer.url;
return this._fallbackOverrides.url || this._initializer.url;
}
resourceType(): string {
@ -106,14 +114,21 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
}
method(): string {
return this._initializer.method;
return this._fallbackOverrides.method || this._initializer.method;
}
postData(): string | null {
if (this._fallbackOverrides.postData)
return this._fallbackOverrides.postData.toString('utf-8');
return this._postData ? this._postData.toString('utf8') : null;
}
postDataBuffer(): Buffer | null {
if (this._fallbackOverrides.postData) {
if (isString(this._fallbackOverrides.postData))
return Buffer.from(this._fallbackOverrides.postData, 'utf-8');
return this._fallbackOverrides.postData;
}
return this._postData;
}
@ -142,7 +157,9 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
* @deprecated
*/
headers(): Headers {
return this._provisionalHeaders.headers();
if (this._fallbackOverrides.headers)
return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers).headers();
return this._fallbackOverrides.headers || this._provisionalHeaders.headers();
}
_context() {
@ -151,6 +168,9 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
}
_actualHeaders(): Promise<RawHeaders> {
if (this._fallbackOverrides.headers)
return Promise.resolve(RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers));
if (!this._actualHeadersPromise) {
this._actualHeadersPromise = this._wrapApiCall(async () => {
return new RawHeaders((await this._channel.rawRequestHeaders()).headers);
@ -219,14 +239,15 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
_finalRequest(): Request {
return this._redirectedTo ? this._redirectedTo._finalRequest() : this;
}
}
type OverridesForContinue = {
url?: string;
method?: string;
headers?: Headers;
postData?: string | Buffer;
};
_applyFallbackOverrides(overrides: FallbackOverrides) {
this._fallbackOverrides = { ...this._fallbackOverrides, ...overrides };
}
_fallbackOverridesForContinue() {
return this._fallbackOverrides;
}
}
export class Route extends ChannelOwner<channels.RouteChannel> implements api.Route {
private _handlingPromise: ManualPromise<boolean> | null = null;
@ -259,8 +280,9 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
return this._handlingPromise;
}
async fallback() {
async fallback(options: FallbackOverrides = {}) {
this._checkNotHandled();
this.request()._applyFallbackOverrides(options);
this._reportHandled(false);
}
@ -360,9 +382,10 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
return 'done';
}
async continue(options: OverridesForContinue = {}) {
async continue(options: FallbackOverrides = {}) {
this._checkNotHandled();
await this._innerContinue(options);
this.request()._applyFallbackOverrides(options);
await this._innerContinue();
this._reportHandled(true);
}
@ -377,7 +400,8 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
chain.resolve(done);
}
async _innerContinue(options: OverridesForContinue = {}, internal = false) {
async _innerContinue(internal = false) {
const options = this.request()._fallbackOverridesForContinue();
return await this._wrapApiCall(async () => {
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
await this._raceWithPageClose(this._channel.continue({
@ -624,6 +648,13 @@ export class RawHeaders {
private _headersArray: HeadersArray;
private _headersMap = new MultiMap<string, string>();
static _fromHeadersObjectLossy(headers: Headers): RawHeaders {
const headersArray: HeadersArray = Object.entries(headers).map(([name, value]) => ({
name, value
})).filter(header => header.value !== undefined);
return new RawHeaders(headersArray);
}
constructor(headers: HeadersArray) {
this._headersArray = headers;
for (const header of headers)

View file

@ -14796,8 +14796,8 @@ export interface Route {
* // Override headers
* const headers = {
* ...request.headers(),
* foo: 'bar', // set "foo" header
* origin: undefined, // remove "origin" header
* foo: 'foo-value', // set "foo" header
* bar: undefined, // remove "bar" header
* };
* route.continue({headers});
* });
@ -14828,8 +14828,28 @@ export interface Route {
}): 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.
* When several routes match the given pattern, they run in the order opposite to their registration. That way the last
* registered route can always override all the previos ones. In the example below, request will be handled by the
* bottom-most handler first, then it'll fall back to the previous one and in the end will be aborted by the first
* registered route.
*
* ```js
* await page.route('**\/*', route => {
* // Runs last.
* route.abort();
* });
* await page.route('**\/*', route => {
* // Runs second.
* route.fallback();
* });
* await page.route('**\/*', route => {
* // Runs first.
* route.fallback();
* });
* ```
*
* Registering multiple routes is useful when you want separate handlers to handle different kinds of requests, for example
* API calls vs page resources or GET requests vs POST requests as in the example below.
*
* ```js
* // Handle GET requests.
@ -14853,8 +14873,45 @@ export interface Route {
* });
* ```
*
* One can also modify request while falling back to the subsequent handler, that way intermediate route handler can modify
* url, method, headers and postData of the request.
*
* ```js
* await page.route('**\/*', (route, request) => {
* // Override headers
* const headers = {
* ...request.headers(),
* foo: 'foo-value', // set "foo" header
* bar: undefined, // remove "bar" header
* };
* route.fallback({headers});
* });
* ```
*
* @param options
*/
fallback(): Promise<void>;
fallback(options?: {
/**
* If set changes the request HTTP headers. Header values will be converted to a string.
*/
headers?: { [key: string]: string; };
/**
* If set changes the request method (e.g. GET or POST)
*/
method?: string;
/**
* If set changes the post data of request
*/
postData?: string|Buffer;
/**
* If set changes the request URL. New URL must have same protocol as original one. Changing the URL won't affect the route
* matching, all the routes are matched using the original request URL.
*/
url?: string;
}): Promise<void>;
/**
* Fulfills route's request with given response.

View file

@ -223,35 +223,3 @@ it('should connect to a browser with the default page', async ({ browserType,cre
expect(context.pages().length).toBe(1);
await context.close();
});
it('route.continue should delete the origin header', async ({ launchPersistent, server, isAndroid, browserName }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' });
it.skip(isAndroid, 'No cross-process on Android');
it.fail(browserName === 'webkit', 'Does not delete origin in webkit');
const { page } = await launchPersistent();
await page.goto(server.PREFIX + '/empty.html');
server.setRoute('/something', (request, response) => {
response.writeHead(200, { 'Access-Control-Allow-Origin': '*' });
response.end('done');
});
let interceptedRequest;
await page.route(server.CROSS_PROCESS_PREFIX + '/something', async (route, request) => {
interceptedRequest = request;
const headers = await request.allHeaders();
delete headers['origin'];
route.continue({ headers });
});
const [text, serverRequest] = await Promise.all([
page.evaluate(async url => {
const data = await fetch(url);
return data.text();
}, server.CROSS_PROCESS_PREFIX + '/something'),
server.waitForRequest('/something')
]);
expect(text).toBe('done');
expect(interceptedRequest.headers()['origin']).toEqual(server.PREFIX);
expect(serverRequest.headers.origin).toBeFalsy();
});

View file

@ -69,7 +69,7 @@ it('should delete header with undefined value', async ({ page, server, browserNa
server.waitForRequest('/something')
]);
expect(text).toBe('done');
expect(interceptedRequest.headers()['foo']).toEqual('a');
expect(interceptedRequest.headers()['foo']).toEqual(undefined);
expect(serverRequest.headers.foo).toBeFalsy();
expect(serverRequest.headers.bar).toBe('b');
});
@ -311,7 +311,6 @@ it('should delete the origin header', async ({ page, server, isAndroid, browserN
server.waitForRequest('/something')
]);
expect(text).toBe('done');
expect(interceptedRequest.headers()['origin']).toEqual(server.PREFIX);
expect(interceptedRequest.headers()['origin']).toEqual(undefined);
expect(serverRequest.headers.origin).toBeFalsy();
});

View file

@ -0,0 +1,245 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test as it, expect } from './pageTest';
it('should work', async ({ page, server }) => {
await page.route('**/*', route => route.fallback());
await page.goto(server.EMPTY_PAGE);
});
it('should fall back', async ({ page, server }) => {
const intercepted = [];
await page.route('**/empty.html', route => {
intercepted.push(1);
route.fallback();
});
await page.route('**/empty.html', route => {
intercepted.push(2);
route.fallback();
});
await page.route('**/empty.html', route => {
intercepted.push(3);
route.fallback();
});
await page.goto(server.EMPTY_PAGE);
expect(intercepted).toEqual([3, 2, 1]);
});
it('should not chain fulfill', async ({ page, server }) => {
let failed = false;
await page.route('**/empty.html', route => {
failed = true;
});
await page.route('**/empty.html', route => {
route.fulfill({ status: 200, body: 'fulfilled' });
});
await page.route('**/empty.html', route => {
route.fallback();
});
const response = await page.goto(server.EMPTY_PAGE);
const body = await response.body();
expect(body.toString()).toEqual('fulfilled');
expect(failed).toBeFalsy();
});
it('should not chain abort', async ({ page, server }) => {
let failed = false;
await page.route('**/empty.html', route => {
failed = true;
});
await page.route('**/empty.html', route => {
route.abort();
});
await page.route('**/empty.html', route => {
route.fallback();
});
const e = await page.goto(server.EMPTY_PAGE).catch(e => e);
expect(e).toBeTruthy();
expect(failed).toBeFalsy();
});
it('should fall back after exception', async ({ page, server }) => {
await page.route('**/empty.html', route => {
route.continue();
});
await page.route('**/empty.html', async route => {
try {
await route.fulfill({ har: { path: 'file' }, response: {} as any });
} catch (e) {
route.fallback();
}
});
await page.goto(server.EMPTY_PAGE);
});
it('should chain once', async ({ page, server }) => {
await page.route('**/empty.html', route => {
route.fulfill({ status: 200, body: 'fulfilled one' });
}, { times: 1 });
await page.route('**/empty.html', route => {
route.fallback();
}, { times: 1 });
const response = await page.goto(server.EMPTY_PAGE);
const body = await response.body();
expect(body.toString()).toEqual('fulfilled one');
});
it('should amend HTTP headers', async ({ page, server }) => {
const values = [];
await page.route('**/sleep.zzz', async route => {
values.push(route.request().headers().foo);
values.push(await route.request().headerValue('FOO'));
route.continue();
});
await page.route('**/*', route => {
route.fallback({ headers: { ...route.request().headers(), FOO: 'bar' } });
});
await page.goto(server.EMPTY_PAGE);
const [request] = await Promise.all([
server.waitForRequest('/sleep.zzz'),
page.evaluate(() => fetch('/sleep.zzz'))
]);
values.push(request.headers['foo']);
expect(values).toEqual(['bar', 'bar', 'bar']);
});
it('should delete header with undefined value', async ({ page, server, browserName }) => {
await page.goto(server.PREFIX + '/empty.html');
server.setRoute('/something', (request, response) => {
response.writeHead(200, { 'Access-Control-Allow-Origin': '*' });
response.end('done');
});
let interceptedRequest;
await page.route('**/*', (route, request) => {
interceptedRequest = request;
route.continue();
});
await page.route(server.PREFIX + '/something', async (route, request) => {
const headers = await request.allHeaders();
route.fallback({
headers: {
...headers,
foo: undefined
}
});
});
const [text, serverRequest] = await Promise.all([
page.evaluate(async url => {
const data = await fetch(url, {
headers: {
foo: 'a',
bar: 'b',
}
});
return data.text();
}, server.PREFIX + '/something'),
server.waitForRequest('/something')
]);
expect(text).toBe('done');
expect(interceptedRequest.headers()['foo']).toEqual(undefined);
expect(interceptedRequest.headers()['bar']).toEqual('b');
expect(serverRequest.headers.foo).toBeFalsy();
expect(serverRequest.headers.bar).toBe('b');
});
it('should amend method', async ({ page, server }) => {
const sRequest = server.waitForRequest('/sleep.zzz');
await page.goto(server.EMPTY_PAGE);
let method: string;
await page.route('**/*', route => {
method = route.request().method();
route.continue();
});
await page.route('**/*', route => route.fallback({ method: 'POST' }));
const [request] = await Promise.all([
server.waitForRequest('/sleep.zzz'),
page.evaluate(() => fetch('/sleep.zzz'))
]);
expect(method).toBe('POST');
expect(request.method).toBe('POST');
expect((await sRequest).method).toBe('POST');
});
it('should override request url', async ({ page, server }) => {
const request = server.waitForRequest('/global-var.html');
let url: string;
await page.route('**/foo', route => {
url = route.request().url();
route.continue();
});
await page.route('**/foo', route => route.fallback({ url: server.PREFIX + '/global-var.html' }));
const [response] = await Promise.all([
page.waitForEvent('response'),
page.goto(server.PREFIX + '/foo'),
]);
expect(url).toBe(server.PREFIX + '/global-var.html');
expect(response.url()).toBe(server.PREFIX + '/foo');
expect(await page.evaluate(() => window['globalVar'])).toBe(123);
expect((await request).method).toBe('GET');
});
it.describe('post data', () => {
it.fixme(({ isAndroid }) => isAndroid, 'Post data does not work');
it('should amend post data', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
let postData: string;
await page.route('**/*', route => {
postData = route.request().postData();
route.continue();
});
await page.route('**/*', route => {
route.fallback({ postData: 'doggo' });
});
const [serverRequest] = await Promise.all([
server.waitForRequest('/sleep.zzz'),
page.evaluate(() => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' }))
]);
expect(postData).toBe('doggo');
expect((await serverRequest.postBody).toString('utf8')).toBe('doggo');
});
it('should amend binary post data', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
const arr = Array.from(Array(256).keys());
let postDataBuffer: Buffer;
await page.route('**/*', route => {
postDataBuffer = route.request().postDataBuffer();
route.continue();
});
await page.route('**/*', route => {
route.fallback({ postData: Buffer.from(arr) });
});
const [serverRequest] = await Promise.all([
server.waitForRequest('/sleep.zzz'),
page.evaluate(() => fetch('/sleep.zzz', { method: 'POST', body: 'birdy' }))
]);
const buffer = await serverRequest.postBody;
expect(postDataBuffer.length).toBe(arr.length);
expect(buffer.length).toBe(arr.length);
for (let i = 0; i < arr.length; ++i) {
expect(buffer[i]).toBe(arr[i]);
expect(postDataBuffer[i]).toBe(arr[i]);
}
});
});

View file

@ -86,6 +86,7 @@ it('should work when POST is redirected with 302', async ({ page, server }) => {
page.waitForNavigation()
]);
});
// @see https://github.com/GoogleChrome/puppeteer/issues/3973
it('should work when header manipulation headers with redirect', async ({ page, server }) => {
server.setRedirect('/rrredirect', '/empty.html');
@ -97,6 +98,7 @@ it('should work when header manipulation headers with redirect', async ({ page,
});
await page.goto(server.PREFIX + '/rrredirect');
});
// @see https://github.com/GoogleChrome/puppeteer/issues/4743
it('should be able to remove headers', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
@ -149,6 +151,7 @@ it('should show custom HTTP headers', async ({ page, server }) => {
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok()).toBe(true);
});
// @see https://github.com/GoogleChrome/puppeteer/issues/4337
it('should work with redirect inside sync XHR', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
@ -837,80 +840,3 @@ for (const method of ['fulfill', 'continue', 'fallback', 'abort'] as const) {
expect(e.message).toContain('Route is already handled!');
});
}
it('should fall back', async ({ page, server }) => {
const intercepted = [];
await page.route('**/empty.html', route => {
intercepted.push(1);
route.fallback();
});
await page.route('**/empty.html', route => {
intercepted.push(2);
route.fallback();
});
await page.route('**/empty.html', route => {
intercepted.push(3);
route.fallback();
});
await page.goto(server.EMPTY_PAGE);
expect(intercepted).toEqual([3, 2, 1]);
});
it('should not chain fulfill', async ({ page, server }) => {
let failed = false;
await page.route('**/empty.html', route => {
failed = true;
});
await page.route('**/empty.html', route => {
route.fulfill({ status: 200, body: 'fulfilled' });
});
await page.route('**/empty.html', route => {
route.fallback();
});
const response = await page.goto(server.EMPTY_PAGE);
const body = await response.body();
expect(body.toString()).toEqual('fulfilled');
expect(failed).toBeFalsy();
});
it('should not chain abort', async ({ page, server }) => {
let failed = false;
await page.route('**/empty.html', route => {
failed = true;
});
await page.route('**/empty.html', route => {
route.abort();
});
await page.route('**/empty.html', route => {
route.fallback();
});
const e = await page.goto(server.EMPTY_PAGE).catch(e => e);
expect(e).toBeTruthy();
expect(failed).toBeFalsy();
});
it('should fall back after exception', async ({ page, server }) => {
await page.route('**/empty.html', route => {
route.continue();
});
await page.route('**/empty.html', async route => {
try {
await route.fulfill({ har: { path: 'file' }, response: {} as any });
} catch (e) {
route.fallback();
}
});
await page.goto(server.EMPTY_PAGE);
});
it('should chain once', async ({ page, server }) => {
await page.route('**/empty.html', route => {
route.fulfill({ status: 200, body: 'fulfilled one' });
}, { times: 1 });
await page.route('**/empty.html', route => {
route.fallback();
}, { times: 1 });
const response = await page.goto(server.EMPTY_PAGE);
const body = await response.body();
expect(body.toString()).toEqual('fulfilled one');
});