Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions docs/src/api/class-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,32 @@ request is issued to a redirected url.

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

## async method: Request.body
* since: v1.62
- returns: <[null]|[string]>

The request body, if present. Unlike [`method: Request.postData`], this method also works for request bodies that the browser only reports after the request has been sent, for example `Blob` or `File` payloads in Chromium-based browsers.

## async method: Request.bodyBuffer
* since: v1.62
- returns: <[null]|[Buffer]>

The request body in a binary form. Returns null if the request has no body.

## async method: Request.bodyJSON
* since: v1.62
* langs: js, python
- returns: <[null]|[Serializable]>

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.

## async method: Request.bodyJSON
* since: v1.62
* langs: csharp
- returns: <[null]|[JsonElement]>

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.

## method: Request.failure
* since: v1.8
- returns: <[null]|[string]>
Expand Down Expand Up @@ -151,35 +177,33 @@ Request's method (GET, POST, etc.)

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

Request's post body, if any.
The request body, if present.

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

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

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

Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any.

When the response is `application/x-www-form-urlencoded` then a key/value object of the values will be returned.
Otherwise it will be parsed as JSON.
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.

## method: Request.postDataJSON
* since: v1.12
* langs: csharp
* discouraged: Use [`method: Request.bodyJSON`] instead.
- returns: <[null]|[JsonElement]>

Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any.

When the response is `application/x-www-form-urlencoded` then a key/value object of the values will be returned.
Otherwise it will be parsed as JSON.
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.

## method: Request.redirectedFrom
* since: v1.8
Expand Down
1 change: 1 addition & 0 deletions packages/isomorphic/protocolMetainfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
['LocalUtils.addStackToTracingNoReply', { internal: true, }],
['LocalUtils.traceDiscarded', { internal: true, }],
['LocalUtils.globToRegex', { internal: true, }],
['Request.body', { title: 'Get request body', group: 'getter', }],
['Request.response', { internal: true, }],
['Request.rawRequestHeaders', { internal: true, }],
['Route.redirectNavigationRequest', { internal: true, }],
Expand Down
35 changes: 30 additions & 5 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21850,6 +21850,26 @@ export interface Request {
*/
allHeaders(): Promise<{ [key: string]: string; }>;

/**
* The request body, if present. Unlike
* [request.postData()](https://playwright.dev/docs/api/class-request#request-post-data), this method also works for
* request bodies that the browser only reports after the request has been sent, for example `Blob` or `File` payloads
* in Chromium-based browsers.
*/
body(): Promise<null|string>;

/**
* The request body in a binary form. Returns null if the request has no body.
*/
bodyBuffer(): Promise<null|Buffer>;

/**
* 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.
*/
bodyJSON(): Promise<null|Serializable>;

/**
* Returns the [Response](https://playwright.dev/docs/api/class-response) object if the response has already been
* received, `null` otherwise.
Expand Down Expand Up @@ -21957,20 +21977,25 @@ export interface Request {
method(): string;

/**
* Request's post body, if any.
* **NOTE** Use [request.body()](https://playwright.dev/docs/api/class-request#request-body) instead.
*
* The request body, if present.
*/
postData(): null|string;

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

/**
* Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any.
* **NOTE** Use [request.bodyJSON()](https://playwright.dev/docs/api/class-request#request-body-json) instead.
*
* When the response is `application/x-www-form-urlencoded` then a key/value object of the values will be returned.
* Otherwise it will be parsed as JSON.
* 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.
*/
postDataJSON(): null|Serializable;

Expand Down
6 changes: 6 additions & 0 deletions packages/playwright-core/src/client/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3599,9 +3599,15 @@ export interface RequestEventTarget {
}
export interface RequestChannel extends RequestEventTarget, Channel {
_type_Request: boolean;
body(params: RequestBodyParams, options: TimeoutOptions): Promise<RequestBodyResult>;
response(params: RequestResponseParams, options: TimeoutOptions): Promise<RequestResponseResult>;
rawRequestHeaders(params: RequestRawRequestHeadersParams, options: TimeoutOptions): Promise<RequestRawRequestHeadersResult>;
}
export type RequestBodyParams = {};
export type RequestBodyOptions = {};
export type RequestBodyResult = {
body?: Binary,
};
export type RequestResponseParams = {};
export type RequestResponseOptions = {};
export type RequestResponseResult = {
Expand Down
17 changes: 16 additions & 1 deletion packages/playwright-core/src/client/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,18 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
return this._fallbackOverrides.method || this._initializer.method;
}

async body(): Promise<string | null> {
return (await this.bodyBuffer())?.toString('utf-8') || null;
}

async bodyBuffer(): Promise<Buffer | null> {
return this._fallbackOverrides.postDataBuffer || (await this._channel.body({}, kNoTimeout)).body || null;
}

async bodyJSON(): Promise<Object | null> {
return this._postDataJSON(await this.body());
}

postData(): string | null {
return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.toString('utf-8') || null;
}
Expand All @@ -145,7 +157,10 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
}

postDataJSON(): Object | null {
const postData = this.postData();
return this._postDataJSON(this.postData());
}

private _postDataJSON(postData: string | null): Object | null {
if (!postData)
return null;

Expand Down
6 changes: 6 additions & 0 deletions packages/playwright-core/src/server/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3600,9 +3600,15 @@ export interface RequestEventTarget {
}
export interface RequestChannel extends RequestEventTarget, Channel {
_type_Request: boolean;
body(params: RequestBodyParams, progress: Progress): Promise<RequestBodyResult>;
response(params: RequestResponseParams, progress: Progress): Promise<RequestResponseResult>;
rawRequestHeaders(params: RequestRawRequestHeadersParams, progress: Progress): Promise<RequestRawRequestHeadersResult>;
}
export type RequestBodyParams = {};
export type RequestBodyOptions = {};
export type RequestBodyResult = {
body?: Binary,
};
export type RequestResponseParams = {};
export type RequestResponseOptions = {};
export type RequestResponseResult = {
Expand Down
14 changes: 14 additions & 0 deletions packages/playwright-core/src/server/chromium/crNetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,19 @@ export class CRNetworkManager {
}
}

// When the request body is not available synchronously (e.g. a Blob or File body), Network.requestWillBeSent
// only reports hasPostData, and the bytes must be retrieved with Network.getRequestPostData. Fetch the body
// asynchronously and push it to the request once available. See https://github.com/microsoft/playwright/issues/6479.
private _maybeFetchPostData(sessionInfo: SessionInfo, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedEvent: Protocol.Fetch.requestPausedPayload | undefined, request: network.Request) {
const cdpRequest = requestPausedEvent ? requestPausedEvent.request : requestWillBeSentEvent.request;
if (!cdpRequest.hasPostData || request.postDataBuffer())
return;
request.deferPostData();
sessionInfo.session.send('Network.getRequestPostData', { requestId: requestWillBeSentEvent.requestId })
.then(result => request.resolvePostData(Buffer.from(result.postData, result.base64Encoded ? 'base64' : 'utf-8')))
.catch(() => request.resolvePostData(null));
}

_onRequestServedFromCache(event: Protocol.Network.requestServedFromCachePayload) {
this._responseExtraInfoTracker.requestServedFromCache(event);
}
Expand Down Expand Up @@ -372,6 +385,7 @@ export class CRNetworkManager {
headersOverride: headersOverride || null,
});
this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request);
this._maybeFetchPostData(requestWillBeSentSessionInfo, requestWillBeSentEvent, requestPausedEvent, request.request);

if (route) {
// We may not receive extra info when intercepting the request.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
ResponseDispatcher.fromNullable(scope, request._existingResponse());
}

async body(params: channels.RequestBodyParams, progress: Progress): Promise<channels.RequestBodyResult> {
const body = await this._object.body(progress);
return { body: body === null ? undefined : body };
}

async rawRequestHeaders(params: channels.RequestRawRequestHeadersParams, progress: Progress): Promise<channels.RequestRawRequestHeadersResult> {
return { headers: await this._object.rawRequestHeaders(progress) };
}
Expand Down
17 changes: 17 additions & 0 deletions packages/playwright-core/src/server/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export class Request extends SdkObject {
readonly _serviceWorker: pages.Worker | null = null;
readonly _context: contexts.BrowserContext;
private _rawRequestHeadersPromise = new ManualPromise<HeadersArray>();
private _deferredPostDataPromise: ManualPromise<Buffer | null> | undefined;
private _waitForResponsePromise = new ManualPromise<Response | null>();
_responseEndTiming = -1;
private _overrides: NormalizedContinueOverrides | undefined;
Expand Down Expand Up @@ -267,6 +268,22 @@ export class Request extends SdkObject {
return this._overrides?.postData || this._postData;
}

// Marks that the post data is not available at request start and will be delivered
// asynchronously via resolvePostData().
deferPostData() {
this._deferredPostDataPromise = new ManualPromise();
}

resolvePostData(postData: Buffer | null) {
if (postData && !this._postData)
this._postData = postData;
this._deferredPostDataPromise?.resolve(this.postDataBuffer());
}

async body(progress: Progress): Promise<Buffer | null> {
return await this.raceWithPageClosure(progress, this._deferredPostDataPromise ?? Promise.resolve(this.postDataBuffer()));
}

headers(): HeadersArray {
return this._overrides?.headers || this._headers;
}
Expand Down
21 changes: 21 additions & 0 deletions packages/playwright-core/src/server/webkit/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5535,6 +5535,10 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
* HTTP POST request data.
*/
postData?: string;
/**
* True when the request has a body which is not included in postData, e.g. because it references blobs. The body can be retrieved with getRequestPostData.
*/
hasPostData?: boolean;
/**
* The level of included referrer information.
*/
Expand Down Expand Up @@ -6070,6 +6074,21 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
}
export type setExtraHTTPHeadersReturnValue = {
}
/**
* Returns post data sent with the request. Returns an error when no data was sent with the request.
*/
export type getRequestPostDataParameters = {
/**
* Identifier of the network request to get content for.
*/
requestId: RequestId;
}
export type getRequestPostDataReturnValue = {
/**
* Base64-encoded request body.
*/
postData: string;
}
/**
* Returns content served for the given request.
*/
Expand Down Expand Up @@ -9588,6 +9607,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"Network.enable": Network.enableParameters;
"Network.disable": Network.disableParameters;
"Network.setExtraHTTPHeaders": Network.setExtraHTTPHeadersParameters;
"Network.getRequestPostData": Network.getRequestPostDataParameters;
"Network.getResponseBody": Network.getResponseBodyParameters;
"Network.setResourceCachingDisabled": Network.setResourceCachingDisabledParameters;
"Network.setClearResourceDataOnNavigate": Network.setClearResourceDataOnNavigateParameters;
Expand Down Expand Up @@ -9898,6 +9918,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"Network.enable": Network.enableReturnValue;
"Network.disable": Network.disableReturnValue;
"Network.setExtraHTTPHeaders": Network.setExtraHTTPHeadersReturnValue;
"Network.getRequestPostData": Network.getRequestPostDataReturnValue;
"Network.getResponseBody": Network.getResponseBodyReturnValue;
"Network.setResourceCachingDisabled": Network.setResourceCachingDisabledReturnValue;
"Network.setClearResourceDataOnNavigate": Network.setClearResourceDataOnNavigateReturnValue;
Expand Down
13 changes: 13 additions & 0 deletions packages/playwright-core/src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,18 @@ export class WKPage implements PageDelegate {
this._onRequest(session, event, false);
}

// When the request body references blobs, Network.requestWillBeSent only reports hasPostData,
// and the bytes must be retrieved asynchronously with Network.getRequestPostData. Fetch the body
// asynchronously and push it to the request once available. See https://github.com/microsoft/playwright/issues/6479.
private _maybeFetchPostData(session: WKSession, event: Protocol.Network.requestWillBeSentPayload, request: network.Request) {
if (!event.request.hasPostData || request.postDataBuffer())
return;
request.deferPostData();
session.send('Network.getRequestPostData', { requestId: event.requestId })
.then(result => request.resolvePostData(Buffer.from(result.postData, 'base64')))
.catch(() => request.resolvePostData(null));
}

private _onRequest(session: WKSession, event: Protocol.Network.requestWillBeSentPayload, intercepted: boolean) {
let redirectedFrom: WKInterceptableRequest | null = null;
if (event.redirectResponse) {
Expand Down Expand Up @@ -1098,6 +1110,7 @@ export class WKPage implements PageDelegate {
request.request.setRawRequestHeaders(null);
}
this._requestIdToRequest.set(event.requestId, request);
this._maybeFetchPostData(session, event, request.request);
this._page.frameManager.requestStarted(request.request, route);
}

Expand Down
Loading
Loading