Skip to content

Commit dcdd3c3

Browse files
authored
feat(route): explicitly fall back to the next handler (#14834)
1 parent 05c56f5 commit dcdd3c3

File tree

7 files changed

+227
-97
lines changed

7 files changed

+227
-97
lines changed

docs/src/api/class-route.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,120 @@ If set changes the post data of request
114114

115115
If set changes the request HTTP headers. Header values will be converted to a string.
116116

117+
## async method: Route.fallback
118+
119+
Proceeds to the next registered route in the route chain. If no more routes are
120+
registered, continues the request as is. This allows registering multiple routes
121+
with the same mask and falling back from one to another.
122+
123+
```js
124+
// Handle GET requests.
125+
await page.route('**/*', route => {
126+
if (route.request().method() !== 'GET') {
127+
route.fallback();
128+
return;
129+
}
130+
// Handling GET only.
131+
// ...
132+
});
133+
134+
// Handle POST requests.
135+
await page.route('**/*', route => {
136+
if (route.request().method() !== 'POST') {
137+
route.fallback();
138+
return;
139+
}
140+
// Handling POST only.
141+
// ...
142+
});
143+
```
144+
145+
```java
146+
// Handle GET requests.
147+
page.route("**/*", route -> {
148+
if (!route.request().method().equals("GET")) {
149+
route.fallback();
150+
return;
151+
}
152+
// Handling GET only.
153+
// ...
154+
});
155+
156+
// Handle POST requests.
157+
page.route("**/*", route -> {
158+
if (!route.request().method().equals("POST")) {
159+
route.fallback();
160+
return;
161+
}
162+
// Handling POST only.
163+
// ...
164+
});
165+
```
166+
167+
```python async
168+
# Handle GET requests.
169+
def handle_post(route):
170+
if route.request.method != "GET":
171+
route.fallback()
172+
return
173+
# Handling GET only.
174+
# ...
175+
176+
# Handle POST requests.
177+
def handle_post(route):
178+
if route.request.method != "POST":
179+
route.fallback()
180+
return
181+
# Handling POST only.
182+
# ...
183+
184+
await page.route("**/*", handle_get)
185+
await page.route("**/*", handle_post)
186+
```
187+
188+
```python sync
189+
# Handle GET requests.
190+
def handle_post(route):
191+
if route.request.method != "GET":
192+
route.fallback()
193+
return
194+
# Handling GET only.
195+
# ...
196+
197+
# Handle POST requests.
198+
def handle_post(route):
199+
if route.request.method != "POST":
200+
route.fallback()
201+
return
202+
# Handling POST only.
203+
# ...
204+
205+
page.route("**/*", handle_get)
206+
page.route("**/*", handle_post)
207+
```
208+
209+
```csharp
210+
// Handle GET requests.
211+
await page.RouteAsync("**/*", route => {
212+
if (route.Request.Method != "GET") {
213+
await route.FallbackAsync();
214+
return;
215+
}
216+
// Handling GET only.
217+
// ...
218+
});
219+
220+
// Handle POST requests.
221+
await page.RouteAsync("**/*", route => {
222+
if (route.Request.Method != "POST") {
223+
await route.FallbackAsync();
224+
return;
225+
}
226+
// Handling POST only.
227+
// ...
228+
});
229+
```
230+
117231
## async method: Route.fulfill
118232

119233
Fulfills route's request with given response.

packages/playwright-core/src/client/browserContext.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -144,31 +144,17 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
144144
}
145145

146146
async _onRoute(route: network.Route, request: network.Request) {
147-
const routes = this._routes.filter(r => r.matches(request.url()));
148-
149-
const nextRoute = async () => {
150-
const routeHandler = routes.shift();
151-
if (!routeHandler) {
152-
await route._finalContinue();
153-
return;
154-
}
155-
147+
const routeHandlers = this._routes.filter(r => r.matches(request.url()));
148+
for (const routeHandler of routeHandlers) {
156149
if (routeHandler.willExpire())
157150
this._routes.splice(this._routes.indexOf(routeHandler), 1);
158-
159-
await new Promise<void>(f => {
160-
routeHandler.handle(route, request, async done => {
161-
if (!done)
162-
await nextRoute();
163-
f();
164-
});
165-
});
166-
};
167-
168-
await nextRoute();
169-
170-
if (!this._routes.length)
171-
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
151+
const handled = await routeHandler.handle(route, request);
152+
if (!this._routes.length)
153+
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
154+
if (handled)
155+
return;
156+
}
157+
await route._innerContinue({}, true);
172158
}
173159

174160
async _onBinding(bindingCall: BindingCall) {

packages/playwright-core/src/client/network.ts

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,7 @@ type OverridesForContinue = {
229229
};
230230

231231
export class Route extends ChannelOwner<channels.RouteChannel> implements api.Route {
232-
private _pendingContinueOverrides: OverridesForContinue | undefined;
233-
private _routeChain: ((done: boolean) => Promise<void>) | null = null;
232+
private _handlingPromise: ManualPromise<boolean> | null = null;
234233

235234
static from(route: channels.RouteChannel): Route {
236235
return (route as any)._object;
@@ -255,22 +254,30 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
255254
]);
256255
}
257256

258-
_startHandling(routeChain: (done: boolean) => Promise<void>) {
259-
this._routeChain = routeChain;
257+
_startHandling(): Promise<boolean> {
258+
this._handlingPromise = new ManualPromise();
259+
return this._handlingPromise;
260+
}
261+
262+
async fallback() {
263+
this._checkNotHandled();
264+
this._reportHandled(false);
260265
}
261266

262267
async abort(errorCode?: string) {
268+
this._checkNotHandled();
263269
await this._raceWithPageClose(this._channel.abort({ errorCode }));
264-
await this._followChain(true);
270+
this._reportHandled(true);
265271
}
266272

267273
async fulfill(options: { response?: api.APIResponse, status?: number, headers?: Headers, contentType?: string, body?: string | Buffer, path?: string, har?: RouteHAR } = {}) {
274+
this._checkNotHandled();
268275
await this._wrapApiCall(async () => {
269276
const fallback = await this._innerFulfill(options);
270277
switch (fallback) {
271278
case 'abort': await this.abort(); break;
272279
case 'continue': await this.continue(); break;
273-
case 'done': await this._followChain(true); break;
280+
case 'done': this._reportHandled(true); break;
274281
}
275282
});
276283
}
@@ -354,20 +361,23 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
354361
}
355362

356363
async continue(options: OverridesForContinue = {}) {
357-
if (!this._routeChain)
364+
this._checkNotHandled();
365+
await this._innerContinue(options);
366+
this._reportHandled(true);
367+
}
368+
369+
_checkNotHandled() {
370+
if (!this._handlingPromise)
358371
throw new Error('Route is already handled!');
359-
this._pendingContinueOverrides = { ...this._pendingContinueOverrides, ...options };
360-
await this._followChain(false);
361372
}
362373

363-
async _followChain(done: boolean) {
364-
const chain = this._routeChain!;
365-
this._routeChain = null;
366-
await chain(done);
374+
_reportHandled(done: boolean) {
375+
const chain = this._handlingPromise!;
376+
this._handlingPromise = null;
377+
chain.resolve(done);
367378
}
368379

369-
async _finalContinue() {
370-
const options = this._pendingContinueOverrides || {};
380+
async _innerContinue(options: OverridesForContinue = {}, internal = false) {
371381
return await this._wrapApiCall(async () => {
372382
const postDataBuffer = isString(options.postData) ? Buffer.from(options.postData, 'utf8') : options.postData;
373383
await this._raceWithPageClose(this._channel.continue({
@@ -376,7 +386,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
376386
headers: options.headers ? headersObjectToArray(options.headers) : undefined,
377387
postData: postDataBuffer ? postDataBuffer.toString('base64') : undefined,
378388
}));
379-
}, !this._pendingContinueOverrides);
389+
}, !!internal);
380390
}
381391
}
382392

@@ -593,12 +603,16 @@ export class RouteHandler {
593603
return urlMatches(this._baseURL, requestURL, this.url);
594604
}
595605

596-
public handle(route: Route, request: Request, routeChain: (done: boolean) => Promise<void>) {
606+
public async handle(route: Route, request: Request): Promise<boolean> {
597607
++this.handledCount;
598-
route._startHandling(routeChain);
608+
const handledPromise = route._startHandling();
599609
// Extract handler into a variable to avoid [RouteHandler.handler] in the stack.
600610
const handler = this.handler;
601-
handler(route, request);
611+
const [handled] = await Promise.all([
612+
handledPromise,
613+
handler(route, request),
614+
]);
615+
return handled;
602616
}
603617

604618
public willExpire(): boolean {

packages/playwright-core/src/client/page.ts

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -180,30 +180,17 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
180180
}
181181

182182
private async _onRoute(route: Route, request: Request) {
183-
const routes = this._routes.filter(r => r.matches(request.url()));
184-
185-
const nextRoute = async () => {
186-
const routeHandler = routes.shift();
187-
if (!routeHandler) {
188-
await this._browserContext._onRoute(route, request);
189-
return;
190-
}
191-
183+
const routeHandlers = this._routes.filter(r => r.matches(request.url()));
184+
for (const routeHandler of routeHandlers) {
192185
if (routeHandler.willExpire())
193186
this._routes.splice(this._routes.indexOf(routeHandler), 1);
194-
195-
await new Promise<void>(f => {
196-
routeHandler.handle(route, request, async done => {
197-
if (!done)
198-
await nextRoute();
199-
f();
200-
});
201-
});
202-
};
203-
204-
await nextRoute();
205-
if (!this._routes.length)
206-
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
187+
const handled = await routeHandler.handle(route, request);
188+
if (!this._routes.length)
189+
this._wrapApiCall(() => this._disableInterception(), true).catch(() => {});
190+
if (handled)
191+
return;
192+
}
193+
await this._browserContext._onRoute(route, request);
207194
}
208195

209196
async _onBinding(bindingCall: BindingCall) {

packages/playwright-core/types/types.d.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14833,6 +14833,35 @@ export interface Route {
1483314833
url?: string;
1483414834
}): Promise<void>;
1483514835

14836+
/**
14837+
* Proceeds to the next registered route in the route chain. If no more routes are registered, continues the request as is.
14838+
* This allows registering multiple routes with the same mask and falling back from one to another.
14839+
*
14840+
* ```js
14841+
* // Handle GET requests.
14842+
* await page.route('**\/*', route => {
14843+
* if (route.request().method() !== 'GET') {
14844+
* route.fallback();
14845+
* return;
14846+
* }
14847+
* // Handling GET only.
14848+
* // ...
14849+
* });
14850+
*
14851+
* // Handle POST requests.
14852+
* await page.route('**\/*', route => {
14853+
* if (route.request().method() !== 'POST') {
14854+
* route.fallback();
14855+
* return;
14856+
* }
14857+
* // Handling POST only.
14858+
* // ...
14859+
* });
14860+
* ```
14861+
*
14862+
*/
14863+
fallback(): Promise<void>;
14864+
1483614865
/**
1483714866
* Fulfills route's request with given response.
1483814867
*

0 commit comments

Comments
 (0)