fix(har): internal redirect in renderer-initiated navigations (#15000)

fix(har): internal redirect in renderer-initiated navigations
This commit is contained in:
Dmitry Gozman 2022-06-21 11:01:01 -07:00 committed by GitHub
parent c0ea28d558
commit 6af6fab84a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 67 additions and 23 deletions

View File

@ -56,7 +56,7 @@ export class HarRouter {
if (response.action === 'redirect') { if (response.action === 'redirect') {
debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`); debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`);
await route._abort(undefined, response.redirectURL); await route._redirectNavigationRequest(response.redirectURL!);
return; return;
} }

View File

@ -282,12 +282,14 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
} }
async abort(errorCode?: string) { async abort(errorCode?: string) {
await this._abort(errorCode); this._checkNotHandled();
await this._raceWithPageClose(this._channel.abort({ errorCode }));
this._reportHandled(true);
} }
async _abort(errorCode?: string, redirectAbortedNavigationToUrl?: string) { async _redirectNavigationRequest(url: string) {
this._checkNotHandled(); this._checkNotHandled();
await this._raceWithPageClose(this._channel.abort({ errorCode, redirectAbortedNavigationToUrl })); await this._raceWithPageClose(this._channel.redirectNavigationRequest({ url }));
this._reportHandled(true); this._reportHandled(true);
} }

View File

@ -3158,17 +3158,23 @@ export interface RouteEventTarget {
} }
export interface RouteChannel extends RouteEventTarget, Channel { export interface RouteChannel extends RouteEventTarget, Channel {
_type_Route: boolean; _type_Route: boolean;
redirectNavigationRequest(params: RouteRedirectNavigationRequestParams, metadata?: Metadata): Promise<RouteRedirectNavigationRequestResult>;
abort(params: RouteAbortParams, metadata?: Metadata): Promise<RouteAbortResult>; abort(params: RouteAbortParams, metadata?: Metadata): Promise<RouteAbortResult>;
continue(params: RouteContinueParams, metadata?: Metadata): Promise<RouteContinueResult>; continue(params: RouteContinueParams, metadata?: Metadata): Promise<RouteContinueResult>;
fulfill(params: RouteFulfillParams, metadata?: Metadata): Promise<RouteFulfillResult>; fulfill(params: RouteFulfillParams, metadata?: Metadata): Promise<RouteFulfillResult>;
} }
export type RouteRedirectNavigationRequestParams = {
url: string,
};
export type RouteRedirectNavigationRequestOptions = {
};
export type RouteRedirectNavigationRequestResult = void;
export type RouteAbortParams = { export type RouteAbortParams = {
errorCode?: string, errorCode?: string,
redirectAbortedNavigationToUrl?: string,
}; };
export type RouteAbortOptions = { export type RouteAbortOptions = {
errorCode?: string, errorCode?: string,
redirectAbortedNavigationToUrl?: string,
}; };
export type RouteAbortResult = void; export type RouteAbortResult = void;
export type RouteContinueParams = { export type RouteContinueParams = {

View File

@ -2491,10 +2491,13 @@ Route:
commands: commands:
redirectNavigationRequest:
parameters:
url: string
abort: abort:
parameters: parameters:
errorCode: string? errorCode: string?
redirectAbortedNavigationToUrl: string?
continue: continue:
parameters: parameters:

View File

@ -1181,9 +1181,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
}); });
scheme.RequestResponseParams = tOptional(tObject({})); scheme.RequestResponseParams = tOptional(tObject({}));
scheme.RequestRawRequestHeadersParams = tOptional(tObject({})); scheme.RequestRawRequestHeadersParams = tOptional(tObject({}));
scheme.RouteRedirectNavigationRequestParams = tObject({
url: tString,
});
scheme.RouteAbortParams = tObject({ scheme.RouteAbortParams = tObject({
errorCode: tOptional(tString), errorCode: tOptional(tString),
redirectAbortedNavigationToUrl: tOptional(tString),
}); });
scheme.RouteContinueParams = tObject({ scheme.RouteContinueParams = tObject({
url: tOptional(tString), url: tOptional(tString),

View File

@ -135,7 +135,11 @@ export class RouteDispatcher extends Dispatcher<Route, channels.RouteChannel> im
} }
async abort(params: channels.RouteAbortParams): Promise<void> { async abort(params: channels.RouteAbortParams): Promise<void> {
await this._object.abort(params.errorCode || 'failed', params.redirectAbortedNavigationToUrl); await this._object.abort(params.errorCode || 'failed');
}
async redirectNavigationRequest(params: channels.RouteRedirectNavigationRequestParams): Promise<void> {
await this._object.redirectNavigationRequest(params.url);
} }
} }

View File

@ -269,7 +269,7 @@ export class FrameManager {
name: frame._name, name: frame._name,
newDocument: frame.pendingDocument(), newDocument: frame.pendingDocument(),
error: new NavigationAbortedError(documentId, errorText), error: new NavigationAbortedError(documentId, errorText),
isPublic: !frame._pendingNavigationRedirectAfterAbort isPublic: !(documentId && frame._redirectedNavigations.has(documentId)),
}; };
frame.setPendingDocument(undefined); frame.setPendingDocument(undefined);
frame.emit(Frame.Events.InternalNavigation, navigationEvent); frame.emit(Frame.Events.InternalNavigation, navigationEvent);
@ -467,7 +467,7 @@ export class Frame extends SdkObject {
readonly _detachedPromise: Promise<void>; readonly _detachedPromise: Promise<void>;
private _detachedCallback = () => {}; private _detachedCallback = () => {};
private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>(); private _raceAgainstEvaluationStallingEventsPromises = new Set<ManualPromise<any>>();
_pendingNavigationRedirectAfterAbort: { url: string, documentId: string } | undefined; readonly _redirectedNavigations = new Map<string, { url: string, gotoPromise: Promise<network.Response | null> }>(); // documentId -> data
constructor(page: Page, id: string, parentFrame: Frame | null) { constructor(page: Page, id: string, parentFrame: Frame | null) {
super(page, 'frame'); super(page, 'frame');
@ -604,12 +604,11 @@ export class Frame extends SdkObject {
this._page._crashedPromise.then(() => { throw new Error('Navigation failed because page crashed!'); }), this._page._crashedPromise.then(() => { throw new Error('Navigation failed because page crashed!'); }),
this._detachedPromise.then(() => { throw new Error('Navigating frame was detached!'); }), this._detachedPromise.then(() => { throw new Error('Navigating frame was detached!'); }),
action().catch(e => { action().catch(e => {
if (this._pendingNavigationRedirectAfterAbort && e instanceof NavigationAbortedError) { if (e instanceof NavigationAbortedError && e.documentId) {
const { url, documentId } = this._pendingNavigationRedirectAfterAbort; const data = this._redirectedNavigations.get(e.documentId);
this._pendingNavigationRedirectAfterAbort = undefined; if (data) {
if (e.documentId === documentId) { progress.log(`waiting for redirected navigation to "${data.url}"`);
progress.log(`redirecting navigation to "${url}"`); return data.gotoPromise;
return this._gotoAction(progress, url, options);
} }
} }
throw e; throw e;
@ -617,8 +616,14 @@ export class Frame extends SdkObject {
]); ]);
} }
redirectNavigationAfterAbort(url: string, documentId: string) { redirectNavigation(url: string, documentId: string, referer: string | undefined) {
this._pendingNavigationRedirectAfterAbort = { url, documentId }; const controller = new ProgressController(serverSideCallMetadata(), this);
const data = {
url,
gotoPromise: controller.run(progress => this._gotoAction(progress, url, { referer }), 0),
};
this._redirectedNavigations.set(documentId, data);
data.gotoPromise.finally(() => this._redirectedNavigations.delete(documentId));
} }
async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> { async goto(metadata: CallMetadata, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
@ -659,7 +664,7 @@ export class Frame extends SdkObject {
if (event.newDocument!.documentId !== navigateResult.newDocumentId) { if (event.newDocument!.documentId !== navigateResult.newDocumentId) {
// This is just a sanity check. In practice, new navigation should // This is just a sanity check. In practice, new navigation should
// cancel the previous one and report "request cancelled"-like error. // cancel the previous one and report "request cancelled"-like error.
throw new Error('Navigation interrupted by another one'); throw new NavigationAbortedError(navigateResult.newDocumentId, 'Navigation interrupted by another one');
} }
if (event.error) if (event.error)
throw event.error; throw event.error;

View File

@ -244,13 +244,17 @@ export class Route extends SdkObject {
return this._request; return this._request;
} }
async abort(errorCode: string = 'failed', redirectAbortedNavigationToUrl?: string) { async abort(errorCode: string = 'failed') {
this._startHandling(); this._startHandling();
if (redirectAbortedNavigationToUrl)
this._request.frame().redirectNavigationAfterAbort(redirectAbortedNavigationToUrl, this._request._documentId!);
await this._delegate.abort(errorCode); await this._delegate.abort(errorCode);
} }
async redirectNavigationRequest(url: string) {
this._startHandling();
assert(this._request.isNavigationRequest());
this._request.frame().redirectNavigation(url, this._request._documentId!, this._request.headerValue('referer'));
}
async fulfill(overrides: channels.RouteFulfillParams) { async fulfill(overrides: channels.RouteFulfillParams) {
this._startHandling(); this._startHandling();
let body = overrides.body; let body = overrides.body;

View File

@ -115,6 +115,7 @@ it('should change document URL after redirected navigation', async ({ contextFac
const page = await context.newPage(); const page = await context.newPage();
const [response] = await Promise.all([ const [response] = await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.waitForURL('https://www.theverge.com/'),
page.goto('https://theverge.com/') page.goto('https://theverge.com/')
]); ]);
await expect(page).toHaveURL('https://www.theverge.com/'); await expect(page).toHaveURL('https://www.theverge.com/');
@ -122,6 +123,23 @@ it('should change document URL after redirected navigation', async ({ contextFac
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/'); expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
}); });
it('should change document URL after redirected navigation on click', async ({ server, contextFactory, isAndroid, asset }) => {
it.fixme(isAndroid);
const path = asset('har-redirect.har');
const context = await contextFactory({ har: { path, urlFilter: /.*theverge.*/ } });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a href="https://theverge.com/">click me</a>`);
const [response] = await Promise.all([
page.waitForNavigation(),
page.click('text=click me'),
]);
await expect(page).toHaveURL('https://www.theverge.com/');
expect(response.request().url()).toBe('https://www.theverge.com/');
expect(await page.evaluate(() => location.href)).toBe('https://www.theverge.com/');
});
it('should goBack to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => { it('should goBack to redirected navigation', async ({ contextFactory, isAndroid, asset, server }) => {
it.fixme(isAndroid); it.fixme(isAndroid);