Skip to content

Commit 6af6fab

Browse files
authored
fix(har): internal redirect in renderer-initiated navigations (#15000)
fix(har): internal redirect in renderer-initiated navigations
1 parent c0ea28d commit 6af6fab

File tree

9 files changed

+67
-23
lines changed

9 files changed

+67
-23
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class HarRouter {
5656

5757
if (response.action === 'redirect') {
5858
debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`);
59-
await route._abort(undefined, response.redirectURL);
59+
await route._redirectNavigationRequest(response.redirectURL!);
6060
return;
6161
}
6262

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,14 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
282282
}
283283

284284
async abort(errorCode?: string) {
285-
await this._abort(errorCode);
285+
this._checkNotHandled();
286+
await this._raceWithPageClose(this._channel.abort({ errorCode }));
287+
this._reportHandled(true);
286288
}
287289

288-
async _abort(errorCode?: string, redirectAbortedNavigationToUrl?: string) {
290+
async _redirectNavigationRequest(url: string) {
289291
this._checkNotHandled();
290-
await this._raceWithPageClose(this._channel.abort({ errorCode, redirectAbortedNavigationToUrl }));
292+
await this._raceWithPageClose(this._channel.redirectNavigationRequest({ url }));
291293
this._reportHandled(true);
292294
}
293295

packages/playwright-core/src/protocol/channels.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3158,17 +3158,23 @@ export interface RouteEventTarget {
31583158
}
31593159
export interface RouteChannel extends RouteEventTarget, Channel {
31603160
_type_Route: boolean;
3161+
redirectNavigationRequest(params: RouteRedirectNavigationRequestParams, metadata?: Metadata): Promise<RouteRedirectNavigationRequestResult>;
31613162
abort(params: RouteAbortParams, metadata?: Metadata): Promise<RouteAbortResult>;
31623163
continue(params: RouteContinueParams, metadata?: Metadata): Promise<RouteContinueResult>;
31633164
fulfill(params: RouteFulfillParams, metadata?: Metadata): Promise<RouteFulfillResult>;
31643165
}
3166+
export type RouteRedirectNavigationRequestParams = {
3167+
url: string,
3168+
};
3169+
export type RouteRedirectNavigationRequestOptions = {
3170+
3171+
};
3172+
export type RouteRedirectNavigationRequestResult = void;
31653173
export type RouteAbortParams = {
31663174
errorCode?: string,
3167-
redirectAbortedNavigationToUrl?: string,
31683175
};
31693176
export type RouteAbortOptions = {
31703177
errorCode?: string,
3171-
redirectAbortedNavigationToUrl?: string,
31723178
};
31733179
export type RouteAbortResult = void;
31743180
export type RouteContinueParams = {

packages/playwright-core/src/protocol/protocol.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2491,10 +2491,13 @@ Route:
24912491

24922492
commands:
24932493

2494+
redirectNavigationRequest:
2495+
parameters:
2496+
url: string
2497+
24942498
abort:
24952499
parameters:
24962500
errorCode: string?
2497-
redirectAbortedNavigationToUrl: string?
24982501

24992502
continue:
25002503
parameters:

packages/playwright-core/src/protocol/validator.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1181,9 +1181,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
11811181
});
11821182
scheme.RequestResponseParams = tOptional(tObject({}));
11831183
scheme.RequestRawRequestHeadersParams = tOptional(tObject({}));
1184+
scheme.RouteRedirectNavigationRequestParams = tObject({
1185+
url: tString,
1186+
});
11841187
scheme.RouteAbortParams = tObject({
11851188
errorCode: tOptional(tString),
1186-
redirectAbortedNavigationToUrl: tOptional(tString),
11871189
});
11881190
scheme.RouteContinueParams = tObject({
11891191
url: tOptional(tString),

packages/playwright-core/src/server/dispatchers/networkDispatchers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ export class RouteDispatcher extends Dispatcher<Route, channels.RouteChannel> im
135135
}
136136

137137
async abort(params: channels.RouteAbortParams): Promise<void> {
138-
await this._object.abort(params.errorCode || 'failed', params.redirectAbortedNavigationToUrl);
138+
await this._object.abort(params.errorCode || 'failed');
139+
}
140+
141+
async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams): Promise<void> {
142+
await this._object.redirectNavigationRequest(params.url);
139143
}
140144
}
141145

packages/playwright-core/src/server/frames.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export class FrameManager {
269269
name: frame._name,
270270
newDocument: frame.pendingDocument(),
271271
error: new NavigationAbortedError(documentId, errorText),
272-
isPublic: !frame._pendingNavigationRedirectAfterAbort
272+
isPublic: !(documentId && frame._redirectedNavigations.has(documentId)),
273273
};
274274
frame.setPendingDocument(undefined);
275275
frame.emit(Frame.Events.InternalNavigation, navigationEvent);
@@ -467,7 +467,7 @@ export class Frame extends SdkObject {
467467
readonly _detachedPromise: Promise<void>;
468468
private _detachedCallback = () => {};
469469
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();
470-
_pendingNavigationRedirectAfterAbort: { url: string, documentId: string } | undefined;
470+
readonly _redirectedNavigations = new Map<string, { url: string, gotoPromise: Promise<network.Response | null> }>(); // documentId -> data
471471

472472
constructor(page: Page, id: string, parentFrame: Frame | null) {
473473
super(page, 'frame');
@@ -604,21 +604,26 @@ export class Frame extends SdkObject {
604604
this._page._crashedPromise.then(() => { throw new Error('Navigation failed because page crashed!'); }),
605605
this._detachedPromise.then(() => { throw new Error('Navigating frame was detached!'); }),
606606
action().catch(e => {
607-
if (this._pendingNavigationRedirectAfterAbort && e instanceof NavigationAbortedError) {
608-
const { url, documentId } = this._pendingNavigationRedirectAfterAbort;
609-
this._pendingNavigationRedirectAfterAbort = undefined;
610-
if (e.documentId === documentId) {
611-
progress.log(`redirecting navigation to "${url}"`);
612-
return this._gotoAction(progress, url, options);
607+
if (e instanceof NavigationAbortedError && e.documentId) {
608+
const data = this._redirectedNavigations.get(e.documentId);
609+
if (data) {
610+
progress.log(`waiting for redirected navigation to "${data.url}"`);
611+
return data.gotoPromise;
613612
}
614613
}
615614
throw e;
616615
}),
617616
]);
618617
}
619618

620-
redirectNavigationAfterAbort(url: string, documentId: string) {
621-
this._pendingNavigationRedirectAfterAbort = { url, documentId };
619+
redirectNavigation(url: string, documentId: string, referer: string | undefined) {
620+
const controller = new ProgressController(serverSideCallMetadata(), this);
621+
const data = {
622+
url,
623+
gotoPromise: controller.run(progress => this._gotoAction(progress, url, { referer }), 0),
624+
};
625+
this._redirectedNavigations.set(documentId, data);
626+
data.gotoPromise.finally(() => this._redirectedNavigations.delete(documentId));
622627
}
623628

624629
async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
@@ -659,7 +664,7 @@ export class Frame extends SdkObject {
659664
if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
660665
// This is just a sanity check. In practice, new navigation should
661666
// cancel the previous one and report "request cancelled"-like error.
662-
throw new Error('Navigation interrupted by another one');
667+
throw new NavigationAbortedError(navigateResult.newDocumentId, 'Navigation interrupted by another one');
663668
}
664669
if (event.error)
665670
throw event.error;

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,13 +244,17 @@ export class Route extends SdkObject {
244244
return this._request;
245245
}
246246

247-
async abort(errorCode: string = 'failed', redirectAbortedNavigationToUrl?: string) {
247+
async abort(errorCode: string = 'failed') {
248248
this._startHandling();
249-
if (redirectAbortedNavigationToUrl)
250-
this._request.frame().redirectNavigationAfterAbort(redirectAbortedNavigationToUrl, this._request._documentId!);
251249
await this._delegate.abort(errorCode);
252250
}
253251

252+
async redirectNavigationRequest(url: string) {
253+
this._startHandling();
254+
assert(this._request.isNavigationRequest());
255+
this._request.frame().redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer'));
256+
}
257+
254258
async fulfill(overrides: channels.RouteFulfillParams) {
255259
this._startHandling();
256260
let body = overrides.body;

tests/library/browsercontext-har.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,31 @@ it('should change document URL after redirected navigation', async ({ contextFac
115115
const page = await context.newPage();
116116
const [response] = await Promise.all([
117117
page.waitForNavigation(),
118+
page.waitForURL('https://www.theverge.com/'),
118119
page.goto('https://theverge.com/')
119120
]);
120121
await expect(page).toHaveURL('https://www.theverge.com/');
121122
expect(response.request().url()).toBe('https://www.theverge.com/');
122123
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
123124
});
124125

126+
it('should change document URL after redirected navigation on click', async ({ server, contextFactory, isAndroid, asset }) => {
127+
it.fixme(isAndroid);
128+
129+
const path = asset('har-redirect.har');
130+
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
131+
const page = await context.newPage();
132+
await page.goto(server.EMPTY_PAGE);
133+
await page.setContent(`<a href="https://theverge.com/">click me</a>`);
134+
const [response] = await Promise.all([
135+
page.waitForNavigation(),
136+
page.click('text=click me'),
137+
]);
138+
await expect(page).toHaveURL('https://www.theverge.com/');
139+
expect(response.request().url()).toBe('https://www.theverge.com/');
140+
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
141+
});
142+
125143
it('should goBack to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
126144
it.fixme(isAndroid);
127145

0 commit comments

Comments
 (0)