Skip to content

Commit a0c3f27

Browse files
committed
chore(bidi): add support for fetching request bodies
1 parent 34a09f3 commit a0c3f27

File tree

11 files changed

+55
-40
lines changed

11 files changed

+55
-40
lines changed

packages/playwright-core/src/server/bidi/bidiBrowser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class BidiBrowser extends Browser {
7474
});
7575

7676
await browser._browserSession.send('network.addDataCollector', {
77-
dataTypes: [bidi.Network.DataType.Response],
77+
dataTypes: [bidi.Network.DataType.Request, bidi.Network.DataType.Response],
7878
maxEncodedDataSize: 20_000_000, // same default as in CDP: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_network_agent.cc;l=134;drc=4128411589187a396829a827f59a655bed876aa7
7979
});
8080

packages/playwright-core/src/server/bidi/bidiNetworkManager.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ export class BidiNetworkManager {
7979
route = new BidiRouteImpl(this._session, param.request.request);
8080
}
8181
}
82-
const request = new BidiRequest(frame, redirectedFrom, param, route);
82+
const getRequestBody = param.request.bodySize ? () => getNetworkData(this._session, param.request, bidi.Network.DataType.Request) : null;
83+
const request = new BidiRequest(frame, redirectedFrom, param, getRequestBody, route);
8384
this._requests.set(request._id, request);
8485
this._page.frameManager.requestStarted(request.request, route);
8586
}
@@ -88,11 +89,7 @@ export class BidiNetworkManager {
8889
const request = this._requests.get(params.request.request);
8990
if (!request)
9091
return;
91-
const getResponseBody = async () => {
92-
const { bytes } = await this._session.send('network.getData', { request: params.request.request, dataType: bidi.Network.DataType.Response });
93-
const encoding = bytes.type === 'base64' ? 'base64' : 'utf8';
94-
return Buffer.from(bytes.value, encoding);
95-
};
92+
const getResponseBody = () => getNetworkData(this._session, params.request, bidi.Network.DataType.Response);
9693
const timings = params.request.timings;
9794
const startTime = timings.requestTime;
9895
function relativeToStart(time: number): number {
@@ -236,14 +233,12 @@ class BidiRequest {
236233
// store the first and only Route in the chain (if any).
237234
_originalRequestRoute: BidiRouteImpl | undefined;
238235

239-
constructor(frame: frames.Frame, redirectedFrom: BidiRequest | null, payload: bidi.Network.BeforeRequestSentParameters, route: BidiRouteImpl | undefined) {
236+
constructor(frame: frames.Frame, redirectedFrom: BidiRequest | null, payload: bidi.Network.BeforeRequestSentParameters, getRequestBody: (() => Promise<Buffer>) | null, route: BidiRouteImpl | undefined) {
240237
this._id = payload.request.request;
241238
if (redirectedFrom)
242239
redirectedFrom._redirectedTo = this;
243-
// TODO: missing in the spec?
244-
const postDataBuffer = null;
245240
this.request = new network.Request(frame._page.browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigation ?? undefined, payload.request.url,
246-
resourceTypeFromBidi(payload.request.destination, payload.request.initiatorType, payload.initiator?.type), payload.request.method, postDataBuffer, fromBidiHeaders(payload.request.headers));
241+
resourceTypeFromBidi(payload.request.destination, payload.request.initiatorType, payload.initiator?.type), payload.request.method, null, fromBidiHeaders(payload.request.headers), getRequestBody);
247242
// "raw" headers are the same as "provisional" headers in Bidi.
248243
this.request.setRawRequestHeaders(null);
249244
this.request._setBodySize(payload.request.bodySize || 0);
@@ -390,3 +385,9 @@ function resourceTypeFromBidi(requestDestination: string, requestInitiatorType:
390385
default: return 'other';
391386
}
392387
}
388+
389+
async function getNetworkData(session: BidiSession, request: bidi.Network.RequestData, dataType: bidi.Network.DataType) {
390+
const { bytes } = await session.send('network.getData', { request: request.request, dataType });
391+
const encoding = bytes.type === 'base64' ? 'base64' : 'utf8';
392+
return Buffer.from(bytes.value, encoding);
393+
}

packages/playwright-core/src/server/bidi/third_party/bidiProtocolCore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,6 +1383,7 @@ export namespace Network {
13831383
}
13841384
export namespace Network {
13851385
export const enum DataType {
1386+
Request = 'request',
13861387
Response = 'response',
13871388
}
13881389
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
6767
}
6868

6969
async body(params: channels.RequestBodyParams, progress: Progress): Promise<channels.RequestBodyResult> {
70-
const postData = this._object.postDataBuffer();
70+
const postData = await this._object.body();
7171
return { body: postData === null ? undefined : postData };
7272
}
7373

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export class Request extends SdkObject {
105105
private _resourceType: string;
106106
private _method: string;
107107
private _postData: Buffer | null;
108+
private _bodyCallback: (() => Promise<Buffer>) | null;
108109
readonly _headers: HeadersArray;
109110
private _headersMap = new Map<string, string>();
110111
readonly _frame: frames.Frame | null = null;
@@ -122,7 +123,7 @@ export class Request extends SdkObject {
122123
};
123124

124125
constructor(context: contexts.BrowserContext, frame: frames.Frame | null, serviceWorker: pages.Worker | null, redirectedFrom: Request | null, documentId: string | undefined,
125-
url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray) {
126+
url: string, resourceType: string, method: string, postData: Buffer | null, headers: HeadersArray, bodyCallback: (() => Promise<Buffer>) | null = null) {
126127
super(frame || context, 'request');
127128
assert(!url.startsWith('data:'), 'Data urls should not fire requests');
128129
this._context = context;
@@ -136,6 +137,7 @@ export class Request extends SdkObject {
136137
this._resourceType = resourceType;
137138
this._method = method;
138139
this._postData = postData;
140+
this._bodyCallback = bodyCallback;
139141
this._headers = headers;
140142
this._updateHeadersMap();
141143
this._isFavicon = url.endsWith('/favicon.ico') || !!redirectedFrom?._isFavicon;
@@ -173,6 +175,15 @@ export class Request extends SdkObject {
173175
return this._overrides?.method || this._method;
174176
}
175177

178+
async body(): Promise<Buffer | null> {
179+
if (this._overrides?.postData)
180+
return this._overrides?.postData;
181+
if (this._bodyCallback)
182+
return await this._bodyCallback();
183+
else
184+
return this._postData;
185+
}
186+
176187
postDataBuffer(): Buffer | null {
177188
return this._overrides?.postData || this._postData;
178189
}

tests/library/browsercontext-har.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ it('should record overridden requests to har', async ({ contextFactory, server }
342342
await page1.route('**/echo_redir', async route => {
343343
await route.fallback({
344344
url: server.PREFIX + '/echo',
345-
postData: +route.request().postData() + 10,
345+
postData: +(await route.request().body()) + 10,
346346
});
347347
});
348348
expect(await page1.evaluate(fetchFunction, { path: '/echo_redir', body: '1' })).toBe('11');

tests/library/browsercontext-route.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ import type { Route } from '@playwright/test';
2121
it('should intercept', async ({ browser, server }) => {
2222
const context = await browser.newContext();
2323
let intercepted = false;
24-
await context.route('**/empty.html', route => {
24+
await context.route('**/empty.html', async route => {
2525
intercepted = true;
2626
const request = route.request();
2727
expect(request.url()).toContain('empty.html');
2828
expect(request.headers()['user-agent']).toBeTruthy();
2929
expect(request.method()).toBe('GET');
30-
expect(request.postData()).toBe(null);
30+
expect(await request.body()).toBe(null);
3131
expect(request.isNavigationRequest()).toBe(true);
3232
expect(request.resourceType()).toBe('document');
3333
expect(request.frame() === page.mainFrame()).toBe(true);

tests/page/network-post-data.spec.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
import { test as it, expect } from './pageTest';
1818

19-
it('should return correct postData buffer for utf-8 body', async ({ page, server }) => {
19+
it.skip(({ channel }) => channel?.startsWith('bidi-chrom') || channel?.startsWith('moz-firefox'), 'request.postData is not supported with BiDi');
20+
21+
it('should return correct postData buffer for utf-8 body', async ({ page, server, channel }) => {
2022
await page.goto(server.EMPTY_PAGE);
2123
const value = 'baẞ';
2224
const [request] = await Promise.all([
@@ -34,7 +36,7 @@ it('should return correct postData buffer for utf-8 body', async ({ page, server
3436
expect(request.postDataJSON()).toBe(value);
3537
});
3638

37-
it('should return post data w/o content-type @smoke', async ({ page, server }) => {
39+
it('should return post data w/o content-type @smoke', async ({ page, server, channel }) => {
3840
await page.goto(server.EMPTY_PAGE);
3941
const [request] = await Promise.all([
4042
page.waitForRequest('**'),
@@ -50,7 +52,7 @@ it('should return post data w/o content-type @smoke', async ({ page, server }) =
5052
expect(request.postDataJSON()).toEqual({ value: 42 });
5153
});
5254

53-
it('should throw on invalid JSON in post data', async ({ page, server }) => {
55+
it('should throw on invalid JSON in post data', async ({ page, server, channel }) => {
5456
await page.goto(server.EMPTY_PAGE);
5557
const [request] = await Promise.all([
5658
page.waitForRequest('**'),
@@ -71,7 +73,7 @@ it('should throw on invalid JSON in post data', async ({ page, server }) => {
7173
expect(error.message).toContain('POST data is not a valid JSON object: <not a json>');
7274
});
7375

74-
it('should return post data for PUT requests', async ({ page, server }) => {
76+
it('should return post data for PUT requests', async ({ page, server, channel }) => {
7577
await page.goto(server.EMPTY_PAGE);
7678
const [request] = await Promise.all([
7779
page.waitForRequest('**'),
@@ -86,7 +88,7 @@ it('should return post data for PUT requests', async ({ page, server }) => {
8688
expect(request.postDataJSON()).toEqual({ value: 42 });
8789
});
8890

89-
it('should get post data for file/blob', async ({ page, server, browserName }) => {
91+
it('should get post data for file/blob', async ({ page, server, browserName, channel }) => {
9092
it.fail(browserName === 'webkit' || browserName === 'chromium');
9193
await page.goto(server.EMPTY_PAGE);
9294
const [request] = await Promise.all([
@@ -106,7 +108,7 @@ it('should get post data for file/blob', async ({ page, server, browserName }) =
106108
expect(request.postData()).toBe('file-contents');
107109
});
108110

109-
it('should get post data for navigator.sendBeacon api calls', async ({ page, server, browserName }) => {
111+
it('should get post data for navigator.sendBeacon api calls', async ({ page, server, browserName, channel }) => {
110112
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/12231' });
111113
it.fail(browserName === 'chromium', 'postData is empty');
112114
it.fail(browserName === 'webkit', 'postData is empty');

tests/page/page-network-request.spec.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ it('should return postData', async ({ page, server }) => {
214214
page.on('request', r => request = r);
215215
await page.evaluate(() => fetch('./post', { method: 'POST', body: JSON.stringify({ foo: 'bar' }) }));
216216
expect(request).toBeTruthy();
217-
expect(request.postData()).toBe('{"foo":"bar"}');
217+
expect(await request.body()).toBe('{"foo":"bar"}');
218218
});
219219

220220
it('should work with binary post data', async ({ page, server }) => {
@@ -226,7 +226,7 @@ it('should work with binary post data', async ({ page, server }) => {
226226
await fetch('./post', { method: 'POST', body: new Uint8Array(Array.from(Array(256).keys())) });
227227
});
228228
expect(request).toBeTruthy();
229-
const buffer = request.postDataBuffer();
229+
const buffer = await request.bodyBuffer();
230230
expect(buffer.length).toBe(256);
231231
for (let i = 0; i < 256; ++i)
232232
expect(buffer[i]).toBe(i);
@@ -242,7 +242,7 @@ it('should work with binary post data and interception', async ({ page, server }
242242
await fetch('./post', { method: 'POST', body: new Uint8Array(Array.from(Array(256).keys())) });
243243
});
244244
expect(request).toBeTruthy();
245-
const buffer = request.postDataBuffer();
245+
const buffer = await request.bodyBuffer();
246246
expect(buffer.length).toBe(256);
247247
for (let i = 0; i < 256; ++i)
248248
expect(buffer[i]).toBe(i);
@@ -255,12 +255,12 @@ it('should override post data content type', async ({ page, server }) => {
255255
request = req;
256256
res.end();
257257
});
258-
await page.route('**/post', (route, request) => {
258+
await page.route('**/post', async (route, request) => {
259259
const headers = request.headers();
260260
headers['content-type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
261261
void route.continue({
262262
headers,
263-
postData: request.postData()
263+
postData: await request.body()
264264
});
265265
});
266266
await page.evaluate(async () => {
@@ -270,9 +270,9 @@ it('should override post data content type', async ({ page, server }) => {
270270
expect(request.headers['content-type']).toBe('application/x-www-form-urlencoded; charset=UTF-8');
271271
});
272272

273-
it('should get |undefined| with postData() when there is no post data', async ({ page, server }) => {
273+
it('should get |undefined| with body() when there is no post data', async ({ page, server }) => {
274274
const response = await page.goto(server.EMPTY_PAGE);
275-
expect(response.request().postData()).toBe(null);
275+
expect(await response.request().body()).toBe(null);
276276
});
277277

278278
it('should parse the json post data', async ({ page, server }) => {
@@ -282,7 +282,7 @@ it('should parse the json post data', async ({ page, server }) => {
282282
page.on('request', r => request = r);
283283
await page.evaluate(() => fetch('./post', { method: 'POST', body: JSON.stringify({ foo: 'bar' }) }));
284284
expect(request).toBeTruthy();
285-
expect(request.postDataJSON()).toEqual({ 'foo': 'bar' });
285+
expect(await request.bodyJSON()).toEqual({ 'foo': 'bar' });
286286
});
287287

288288
it('should parse the data if content-type is application/x-www-form-urlencoded', async ({ page, server }) => {
@@ -293,7 +293,7 @@ it('should parse the data if content-type is application/x-www-form-urlencoded',
293293
await page.setContent(`<form method='POST' action='/post'><input type='text' name='foo' value='bar'><input type='number' name='baz' value='123'><input type='submit'></form>`);
294294
await page.click('input[type=submit]');
295295
expect(request).toBeTruthy();
296-
expect(request.postDataJSON()).toEqual({ 'foo': 'bar', 'baz': '123' });
296+
expect(await request.bodyJSON()).toEqual({ 'foo': 'bar', 'baz': '123' });
297297
});
298298

299299
it('should parse the data if content-type is application/x-www-form-urlencoded; charset=UTF-8', async ({ page, server }) => {
@@ -307,12 +307,12 @@ it('should parse the data if content-type is application/x-www-form-urlencoded;
307307
},
308308
body: 'foo=bar&baz=123'
309309
}));
310-
expect((await requestPromise).postDataJSON()).toEqual({ 'foo': 'bar', 'baz': '123' });
310+
expect(await (await requestPromise).bodyJSON()).toEqual({ 'foo': 'bar', 'baz': '123' });
311311
});
312312

313-
it('should get |undefined| with postDataJSON() when there is no post data', async ({ page, server }) => {
313+
it('should get |undefined| with bodyJSON() when there is no post data', async ({ page, server }) => {
314314
const response = await page.goto(server.EMPTY_PAGE);
315-
expect(response.request().postDataJSON()).toBe(null);
315+
expect(await response.request().bodyJSON()).toBe(null);
316316
});
317317

318318
it('should return multipart/form-data', async ({ page, server, browserName, browserMajorVersion }) => {
@@ -337,7 +337,7 @@ it('should return multipart/form-data', async ({ page, server, browserName, brow
337337
expect(contentType).toMatch(re);
338338
const b = contentType.match(re)[1]!;
339339
const expected = `--${b}\r\nContent-Disposition: form-data; name=\"name1\"\r\n\r\nvalue1\r\n--${b}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"foo.txt\"\r\nContent-Type: application/octet-stream\r\n\r\nfile-value\r\n--${b}\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nvalue2\r\n--${b}\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nanother-value2\r\n--${b}--\r\n`;
340-
expect(request.postDataBuffer().toString('utf8')).toEqual(expected);
340+
expect((await request.bodyBuffer()).toString('utf8')).toEqual(expected);
341341
});
342342

343343
it('should return event source', async ({ page, server }) => {

tests/page/page-request-intercept.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ it('should intercept multipart/form-data request body', async ({ page, server, a
191191
page.click('input[type=submit]', { noWaitAfter: true })
192192
]);
193193
expect(request.method()).toBe('POST');
194-
expect(request.postData()).toContain(fs.readFileSync(filePath, 'utf8'));
194+
expect(await request.body()).toContain(fs.readFileSync(filePath, 'utf8'));
195195
});
196196

197197
it('should fulfill intercepted response using alias', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
@@ -307,7 +307,7 @@ it('request.postData is not null when fetching FormData with a Blob', {
307307
const postDataPromise = new Promise<string>(resolve => resolvePostData = resolve);
308308
await page.route(server.PREFIX + '/upload', async (route, request) => {
309309
expect(request.method()).toBe('POST');
310-
resolvePostData(await request.postData());
310+
resolvePostData(await request.body());
311311
await route.fulfill({
312312
status: 200,
313313
body: 'ok',

0 commit comments

Comments
 (0)