Skip to content

Commit 130a0fa

Browse files
authored
feat: add request.body async getters (#38179)
1 parent 454f154 commit 130a0fa

File tree

11 files changed

+248
-21
lines changed

11 files changed

+248
-21
lines changed

docs/src/api/class-request.md

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,32 @@ request is issued to a redirected url.
2323

2424
An object with all the request HTTP headers associated with this request. The header names are lower-cased.
2525

26+
## async method: Request.body
27+
* since: v1.57
28+
- returns: <[null]|[string]>
29+
30+
The request body, if present.
31+
32+
## async method: Request.bodyBuffer
33+
* since: v1.57
34+
- returns: <[null]|[Buffer]>
35+
36+
The request body in a binary form. Returns null if the request has no body.
37+
38+
## async method: Request.bodyJSON
39+
* since: v1.57
40+
* langs: js, python
41+
- returns: <[null]|[Serializable]>
42+
43+
Returns the request body as a parsed JSON object. If the request `Content-Type` is `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise, it parses the body as JSON.
44+
45+
## async method: Request.bodyJSON
46+
* since: v1.57
47+
* langs: csharp
48+
- returns: <[null]|[JsonElement]>
49+
50+
Returns the request body as a parsed JSON object. If the request `Content-Type` is `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise, it parses the body as JSON.
51+
2652
## method: Request.failure
2753
* since: v1.8
2854
- returns: <[null]|[string]>
@@ -149,35 +175,33 @@ Request's method (GET, POST, etc.)
149175

150176
## method: Request.postData
151177
* since: v1.8
178+
* discouraged: Use [`method: Request.body`] instead.
152179
- returns: <[null]|[string]>
153180

154-
Request's post body, if any.
181+
The request body, if present.
155182

156183
## method: Request.postDataBuffer
157184
* since: v1.8
185+
* discouraged: Use [`method: Request.bodyBuffer`] instead.
158186
- returns: <[null]|[Buffer]>
159187

160-
Request's post body in a binary form, if any.
188+
The request body in a binary form. Returns null if the request has no body.
161189

162190
## method: Request.postDataJSON
163191
* since: v1.8
164192
* langs: js, python
193+
* discouraged: Use [`method: Request.bodyJSON`] instead.
165194
- returns: <[null]|[Serializable]>
166195

167-
Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any.
168-
169-
When the response is `application/x-www-form-urlencoded` then a key/value object of the values will be returned.
170-
Otherwise it will be parsed as JSON.
196+
Returns the request body as a parsed JSON object. If the request `Content-Type` is `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise, it parses the body as JSON.
171197

172198
## method: Request.postDataJSON
173199
* since: v1.12
174200
* langs: csharp
201+
* discouraged: Use [`method: Request.bodyJSON`] instead.
175202
- returns: <[null]|[JsonElement]>
176203

177-
Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any.
178-
179-
When the response is `application/x-www-form-urlencoded` then a key/value object of the values will be returned.
180-
Otherwise it will be parsed as JSON.
204+
Returns the request body as a parsed JSON object. If the request `Content-Type` is `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise, it parses the body as JSON.
181205

182206
## method: Request.redirectedFrom
183207
* since: v1.8

packages/playwright-client/types/types.d.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20311,6 +20311,23 @@ export interface Request {
2031120311
*/
2031220312
allHeaders(): Promise<{ [key: string]: string; }>;
2031320313

20314+
/**
20315+
* The request body, if present.
20316+
*/
20317+
body(): Promise<null|string>;
20318+
20319+
/**
20320+
* The request body in a binary form. Returns null if the request has no body.
20321+
*/
20322+
bodyBuffer(): Promise<null|Buffer>;
20323+
20324+
/**
20325+
* Returns the request body as a parsed JSON object. If the request `Content-Type` is
20326+
* `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise,
20327+
* it parses the body as JSON.
20328+
*/
20329+
bodyJSON(): Promise<null|Serializable>;
20330+
2031420331
/**
2031520332
* The method returns `null` unless this request has failed, as reported by `requestfailed` event.
2031620333
*
@@ -20408,20 +20425,25 @@ export interface Request {
2040820425
method(): string;
2040920426

2041020427
/**
20411-
* Request's post body, if any.
20428+
* **NOTE** Use [request.body()](https://playwright.dev/docs/api/class-request#request-body) instead.
20429+
*
20430+
* The request body, if present.
2041220431
*/
2041320432
postData(): null|string;
2041420433

2041520434
/**
20416-
* Request's post body in a binary form, if any.
20435+
* **NOTE** Use [request.bodyBuffer()](https://playwright.dev/docs/api/class-request#request-body-buffer) instead.
20436+
*
20437+
* The request body in a binary form. Returns null if the request has no body.
2041720438
*/
2041820439
postDataBuffer(): null|Buffer;
2041920440

2042020441
/**
20421-
* Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any.
20442+
* **NOTE** Use [request.bodyJSON()](https://playwright.dev/docs/api/class-request#request-body-json) instead.
2042220443
*
20423-
* When the response is `application/x-www-form-urlencoded` then a key/value object of the values will be returned.
20424-
* Otherwise it will be parsed as JSON.
20444+
* Returns the request body as a parsed JSON object. If the request `Content-Type` is
20445+
* `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise,
20446+
* it parses the body as JSON.
2042520447
*/
2042620448
postDataJSON(): null|Serializable;
2042720449

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
134134
return this._fallbackOverrides.method || this._initializer.method;
135135
}
136136

137+
async body(): Promise<string | null> {
138+
return (this._fallbackOverrides.postDataBuffer || (await this._channel.body()).body)?.toString('utf-8') || null;
139+
}
140+
141+
async bodyBuffer(): Promise<Buffer | null> {
142+
return this._fallbackOverrides.postDataBuffer || (await this._channel.body()).body || null;
143+
}
144+
145+
async bodyJSON(): Promise<Object | null> {
146+
return this._postDataJSON(await this.body());
147+
}
148+
137149
postData(): string | null {
138150
return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.toString('utf-8') || null;
139151
}
@@ -144,6 +156,10 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
144156

145157
postDataJSON(): Object | null {
146158
const postData = this.postData();
159+
return this._postDataJSON(postData);
160+
}
161+
162+
private _postDataJSON(postData: string | null): Object | null {
147163
if (!postData)
148164
return null;
149165

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2221,6 +2221,10 @@ scheme.RequestInitializer = tObject({
22212221
hasResponse: tBoolean,
22222222
});
22232223
scheme.RequestResponseEvent = tOptional(tObject({}));
2224+
scheme.RequestBodyParams = tOptional(tObject({}));
2225+
scheme.RequestBodyResult = tObject({
2226+
body: tOptional(tBinary),
2227+
});
22242228
scheme.RequestResponseParams = tOptional(tObject({}));
22252229
scheme.RequestResponseResult = tObject({
22262230
response: tOptional(tChannel(['Response'])),

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
6666
this.addObjectListener(Request.Events.Response, () => this._dispatchEvent('response', {}));
6767
}
6868

69+
async body(params: channels.RequestBodyParams, progress: Progress): Promise<channels.RequestBodyResult> {
70+
const postData = this._object.postDataBuffer();
71+
return { body: postData === null ? undefined : postData };
72+
}
73+
6974
async rawRequestHeaders(params: channels.RequestRawRequestHeadersParams, progress: Progress): Promise<channels.RequestRawRequestHeadersResult> {
7075
return { headers: await progress.race(this._object.rawRequestHeaders()) };
7176
}

packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ export const methodMetainfo = new Map<string, { internal?: boolean, title?: stri
234234
['ElementHandle.uncheck', { title: 'Uncheck', slowMo: true, snapshot: true, pausesBeforeInput: true, }],
235235
['ElementHandle.waitForElementState', { title: 'Wait for state', snapshot: true, pausesBeforeAction: true, }],
236236
['ElementHandle.waitForSelector', { title: 'Wait for selector', snapshot: true, }],
237+
['Request.body', { title: 'Get request body', group: 'getter', }],
237238
['Request.response', { internal: true, }],
238239
['Request.rawRequestHeaders', { internal: true, }],
239240
['Route.redirectNavigationRequest', { internal: true, }],

packages/playwright-core/types/types.d.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20311,6 +20311,23 @@ export interface Request {
2031120311
*/
2031220312
allHeaders(): Promise<{ [key: string]: string; }>;
2031320313

20314+
/**
20315+
* The request body, if present.
20316+
*/
20317+
body(): Promise<null|string>;
20318+
20319+
/**
20320+
* The request body in a binary form. Returns null if the request has no body.
20321+
*/
20322+
bodyBuffer(): Promise<null|Buffer>;
20323+
20324+
/**
20325+
* Returns the request body as a parsed JSON object. If the request `Content-Type` is
20326+
* `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise,
20327+
* it parses the body as JSON.
20328+
*/
20329+
bodyJSON(): Promise<null|Serializable>;
20330+
2031420331
/**
2031520332
* The method returns `null` unless this request has failed, as reported by `requestfailed` event.
2031620333
*
@@ -20408,20 +20425,25 @@ export interface Request {
2040820425
method(): string;
2040920426

2041020427
/**
20411-
* Request's post body, if any.
20428+
* **NOTE** Use [request.body()](https://playwright.dev/docs/api/class-request#request-body) instead.
20429+
*
20430+
* The request body, if present.
2041220431
*/
2041320432
postData(): null|string;
2041420433

2041520434
/**
20416-
* Request's post body in a binary form, if any.
20435+
* **NOTE** Use [request.bodyBuffer()](https://playwright.dev/docs/api/class-request#request-body-buffer) instead.
20436+
*
20437+
* The request body in a binary form. Returns null if the request has no body.
2041720438
*/
2041820439
postDataBuffer(): null|Buffer;
2041920440

2042020441
/**
20421-
* Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any.
20442+
* **NOTE** Use [request.bodyJSON()](https://playwright.dev/docs/api/class-request#request-body-json) instead.
2042220443
*
20423-
* When the response is `application/x-www-form-urlencoded` then a key/value object of the values will be returned.
20424-
* Otherwise it will be parsed as JSON.
20444+
* Returns the request body as a parsed JSON object. If the request `Content-Type` is
20445+
* `application/x-www-form-urlencoded`, this method returns a key/value object parsed from the form data. Otherwise,
20446+
* it parses the body as JSON.
2042520447
*/
2042620448
postDataJSON(): null|Serializable;
2042720449

packages/protocol/src/channels.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3815,10 +3815,16 @@ export interface RequestEventTarget {
38153815
}
38163816
export interface RequestChannel extends RequestEventTarget, Channel {
38173817
_type_Request: boolean;
3818+
body(params?: RequestBodyParams, progress?: Progress): Promise<RequestBodyResult>;
38183819
response(params?: RequestResponseParams, progress?: Progress): Promise<RequestResponseResult>;
38193820
rawRequestHeaders(params?: RequestRawRequestHeadersParams, progress?: Progress): Promise<RequestRawRequestHeadersResult>;
38203821
}
38213822
export type RequestResponseEvent = {};
3823+
export type RequestBodyParams = {};
3824+
export type RequestBodyOptions = {};
3825+
export type RequestBodyResult = {
3826+
body?: Binary,
3827+
};
38223828
export type RequestResponseParams = {};
38233829
export type RequestResponseOptions = {};
38243830
export type RequestResponseResult = {

packages/protocol/src/protocol.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3377,6 +3377,12 @@ Request:
33773377

33783378
commands:
33793379

3380+
body:
3381+
title: Get request body
3382+
group: getter
3383+
returns:
3384+
body: binary?
3385+
33803386
response:
33813387
internal: true
33823388
returns:
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { test as it, expect } from './pageTest';
18+
19+
it('should return correct request body buffer for utf-8 body', async ({ page, server }) => {
20+
await page.goto(server.EMPTY_PAGE);
21+
const value = 'baẞ';
22+
const [request] = await Promise.all([
23+
page.waitForRequest('**'),
24+
page.evaluate(({ url, value }) => {
25+
const request = new Request(url, {
26+
method: 'POST',
27+
body: JSON.stringify(value),
28+
});
29+
request.headers.set('content-type', 'application/json;charset=UTF-8');
30+
return fetch(request);
31+
}, { url: server.PREFIX + '/title.html', value })
32+
]);
33+
expect((await request.bodyBuffer()).equals(Buffer.from(JSON.stringify(value), 'utf-8'))).toBe(true);
34+
expect(await request.bodyJSON()).toBe(value);
35+
});
36+
37+
it('should return request body w/o content-type @smoke', async ({ page, server }) => {
38+
await page.goto(server.EMPTY_PAGE);
39+
const [request] = await Promise.all([
40+
page.waitForRequest('**'),
41+
page.evaluate(({ url }) => {
42+
const request = new Request(url, {
43+
method: 'POST',
44+
body: JSON.stringify({ value: 42 }),
45+
});
46+
request.headers.set('content-type', '');
47+
return fetch(request);
48+
}, { url: server.PREFIX + '/title.html' })
49+
]);
50+
expect(await request.bodyJSON()).toEqual({ value: 42 });
51+
});
52+
53+
it('should throw on invalid JSON in post data', async ({ page, server }) => {
54+
await page.goto(server.EMPTY_PAGE);
55+
const [request] = await Promise.all([
56+
page.waitForRequest('**'),
57+
page.evaluate(({ url }) => {
58+
const request = new Request(url, {
59+
method: 'POST',
60+
body: '<not a json>',
61+
});
62+
return fetch(request);
63+
}, { url: server.PREFIX + '/title.html' })
64+
]);
65+
let error;
66+
try {
67+
await request.bodyJSON();
68+
} catch (e) {
69+
error = e;
70+
}
71+
expect(error.message).toContain('POST data is not a valid JSON object: <not a json>');
72+
});
73+
74+
it('should return body for PUT requests', async ({ page, server }) => {
75+
await page.goto(server.EMPTY_PAGE);
76+
const [request] = await Promise.all([
77+
page.waitForRequest('**'),
78+
page.evaluate(({ url }) => {
79+
const request = new Request(url, {
80+
method: 'PUT',
81+
body: JSON.stringify({ value: 42 }),
82+
});
83+
return fetch(request);
84+
}, { url: server.PREFIX + '/title.html' })
85+
]);
86+
expect(await request.bodyJSON()).toEqual({ value: 42 });
87+
});
88+
89+
it('should get request body for file/blob', async ({ page, server, browserName }) => {
90+
it.fail(browserName === 'webkit' || browserName === 'chromium');
91+
await page.goto(server.EMPTY_PAGE);
92+
const [request] = await Promise.all([
93+
page.waitForRequest('**/*'),
94+
page.evaluate(() => {
95+
const file = new File(['file-contents'], 'filename.txt');
96+
97+
void fetch('/data', {
98+
method: 'POST',
99+
headers: {
100+
'content-type': 'application/octet-stream'
101+
},
102+
body: file
103+
});
104+
})
105+
]);
106+
expect(await request.body()).toBe('file-contents');
107+
});
108+
109+
it('should get request body for navigator.sendBeacon api calls', async ({ page, server, browserName }) => {
110+
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/12231' });
111+
it.fail(browserName === 'chromium', 'body is empty');
112+
it.fail(browserName === 'webkit', 'body is empty');
113+
await page.goto(server.EMPTY_PAGE);
114+
const [request] = await Promise.all([
115+
page.waitForRequest('**/*'),
116+
page.evaluate(() => navigator.sendBeacon(window.location.origin + '/api/foo', new Blob([JSON.stringify({ foo: 'bar' })])))
117+
]);
118+
expect(request.method()).toBe('POST');
119+
expect(request.url()).toBe(server.PREFIX + '/api/foo');
120+
expect(await request.bodyJSON()).toStrictEqual({ foo: 'bar' });
121+
});

0 commit comments

Comments
 (0)