diff --git a/docs/api/puppeteer.httprequest.fetchpostdata.md b/docs/api/puppeteer.httprequest.fetchpostdata.md new file mode 100644 index 00000000000..7e88d32328b --- /dev/null +++ b/docs/api/puppeteer.httprequest.fetchpostdata.md @@ -0,0 +1,19 @@ +--- +sidebar_label: HTTPRequest.fetchPostData +--- + +# HTTPRequest.fetchPostData() method + +Fetches the POST data for the request from the browser. + +#### Signature: + +```typescript +class HTTPRequest { + abstract fetchPostData(): Promise; +} +``` + +**Returns:** + +Promise<string \| undefined> diff --git a/docs/api/puppeteer.httprequest.haspostdata.md b/docs/api/puppeteer.httprequest.haspostdata.md new file mode 100644 index 00000000000..e244ccdff02 --- /dev/null +++ b/docs/api/puppeteer.httprequest.haspostdata.md @@ -0,0 +1,19 @@ +--- +sidebar_label: HTTPRequest.hasPostData +--- + +# HTTPRequest.hasPostData() method + +True when the request has POST data. Note that [HTTPRequest.postData()](./puppeteer.httprequest.postdata.md) might still be undefined when this flag is true when the data is too long or not readily available in the decoded form. In that case, use [HTTPRequest.fetchPostData()](./puppeteer.httprequest.fetchpostdata.md). + +#### Signature: + +```typescript +class HTTPRequest { + abstract hasPostData(): boolean; +} +``` + +**Returns:** + +boolean diff --git a/docs/api/puppeteer.httprequest.md b/docs/api/puppeteer.httprequest.md index 6e9192b9c30..ea8d41135de 100644 --- a/docs/api/puppeteer.httprequest.md +++ b/docs/api/puppeteer.httprequest.md @@ -48,8 +48,10 @@ The constructor for this class is marked as internal. Third-party code should no | [continueRequestOverrides()](./puppeteer.httprequest.continuerequestoverrides.md) | | The ContinueRequestOverrides that will be used if the interception is allowed to continue (ie, abort() and respond() aren't called). | | [enqueueInterceptAction(pendingHandler)](./puppeteer.httprequest.enqueueinterceptaction.md) | | Adds an async request handler to the processing queue. Deferred handlers are not guaranteed to execute in any particular order, but they are guaranteed to resolve before the request interception is finalized. | | [failure()](./puppeteer.httprequest.failure.md) | | Access information about the request's failure. | +| [fetchPostData()](./puppeteer.httprequest.fetchpostdata.md) | | Fetches the POST data for the request from the browser. | | [finalizeInterceptions()](./puppeteer.httprequest.finalizeinterceptions.md) | | Awaits pending interception handlers and then decides how to fulfill the request interception. | | [frame()](./puppeteer.httprequest.frame.md) | | The frame that initiated the request, or null if navigating to error pages. | +| [hasPostData()](./puppeteer.httprequest.haspostdata.md) | | True when the request has POST data. Note that [HTTPRequest.postData()](./puppeteer.httprequest.postdata.md) might still be undefined when this flag is true when the data is too long or not readily available in the decoded form. In that case, use [HTTPRequest.fetchPostData()](./puppeteer.httprequest.fetchpostdata.md). | | [headers()](./puppeteer.httprequest.headers.md) | | An object with HTTP headers associated with the request. All header names are lower-case. | | [initiator()](./puppeteer.httprequest.initiator.md) | | The initiator of the request. | | [interceptResolutionState()](./puppeteer.httprequest.interceptresolutionstate.md) | |

An InterceptResolutionState object describing the current resolution action and priority.

InterceptResolutionState contains: action: InterceptResolutionAction priority?: number

InterceptResolutionAction is one of: abort, respond, continue, disabled, none, or already-handled.

| diff --git a/packages/puppeteer-core/src/api/HTTPRequest.ts b/packages/puppeteer-core/src/api/HTTPRequest.ts index fc6a3591489..cf0d043e6ec 100644 --- a/packages/puppeteer-core/src/api/HTTPRequest.ts +++ b/packages/puppeteer-core/src/api/HTTPRequest.ts @@ -212,6 +212,19 @@ export abstract class HTTPRequest { */ abstract postData(): string | undefined; + /** + * True when the request has POST data. Note that {@link HTTPRequest.postData} + * might still be undefined when this flag is true when the data is too long + * or not readily available in the decoded form. In that case, use + * {@link HTTPRequest.fetchPostData}. + */ + abstract hasPostData(): boolean; + + /** + * Fetches the POST data for the request from the browser. + */ + abstract fetchPostData(): Promise; + /** * An object with HTTP headers associated with the request. All * header names are lower-case. diff --git a/packages/puppeteer-core/src/bidi/HTTPRequest.ts b/packages/puppeteer-core/src/bidi/HTTPRequest.ts index 78d27ad0e25..92856905d0d 100644 --- a/packages/puppeteer-core/src/bidi/HTTPRequest.ts +++ b/packages/puppeteer-core/src/bidi/HTTPRequest.ts @@ -89,6 +89,14 @@ export class BidiHTTPRequest extends HTTPRequest { return this.#postData; } + override hasPostData(): boolean { + return this.#postData !== undefined; + } + + override async fetchPostData(): Promise { + return this.#postData; + } + override headers(): Record { return this.#headers; } diff --git a/packages/puppeteer-core/src/cdp/HTTPRequest.ts b/packages/puppeteer-core/src/cdp/HTTPRequest.ts index 6b4cea40ce6..92d39e0fa42 100644 --- a/packages/puppeteer-core/src/cdp/HTTPRequest.ts +++ b/packages/puppeteer-core/src/cdp/HTTPRequest.ts @@ -49,6 +49,7 @@ export class CdpHTTPRequest extends HTTPRequest { #resourceType: ResourceType; #method: string; + #hasPostData = false; #postData?: string; #headers: Record = {}; #frame: Frame | null; @@ -109,6 +110,7 @@ export class CdpHTTPRequest extends HTTPRequest { this.#resourceType = (data.type || 'other').toLowerCase() as ResourceType; this.#method = data.request.method; this.#postData = data.request.postData; + this.#hasPostData = data.request.hasPostData ?? false; this.#frame = frame; this._redirectChain = redirectChain; this.#continueRequestOverrides = {}; @@ -189,6 +191,22 @@ export class CdpHTTPRequest extends HTTPRequest { return this.#postData; } + override hasPostData(): boolean { + return this.#hasPostData; + } + + override async fetchPostData(): Promise { + try { + const result = await this.#client.send('Network.getRequestPostData', { + requestId: this._requestId, + }); + return result.postData; + } catch (err) { + debugError(err); + return; + } + } + override headers(): Record { return this.#headers; } diff --git a/test/TestExpectations.json b/test/TestExpectations.json index 0c150a4cb71..3faf41c3a13 100644 --- a/test/TestExpectations.json +++ b/test/TestExpectations.json @@ -851,6 +851,13 @@ "parameters": ["webDriverBiDi"], "expectations": ["FAIL"] }, + { + "testIdPattern": "[network.spec] network Request.postData should work with blobs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "Not implemented for BiDi yet." + }, { "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker", "platforms": ["darwin", "linux", "win32"], @@ -2711,6 +2718,13 @@ "parameters": ["cdp", "firefox"], "expectations": ["FAIL"] }, + { + "testIdPattern": "[network.spec] network Request.postData should work with blobs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"], + "comment": "Blobs have no POST data in Firefox's CDP implementation." + }, { "testIdPattern": "[network.spec] network Response.buffer should throw if the response does not have a body", "platforms": ["darwin", "linux", "win32"], diff --git a/test/src/network.spec.ts b/test/src/network.spec.ts index a441fb7b6a5..dbc1591c899 100644 --- a/test/src/network.spec.ts +++ b/test/src/network.spec.ts @@ -267,12 +267,41 @@ describe('network', function () { expect(request).toBeTruthy(); expect(request.postData()).toBe('{"foo":"bar"}'); }); + it('should be |undefined| when there is no post data', async () => { const {page, server} = await getTestState(); const response = (await page.goto(server.EMPTY_PAGE))!; expect(response.request().postData()).toBe(undefined); }); + + it('should work with blobs', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (_req, res) => { + return res.end(); + }); + + const [request] = await Promise.all([ + waitEvent(page, 'request', r => { + return !isFavicon(r); + }), + page.evaluate(() => { + return fetch('./post', { + method: 'POST', + body: new Blob([JSON.stringify({foo: 'bar'})], { + type: 'application/json', + }), + }); + }), + ]); + + expect(request).toBeTruthy(); + expect(request.postData()).toBe(undefined); + expect(request.hasPostData()).toBe(true); + expect(await request.fetchPostData()).toBe('{"foo":"bar"}'); + }); }); describe('Response.text', function () {