feat(fetch): get body lazily (#8784)
This commit is contained in:
parent
77b3b0965a
commit
b4ca77be23
|
|
@ -228,7 +228,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||||
});
|
});
|
||||||
if (result.error)
|
if (result.error)
|
||||||
throw new Error(`Request failed: ${result.error}`);
|
throw new Error(`Request failed: ${result.error}`);
|
||||||
return new network.FetchResponse(result.response!);
|
return new network.FetchResponse(this, result.response!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { Waiter } from './waiter';
|
||||||
import * as api from '../../types/types';
|
import * as api from '../../types/types';
|
||||||
import { URLMatch } from '../common/types';
|
import { URLMatch } from '../common/types';
|
||||||
import { urlMatches } from './clientHelper';
|
import { urlMatches } from './clientHelper';
|
||||||
|
import { BrowserContext } from './browserContext';
|
||||||
|
|
||||||
export type NetworkCookie = {
|
export type NetworkCookie = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -522,12 +523,12 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
|
||||||
export class FetchResponse {
|
export class FetchResponse {
|
||||||
private readonly _initializer: channels.FetchResponse;
|
private readonly _initializer: channels.FetchResponse;
|
||||||
private readonly _headers: Headers;
|
private readonly _headers: Headers;
|
||||||
private readonly _body: Buffer;
|
private readonly _context: BrowserContext;
|
||||||
|
|
||||||
constructor(initializer: channels.FetchResponse) {
|
constructor(context: BrowserContext, initializer: channels.FetchResponse) {
|
||||||
|
this._context = context;
|
||||||
this._initializer = initializer;
|
this._initializer = initializer;
|
||||||
this._headers = headersArrayToObject(this._initializer.headers, true /* lowerCase */);
|
this._headers = headersArrayToObject(this._initializer.headers, true /* lowerCase */);
|
||||||
this._body = Buffer.from(initializer.body, 'base64');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ok(): boolean {
|
ok(): boolean {
|
||||||
|
|
@ -551,7 +552,12 @@ export class FetchResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
async body(): Promise<Buffer> {
|
async body(): Promise<Buffer> {
|
||||||
return this._body;
|
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||||
|
const result = await channel.fetchResponseBody({ fetchUid: this._initializer.fetchUid });
|
||||||
|
if (!result.binary)
|
||||||
|
throw new Error('Response has been disposed');
|
||||||
|
return Buffer.from(result.binary!, 'base64');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async text(): Promise<string> {
|
async text(): Promise<string> {
|
||||||
|
|
@ -563,6 +569,12 @@ export class FetchResponse {
|
||||||
const content = await this.text();
|
const content = await this.text();
|
||||||
return JSON.parse(content);
|
return JSON.parse(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async dispose(): Promise<void> {
|
||||||
|
return this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||||
|
await channel.disposeFetchResponse({ fetchUid: this._initializer.fetchUid });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> implements api.WebSocket {
|
export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> implements api.WebSocket {
|
||||||
|
|
|
||||||
|
|
@ -122,12 +122,21 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
status: fetchResponse.status,
|
status: fetchResponse.status,
|
||||||
statusText: fetchResponse.statusText,
|
statusText: fetchResponse.statusText,
|
||||||
headers: fetchResponse.headers,
|
headers: fetchResponse.headers,
|
||||||
body: fetchResponse.body.toString('base64')
|
fetchUid: fetchResponse.fetchUid
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { response, error };
|
return { response, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchResponseBody(params: channels.BrowserContextFetchResponseBodyParams): Promise<channels.BrowserContextFetchResponseBodyResult> {
|
||||||
|
const buffer = this._context.fetchResponses.get(params.fetchUid);
|
||||||
|
return { binary: buffer ? buffer.toString('base64') : undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
async disposeFetchResponse(params: channels.BrowserContextDisposeFetchResponseParams): Promise<channels.BrowserContextDisposeFetchResponseResult> {
|
||||||
|
this._context.fetchResponses.delete(params.fetchUid);
|
||||||
|
}
|
||||||
|
|
||||||
async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
|
async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
|
||||||
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) };
|
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,11 +151,11 @@ export type InterceptedResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FetchResponse = {
|
export type FetchResponse = {
|
||||||
|
fetchUid: string,
|
||||||
url: string,
|
url: string,
|
||||||
status: number,
|
status: number,
|
||||||
statusText: string,
|
statusText: string,
|
||||||
headers: NameValue[],
|
headers: NameValue[],
|
||||||
body: Binary,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------- Root -----------
|
// ----------- Root -----------
|
||||||
|
|
@ -754,6 +754,8 @@ export interface BrowserContextChannel extends EventTargetChannel {
|
||||||
cookies(params: BrowserContextCookiesParams, metadata?: Metadata): Promise<BrowserContextCookiesResult>;
|
cookies(params: BrowserContextCookiesParams, metadata?: Metadata): Promise<BrowserContextCookiesResult>;
|
||||||
exposeBinding(params: BrowserContextExposeBindingParams, metadata?: Metadata): Promise<BrowserContextExposeBindingResult>;
|
exposeBinding(params: BrowserContextExposeBindingParams, metadata?: Metadata): Promise<BrowserContextExposeBindingResult>;
|
||||||
fetch(params: BrowserContextFetchParams, metadata?: Metadata): Promise<BrowserContextFetchResult>;
|
fetch(params: BrowserContextFetchParams, metadata?: Metadata): Promise<BrowserContextFetchResult>;
|
||||||
|
fetchResponseBody(params: BrowserContextFetchResponseBodyParams, metadata?: Metadata): Promise<BrowserContextFetchResponseBodyResult>;
|
||||||
|
disposeFetchResponse(params: BrowserContextDisposeFetchResponseParams, metadata?: Metadata): Promise<BrowserContextDisposeFetchResponseResult>;
|
||||||
grantPermissions(params: BrowserContextGrantPermissionsParams, metadata?: Metadata): Promise<BrowserContextGrantPermissionsResult>;
|
grantPermissions(params: BrowserContextGrantPermissionsParams, metadata?: Metadata): Promise<BrowserContextGrantPermissionsResult>;
|
||||||
newPage(params?: BrowserContextNewPageParams, metadata?: Metadata): Promise<BrowserContextNewPageResult>;
|
newPage(params?: BrowserContextNewPageParams, metadata?: Metadata): Promise<BrowserContextNewPageResult>;
|
||||||
setDefaultNavigationTimeoutNoReply(params: BrowserContextSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<BrowserContextSetDefaultNavigationTimeoutNoReplyResult>;
|
setDefaultNavigationTimeoutNoReply(params: BrowserContextSetDefaultNavigationTimeoutNoReplyParams, metadata?: Metadata): Promise<BrowserContextSetDefaultNavigationTimeoutNoReplyResult>;
|
||||||
|
|
@ -870,6 +872,22 @@ export type BrowserContextFetchResult = {
|
||||||
response?: FetchResponse,
|
response?: FetchResponse,
|
||||||
error?: string,
|
error?: string,
|
||||||
};
|
};
|
||||||
|
export type BrowserContextFetchResponseBodyParams = {
|
||||||
|
fetchUid: string,
|
||||||
|
};
|
||||||
|
export type BrowserContextFetchResponseBodyOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type BrowserContextFetchResponseBodyResult = {
|
||||||
|
binary?: Binary,
|
||||||
|
};
|
||||||
|
export type BrowserContextDisposeFetchResponseParams = {
|
||||||
|
fetchUid: string,
|
||||||
|
};
|
||||||
|
export type BrowserContextDisposeFetchResponseOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type BrowserContextDisposeFetchResponseResult = void;
|
||||||
export type BrowserContextGrantPermissionsParams = {
|
export type BrowserContextGrantPermissionsParams = {
|
||||||
permissions: string[],
|
permissions: string[],
|
||||||
origin?: string,
|
origin?: string,
|
||||||
|
|
|
||||||
|
|
@ -220,13 +220,13 @@ InterceptedResponse:
|
||||||
FetchResponse:
|
FetchResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
fetchUid: string
|
||||||
url: string
|
url: string
|
||||||
status: number
|
status: number
|
||||||
statusText: string
|
statusText: string
|
||||||
headers:
|
headers:
|
||||||
type: array
|
type: array
|
||||||
items: NameValue
|
items: NameValue
|
||||||
body: binary
|
|
||||||
|
|
||||||
LaunchOptions:
|
LaunchOptions:
|
||||||
type: mixin
|
type: mixin
|
||||||
|
|
@ -626,6 +626,16 @@ BrowserContext:
|
||||||
response: FetchResponse?
|
response: FetchResponse?
|
||||||
error: string?
|
error: string?
|
||||||
|
|
||||||
|
fetchResponseBody:
|
||||||
|
parameters:
|
||||||
|
fetchUid: string
|
||||||
|
returns:
|
||||||
|
binary?: binary
|
||||||
|
|
||||||
|
disposeFetchResponse:
|
||||||
|
parameters:
|
||||||
|
fetchUid: string
|
||||||
|
|
||||||
grantPermissions:
|
grantPermissions:
|
||||||
parameters:
|
parameters:
|
||||||
permissions:
|
permissions:
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,11 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
headers: tArray(tType('NameValue')),
|
headers: tArray(tType('NameValue')),
|
||||||
});
|
});
|
||||||
scheme.FetchResponse = tObject({
|
scheme.FetchResponse = tObject({
|
||||||
|
fetchUid: tString,
|
||||||
url: tString,
|
url: tString,
|
||||||
status: tNumber,
|
status: tNumber,
|
||||||
statusText: tString,
|
statusText: tString,
|
||||||
headers: tArray(tType('NameValue')),
|
headers: tArray(tType('NameValue')),
|
||||||
body: tBinary,
|
|
||||||
});
|
});
|
||||||
scheme.RootInitializeParams = tObject({
|
scheme.RootInitializeParams = tObject({
|
||||||
sdkLanguage: tString,
|
sdkLanguage: tString,
|
||||||
|
|
@ -399,6 +399,12 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
postData: tOptional(tBinary),
|
postData: tOptional(tBinary),
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
});
|
});
|
||||||
|
scheme.BrowserContextFetchResponseBodyParams = tObject({
|
||||||
|
fetchUid: tString,
|
||||||
|
});
|
||||||
|
scheme.BrowserContextDisposeFetchResponseParams = tObject({
|
||||||
|
fetchUid: tString,
|
||||||
|
});
|
||||||
scheme.BrowserContextGrantPermissionsParams = tObject({
|
scheme.BrowserContextGrantPermissionsParams = tObject({
|
||||||
permissions: tArray(tString),
|
permissions: tArray(tString),
|
||||||
origin: tOptional(tString),
|
origin: tOptional(tString),
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
private _origins = new Set<string>();
|
private _origins = new Set<string>();
|
||||||
readonly _harRecorder: HarRecorder | undefined;
|
readonly _harRecorder: HarRecorder | undefined;
|
||||||
readonly tracing: Tracing;
|
readonly tracing: Tracing;
|
||||||
|
readonly fetchResponses: Map<string, Buffer> = new Map();
|
||||||
|
|
||||||
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
|
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
|
||||||
super(browser, 'browser-context');
|
super(browser, 'browser-context');
|
||||||
|
|
@ -381,6 +382,12 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
this.on(BrowserContext.Events.Page, installInPage);
|
this.on(BrowserContext.Events.Page, installInPage);
|
||||||
return Promise.all(this.pages().map(installInPage));
|
return Promise.all(this.pages().map(installInPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storeFetchResponseBody(body: Buffer): string {
|
||||||
|
const uid = createGuid();
|
||||||
|
this.fetchResponses.set(uid, body);
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertBrowserContextIsNotOwned(context: BrowserContext) {
|
export function assertBrowserContextIsNotOwned(context: BrowserContext) {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import * as types from './types';
|
||||||
import { pipeline, Readable, Transform } from 'stream';
|
import { pipeline, Readable, Transform } from 'stream';
|
||||||
import { monotonicTime } from '../utils/utils';
|
import { monotonicTime } from '../utils/utils';
|
||||||
|
|
||||||
export async function playwrightFetch(context: BrowserContext, params: types.FetchOptions): Promise<{fetchResponse?: types.FetchResponse, error?: string}> {
|
export async function playwrightFetch(context: BrowserContext, params: types.FetchOptions): Promise<{fetchResponse?: Omit<types.FetchResponse, 'body'> & { fetchUid: string }, error?: string}> {
|
||||||
try {
|
try {
|
||||||
const headers: { [name: string]: string } = {};
|
const headers: { [name: string]: string } = {};
|
||||||
if (params.headers) {
|
if (params.headers) {
|
||||||
|
|
@ -62,7 +62,8 @@ export async function playwrightFetch(context: BrowserContext, params: types.Fet
|
||||||
timeout,
|
timeout,
|
||||||
deadline
|
deadline
|
||||||
}, params.postData);
|
}, params.postData);
|
||||||
return { fetchResponse };
|
const fetchUid = context.storeFetchResponseBody(fetchResponse.body);
|
||||||
|
return { fetchResponse: { ...fetchResponse, fetchUid } };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { error: String(e) };
|
return { error: String(e) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -613,3 +613,22 @@ it('should respect timeout after redirects', async function({context, server}) {
|
||||||
const error = await context._fetch(server.PREFIX + '/redirect').catch(e => e);
|
const error = await context._fetch(server.PREFIX + '/redirect').catch(e => e);
|
||||||
expect(error.message).toContain(`Request timed out after 100ms`);
|
expect(error.message).toContain(`Request timed out after 100ms`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should dispose', async function({context, server}) {
|
||||||
|
// @ts-expect-error
|
||||||
|
const response = await context._fetch(server.PREFIX + '/simple.json');
|
||||||
|
expect(await response.json()).toEqual({ foo: 'bar' });
|
||||||
|
await response.dispose();
|
||||||
|
const error = await response.body().catch(e => e);
|
||||||
|
expect(error.message).toContain('Response has been disposed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispose when context closes', async function({context, server}) {
|
||||||
|
// @ts-expect-error
|
||||||
|
const response = await context._fetch(server.PREFIX + '/simple.json');
|
||||||
|
expect(await response.json()).toEqual({ foo: 'bar' });
|
||||||
|
await context.close();
|
||||||
|
const error = await response.body().catch(e => e);
|
||||||
|
expect(error.message).toContain('Target page, context or browser has been closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue