feat(har): disambiguate requests by post data (#14993)

This commit is contained in:
Pavel Feldman 2022-06-20 14:14:40 -07:00 committed by GitHub
parent 5397394653
commit eb87966441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 38 deletions

View File

@ -471,6 +471,7 @@ async function launchContext(options: Options, headless: boolean, executablePath
contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar) }; contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar) };
if (options.saveHarGlob) if (options.saveHarGlob)
contextOptions.recordHar.urlFilter = options.saveHarGlob; contextOptions.recordHar.urlFilter = options.saveHarGlob;
contextOptions.serviceWorkers = 'block';
} }
// Close app when the last window closes. // Close app when the last window closes.

View File

@ -44,11 +44,14 @@ export class HarRouter {
} }
private async _handle(route: Route) { private async _handle(route: Route) {
const request = route.request();
const response = await this._localUtils._channel.harLookup({ const response = await this._localUtils._channel.harLookup({
harId: this._harId, harId: this._harId,
url: route.request().url(), url: request.url(),
method: route.request().method(), method: request.method(),
isNavigationRequest: route.request().isNavigationRequest() headers: (await request.headersArray()),
postData: request.postDataBuffer()?.toString('base64'),
isNavigationRequest: request.isNavigationRequest()
}); });
if (response.action === 'redirect') { if (response.action === 'redirect') {
@ -61,7 +64,7 @@ export class HarRouter {
await route.fulfill({ await route.fulfill({
status: response.status, status: response.status,
headers: Object.fromEntries(response.headers!.map(h => [h.name, h.value])), headers: Object.fromEntries(response.headers!.map(h => [h.name, h.value])),
body: response.base64Encoded ? Buffer.from(response.body!, 'base64') : response.body body: Buffer.from(response.body!, 'base64')
}); });
return; return;
} }

View File

@ -404,10 +404,12 @@ export type LocalUtilsHarLookupParams = {
harId: string, harId: string,
url: string, url: string,
method: string, method: string,
headers: NameValue[],
postData?: string,
isNavigationRequest: boolean, isNavigationRequest: boolean,
}; };
export type LocalUtilsHarLookupOptions = { export type LocalUtilsHarLookupOptions = {
postData?: string,
}; };
export type LocalUtilsHarLookupResult = { export type LocalUtilsHarLookupResult = {
action: 'error' | 'redirect' | 'fulfill' | 'noentry', action: 'error' | 'redirect' | 'fulfill' | 'noentry',
@ -416,7 +418,6 @@ export type LocalUtilsHarLookupResult = {
status?: number, status?: number,
headers?: NameValue[], headers?: NameValue[],
body?: string, body?: string,
base64Encoded?: boolean,
}; };
export type LocalUtilsHarCloseParams = { export type LocalUtilsHarCloseParams = {
harId: string, harId: string,

View File

@ -490,6 +490,10 @@ LocalUtils:
harId: string harId: string
url: string url: string
method: string method: string
headers:
type: array
items: NameValue
postData: string?
isNavigationRequest: boolean isNavigationRequest: boolean
returns: returns:
action: action:
@ -506,7 +510,6 @@ LocalUtils:
type: array? type: array?
items: NameValue items: NameValue
body: string? body: string?
base64Encoded: boolean?
harClose: harClose:
parameters: parameters:

View File

@ -212,6 +212,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
harId: tString, harId: tString,
url: tString, url: tString,
method: tString, method: tString,
headers: tArray(tType('NameValue')),
postData: tOptional(tString),
isNavigationRequest: tBoolean, isNavigationRequest: tBoolean,
}); });
scheme.LocalUtilsHarCloseParams = tObject({ scheme.LocalUtilsHarCloseParams = tObject({

View File

@ -114,7 +114,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
const harBackend = this._harBakends.get(params.harId); const harBackend = this._harBakends.get(params.harId);
if (!harBackend) if (!harBackend)
return { action: 'error', message: `Internal error: har was not opened` }; return { action: 'error', message: `Internal error: har was not opened` };
return await harBackend.lookup(params.url, params.method, params.isNavigationRequest); return await harBackend.lookup(params.url, params.method, params.headers, params.postData ? Buffer.from(params.postData, 'base64') : undefined, params.isNavigationRequest);
} }
async harClose(params: channels.LocalUtilsHarCloseParams, metadata?: channels.Metadata): Promise<void> { async harClose(params: channels.LocalUtilsHarCloseParams, metadata?: channels.Metadata): Promise<void> {
@ -140,7 +140,7 @@ class HarBackend {
this._zipFile = zipFile; this._zipFile = zipFile;
} }
async lookup(url: string, method: string, isNavigationRequest: boolean): Promise<{ async lookup(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined, isNavigationRequest: boolean): Promise<{
action: 'error' | 'redirect' | 'fulfill' | 'noentry', action: 'error' | 'redirect' | 'fulfill' | 'noentry',
message?: string, message?: string,
redirectURL?: string, redirectURL?: string,
@ -150,7 +150,7 @@ class HarBackend {
base64Encoded?: boolean }> { base64Encoded?: boolean }> {
let entry; let entry;
try { try {
entry = this._harFindResponse(url, method); entry = await this._harFindResponse(url, method, headers, postData);
} catch (e) { } catch (e) {
return { action: 'error', message: 'HAR error: ' + e.message }; return { action: 'error', message: 'HAR error: ' + e.message };
} }
@ -163,45 +163,72 @@ class HarBackend {
return { action: 'redirect', redirectURL: entry.request.url }; return { action: 'redirect', redirectURL: entry.request.url };
const response = entry.response; const response = entry.response;
const sha1 = (response.content as any)._sha1;
let body = response.content.text;
let base64Encoded = response.content.encoding === 'base64';
if (sha1) {
let buffer: Buffer;
try { try {
if (this._zipFile) const buffer = await this._loadContent(response.content);
buffer = await this._zipFile.read(sha1);
else
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, sha1));
} catch (e) {
return { action: 'error', message: e.message };
}
body = buffer.toString('base64');
base64Encoded = true;
}
return { return {
action: 'fulfill', action: 'fulfill',
status: response.status, status: response.status,
headers: response.headers, headers: response.headers,
body, body: buffer.toString('base64'),
base64Encoded
}; };
} catch (e) {
return { action: 'error', message: e.message };
}
} }
private _harFindResponse(url: string, method: string): HAREntry | undefined { private async _loadContent(content: { text?: string, encoding?: string, _sha1?: string }): Promise<Buffer> {
const sha1 = content._sha1;
let buffer: Buffer;
if (sha1) {
if (this._zipFile)
buffer = await this._zipFile.read(sha1);
else
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, sha1));
} else {
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
}
return buffer;
}
private async _harFindResponse(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined): Promise<HAREntry | undefined> {
const harLog = this._harFile.log; const harLog = this._harFile.log;
const visited = new Set<HAREntry>(); const visited = new Set<HAREntry>();
while (true) { while (true) {
const entry = harLog.entries.find(entry => entry.request.url === url && entry.request.method === method); const entries = harLog.entries.filter(entry => entry.request.url === url && entry.request.method === method);
if (!entry) if (!entries.length)
return; return;
let entry: HAREntry | undefined;
if (entries.length > 1) {
// Disambiguating requests
// 1. Disambiguate by postData - this covers GraphQL
if (!entry && postData) {
for (const candidate of entries) {
if (!candidate.request.postData)
continue;
const buffer = await this._loadContent(candidate.request.postData);
if (buffer.equals(postData)) {
entry = candidate;
break;
}
}
}
// TODO: disambiguate by headers.
}
// Fall back to first entry.
if (!entry)
entry = entries[0];
if (visited.has(entry)) if (visited.has(entry))
throw new Error(`Found redirect cycle for ${url}`); throw new Error(`Found redirect cycle for ${url}`);
visited.add(entry); visited.add(entry);
// Follow redirects.
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location'); const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
if (redirectStatus.includes(entry.response.status) && locationHeader) { if (redirectStatus.includes(entry.response.status) && locationHeader) {
const locationURL = new URL(locationHeader.value, url); const locationURL = new URL(locationHeader.value, url);