diff --git a/docs/src/api/class-request.md b/docs/src/api/class-request.md index 05063f4486..02306d85a7 100644 --- a/docs/src/api/class-request.md +++ b/docs/src/api/class-request.md @@ -183,6 +183,17 @@ following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttr Returns the matching [Response] object, or `null` if the response was not received due to error. +## method: Request.sizes +- returns: <[Object]> + - `requestBodySize` <[int]> Size of the request body (POST data payload) in bytes. Set to 0 if there was no body. + - `requestHeadersSize` <[float]> Total number of bytes from the start of the HTTP request message until (and including) the double CRLF before the body. + - `responseBodySize` <[int]> Size of the received response body in bytes. + - `responseHeadersSize` <[float]> Total number of bytes from the start of the HTTP response message until (and including) the double CRLF before the body. + - `responseTransferSize` <[float]> Total number of bytes received for the request. + +Returns resource size information for given request. Requires the response to be finished via [`method: Response.finished`] +to ensure the info is available. + ## method: Request.timing - returns: <[Object]> - `startTime` <[float]> Request start time in milliseconds elapsed since January 1, 1970 00:00:00 UTC diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 000f404f02..d19795993e 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -86,7 +86,9 @@ export class BrowserContext extends ChannelOwner this._onRequest(network.Request.from(request), Page.fromNullable(page))); this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page))); - this._channel.on('requestFinished', ({ request, responseEndTiming, page }) => this._onRequestFinished(network.Request.from(request), responseEndTiming, Page.fromNullable(page))); + this._channel.on('requestFinished', ({ request, responseEndTiming, page, requestSizes }) => + this._onRequestFinished(network.Request.from(request), responseEndTiming, requestSizes, Page.fromNullable(page)) + ); this._channel.on('response', ({ response, page }) => this._onResponse(network.Response.from(response), Page.fromNullable(page))); this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f)); } @@ -124,9 +126,10 @@ export class BrowserContext extends ChannelOwner implements api.Response { private _headers: Headers; private _request: Request; diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index 861d1bbb98..91aac46f03 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -86,7 +86,8 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request), responseEndTiming: request._responseEndTiming, - page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined()) + requestSizes: request.sizes(), + page: PageDispatcher.fromNullable(this._scope, request.frame()._page.initializedOrUndefined()), })); } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index e3fbc81622..6a8feb2982 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -766,6 +766,7 @@ export type BrowserContextRequestFailedEvent = { export type BrowserContextRequestFinishedEvent = { request: RequestChannel, responseEndTiming: number, + requestSizes: RequestSizes, page?: PageChannel, }; export type BrowserContextResponseEvent = { @@ -2638,6 +2639,14 @@ export type SecurityDetails = { validTo?: number, }; +export type RequestSizes = { + requestBodySize: number, + requestHeadersSize: number, + responseBodySize: number, + responseHeadersSize: number, + responseTransferSize: number, +}; + export type RemoteAddr = { ipAddress: string, port: number, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index f243c00cae..6f531980d5 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -756,6 +756,7 @@ BrowserContext: parameters: request: Request responseEndTiming: number + requestSizes: RequestSizes page: Page? response: @@ -2232,6 +2233,15 @@ SecurityDetails: validFrom: number? validTo: number? +RequestSizes: + type: object + properties: + requestBodySize: number + requestHeadersSize: number + responseBodySize: number + responseHeadersSize: number + responseTransferSize: number + RemoteAddr: type: object diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index de2530935b..f604dcca6e 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -1055,6 +1055,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { validFrom: tOptional(tNumber), validTo: tOptional(tNumber), }); + scheme.RequestSizes = tObject({ + requestBodySize: tNumber, + requestHeadersSize: tNumber, + responseBodySize: tNumber, + responseHeadersSize: tNumber, + responseTransferSize: tNumber, + }); scheme.RemoteAddr = tObject({ ipAddress: tString, port: tNumber, diff --git a/src/server/network.ts b/src/server/network.ts index 46068796b1..e01807fbc5 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -215,6 +215,16 @@ export class Request extends SdkObject { headersSize += header.name.length + header.value.length + 4; // 4 = ': ' + '\r\n' return headersSize; } + + sizes() { + return { + requestBodySize: this.bodySize(), + requestHeadersSize: this.headersSize(), + responseBodySize: this._sizes.responseBodySize, + responseHeadersSize: this._existingResponse()!.headersSize(), + responseTransferSize: this._sizes.transferSize, + }; + } } export class Route extends SdkObject { diff --git a/tests/page/page-network-request.spec.ts b/tests/page/page-network-request.spec.ts index 544b680723..18e550b5a7 100644 --- a/tests/page/page-network-request.spec.ts +++ b/tests/page/page-network-request.spec.ts @@ -265,3 +265,63 @@ it('should return navigation bit when navigating to image', async ({page, server await page.goto(server.PREFIX + '/pptr.png'); expect(requests[0].isNavigationRequest()).toBe(true); }); + +it('should set bodySize and headersSize', async ({page, server,browserName, platform}) => { + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForEvent('request'), + page.evaluate(() => fetch('./get', { method: 'POST', body: '12345'}).then(r => r.text())), + ]); + await (await request.response()).finished(); + expect(request.sizes().requestBodySize).toBe(5); + expect(request.sizes().requestHeadersSize).toBeGreaterThanOrEqual(300); +}); + +it('should should set bodySize to 0 if there was no body', async ({page, server,browserName, platform}) => { + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForEvent('request'), + page.evaluate(() => fetch('./get').then(r => r.text())), + ]); + await (await request.response()).finished(); + expect(request.sizes().requestBodySize).toBe(0); + expect(request.sizes().requestHeadersSize).toBeGreaterThanOrEqual(250); +}); + +it('should should set bodySize, headersSize, and transferSize', async ({page, server, browserName, platform}) => { + server.setRoute('/get', (req, res) => { + // In Firefox, |fetch| will be hanging until it receives |Content-Type| header + // from server. + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.end('abc134'); + }); + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForEvent('response'), + page.evaluate(async () => fetch('./get').then(r => r.text())), + server.waitForRequest('/get'), + ]); + await response.finished(); + expect(response.request().sizes().responseBodySize).toBe(6); + expect(response.request().sizes().responseHeadersSize).toBeGreaterThanOrEqual(150); + expect(response.request().sizes().responseTransferSize).toBeGreaterThanOrEqual(160); +}); + +it('should should set bodySize to 0 when there was no response body', async ({page, server, browserName, platform}) => { + server.setRoute('/get', (req, res) => { + // In Firefox, |fetch| will be hanging until it receives |Content-Type| header + // from server. + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.end(''); + }); + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForEvent('response'), + page.evaluate(async () => fetch('./get').then(r => r.text())), + server.waitForRequest('/get'), + ]); + await response.finished(); + expect(response.request().sizes().responseBodySize).toBe(0); + expect(response.request().sizes().responseHeadersSize).toBeGreaterThanOrEqual(150); + expect(response.request().sizes().responseTransferSize).toBeGreaterThanOrEqual(160); +}); diff --git a/types/types.d.ts b/types/types.d.ts index abd8e7d781..1807bce0df 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -11668,6 +11668,37 @@ export interface Request { */ response(): Promise; + /** + * Returns resource size information for given request. Requires the response to be finished via + * [response.finished()](https://playwright.dev/docs/api/class-response#response-finished) to ensure the info is available. + */ + sizes(): { + /** + * Size of the request body (POST data payload) in bytes. Set to 0 if there was no body. + */ + requestBodySize: number; + + /** + * Total number of bytes from the start of the HTTP request message until (and including) the double CRLF before the body. + */ + requestHeadersSize: number; + + /** + * Size of the received response body in bytes. + */ + responseBodySize: number; + + /** + * Total number of bytes from the start of the HTTP response message until (and including) the double CRLF before the body. + */ + responseHeadersSize: number; + + /** + * Total number of bytes received for the request. + */ + responseTransferSize: number; + }; + /** * Returns resource timing information for given request. Most of the timing values become available upon the response, * `responseEnd` becomes available when request finishes. Find more information at