From d31ae50d8c8e89a1c1c6c1d5dc165f0127865fae Mon Sep 17 00:00:00 2001 From: smckee-r7 Date: Wed, 18 Feb 2026 11:55:05 +0000 Subject: [PATCH 1/4] feat(api): add Response.httpVersion() method --- docs/src/api/class-response.md | 6 ++++++ packages/playwright-client/types/types.d.ts | 5 +++++ .../playwright-core/src/client/network.ts | 6 ++++++ .../playwright-core/src/protocol/validator.ts | 4 ++++ .../server/dispatchers/networkDispatchers.ts | 4 ++++ .../src/server/har/harTracer.ts | 6 +++--- .../playwright-core/src/server/network.ts | 20 +++++++++++-------- .../src/utils/isomorphic/protocolMetainfo.ts | 1 + packages/playwright-core/types/types.d.ts | 5 +++++ packages/protocol/src/channels.d.ts | 6 ++++++ packages/protocol/src/protocol.yml | 5 +++++ 11 files changed, 57 insertions(+), 11 deletions(-) diff --git a/docs/src/api/class-response.md b/docs/src/api/class-response.md index cddaf696592ce..8fcef0883d764 100644 --- a/docs/src/api/class-response.md +++ b/docs/src/api/class-response.md @@ -80,6 +80,12 @@ Returns all values of the headers matching the name, for example `set-cookie`. T Name of the header. +## async method: Response.httpVersion +* since: v1.59 +- returns: <[string]> + +Returns the http version used by the response. + ## async method: Response.json * since: v1.8 * langs: js, python diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 604208ad14b25..fe0d2ff377920 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -21155,6 +21155,11 @@ export interface Response { */ headerValues(name: string): Promise>; + /** + * Returns the http version used by the response. + */ + httpVersion(): Promise; + /** * Returns the JSON representation of response body. * diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index e43e3d4ed75be..00728875609a4 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -741,6 +741,12 @@ export class Response extends ChannelOwner implements async securityDetails(): Promise { return (await this._channel.securityDetails()).value || null; } + + async httpVersion(): Promise { + return this._wrapApiCall(async () => { + return (await this._channel.httpVersion()).value; + }, { internal: true }); + } } export class WebSocket extends ChannelOwner implements api.WebSocket { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 06622ef91f8a3..8a59f8ab5932c 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2416,6 +2416,10 @@ scheme.ResponseRawResponseHeadersParams = tOptional(tObject({})); scheme.ResponseRawResponseHeadersResult = tObject({ headers: tArray(tType('NameValue')), }); +scheme.ResponseHttpVersionParams = tOptional(tObject({})); +scheme.ResponseHttpVersionResult = tObject({ + value: tString, +}); scheme.ResponseSizesParams = tOptional(tObject({})); scheme.ResponseSizesResult = tObject({ sizes: tType('RequestSizes'), diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 41b8618109695..df73f81f78af2 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -117,6 +117,10 @@ export class ResponseDispatcher extends Dispatcher { + return { value: await progress.race(this._object.httpVersion()) }; + } + async sizes(params: channels.ResponseSizesParams, progress: Progress): Promise { return { sizes: await progress.race(this._object.sizes()) }; } diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 601f1821e554d..63574f3ecf87a 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -323,7 +323,7 @@ export class HarTracer { })); } - const httpVersion = response.httpVersion(); + const httpVersion = await response.httpVersion(); harEntry.request.httpVersion = httpVersion; harEntry.response.httpVersion = httpVersion; @@ -441,7 +441,7 @@ export class HarTracer { } } - private _onResponse(response: network.Response) { + private async _onResponse(response: network.Response) { const harEntry = this._entryForRequest(response.request()); if (!harEntry) return; @@ -452,7 +452,7 @@ export class HarTracer { harEntry.response = { status: response.status(), statusText: response.statusText(), - httpVersion: response.httpVersion(), + httpVersion: await response.httpVersion(), // These are bad values that will be overwritten below. cookies: [], headers: [], diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index 5a45644d3f3cd..bbd6f194e0a84 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -508,7 +508,7 @@ export class Response extends SdkObject { private _serverAddrPromise = new ManualPromise(); private _securityDetailsPromise = new ManualPromise(); private _rawResponseHeadersPromise = new ManualPromise(); - private _httpVersion: string | undefined; + private _httpVersionPromise = new ManualPromise(); private _fromServiceWorker: boolean; private _encodedBodySizePromise = new ManualPromise(); private _transferSizePromise = new ManualPromise(); @@ -526,7 +526,8 @@ export class Response extends SdkObject { this._headersMap.set(name.toLowerCase(), value); this._getResponseBodyCallback = getResponseBodyCallback; this._request._setResponse(this); - this._httpVersion = httpVersion; + if (httpVersion) + this._httpVersionPromise.resolve(httpVersion); this._fromServiceWorker = fromServiceWorker; } @@ -547,7 +548,7 @@ export class Response extends SdkObject { } _setHttpVersion(httpVersion: string) { - this._httpVersion = httpVersion; + this._httpVersionPromise.resolve(httpVersion); } url(): string { @@ -631,14 +632,17 @@ export class Response extends SdkObject { return this._request.frame(); } - httpVersion(): string { - if (!this._httpVersion) + async httpVersion(): Promise { + const httpVersion = await this._httpVersionPromise || null; + if (!httpVersion) return 'HTTP/1.1'; - if (this._httpVersion === 'http/1.1') + if (httpVersion === 'http/1.1') return 'HTTP/1.1'; - if (this._httpVersion === 'h2') + if (httpVersion === 'h2') return 'HTTP/2.0'; - return this._httpVersion; + if (httpVersion === 'h3') + return 'HTTP/3.0'; + return httpVersion; } fromServiceWorker(): boolean { diff --git a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts index f21f37956abc6..63a7027df02f9 100644 --- a/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts +++ b/packages/playwright-core/src/utils/isomorphic/protocolMetainfo.ts @@ -263,6 +263,7 @@ export const methodMetainfo = new Map>; + /** + * Returns the http version used by the response. + */ + httpVersion(): Promise; + /** * Returns the JSON representation of response body. * diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index a7c8251861ada..77f9ca5648551 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -4136,6 +4136,7 @@ export interface ResponseChannel extends ResponseEventTarget, Channel { securityDetails(params?: ResponseSecurityDetailsParams, progress?: Progress): Promise; serverAddr(params?: ResponseServerAddrParams, progress?: Progress): Promise; rawResponseHeaders(params?: ResponseRawResponseHeadersParams, progress?: Progress): Promise; + httpVersion(params?: ResponseHttpVersionParams, progress?: Progress): Promise; sizes(params?: ResponseSizesParams, progress?: Progress): Promise; } export type ResponseBodyParams = {}; @@ -4158,6 +4159,11 @@ export type ResponseRawResponseHeadersOptions = {}; export type ResponseRawResponseHeadersResult = { headers: NameValue[], }; +export type ResponseHttpVersionParams = {}; +export type ResponseHttpVersionOptions = {}; +export type ResponseHttpVersionResult = { + value: string, +}; export type ResponseSizesParams = {}; export type ResponseSizesOptions = {}; export type ResponseSizesResult = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index defad2e499f64..347a5a4d0291a 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3656,6 +3656,11 @@ Response: type: array items: NameValue + httpVersion: + internal: true + returns: + value: string + sizes: internal: true returns: From c4fd24503b5d569865f64d0eda53759ef760524f Mon Sep 17 00:00:00 2001 From: smckee-r7 Date: Mon, 23 Feb 2026 10:28:43 +0000 Subject: [PATCH 2/4] serialize httpVersion(), remove await pattern in event handlers --- .../playwright-core/src/server/har/harTracer.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 63574f3ecf87a..56e00ab2a2ac0 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -323,9 +323,10 @@ export class HarTracer { })); } - const httpVersion = await response.httpVersion(); - harEntry.request.httpVersion = httpVersion; - harEntry.response.httpVersion = httpVersion; + this._addBarrier(page || request.serviceWorker(), response.httpVersion().then(httpVersion => { + harEntry.request.httpVersion = httpVersion; + harEntry.response.httpVersion = httpVersion; + })); const compressionCalculationBarrier = this._options.omitSizes ? undefined : { _encodedBodySize: -1, @@ -452,7 +453,7 @@ export class HarTracer { harEntry.response = { status: response.status(), statusText: response.statusText(), - httpVersion: await response.httpVersion(), + httpVersion: FALLBACK_HTTP_VERSION, // These are bad values that will be overwritten below. cookies: [], headers: [], @@ -466,6 +467,10 @@ export class HarTracer { _transferSize: this._options.omitSizes ? undefined : -1 }; + this._addBarrier(page || request.serviceWorker(), response.httpVersion().then(httpVersion => { + harEntry.response.httpVersion = httpVersion; + })); + if (!this._options.omitTiming) { const startDateTime = pageEntry ? ((pageEntry as any)[startedDateSymbol] as Date).valueOf() : 0; const timing = response.timing(); From 0b1894dc832b0de40e84ffc750f36b609fa62d6b Mon Sep 17 00:00:00 2001 From: smckee-r7 Date: Fri, 27 Feb 2026 10:32:51 +0000 Subject: [PATCH 3/4] address comments --- packages/playwright-core/src/client/network.ts | 4 +--- .../src/server/chromium/crNetworkManager.ts | 3 ++- .../src/server/firefox/ffNetworkManager.ts | 3 +-- packages/playwright-core/src/server/har/harTracer.ts | 2 +- packages/playwright-core/src/server/network.ts | 10 +++------- packages/playwright-core/src/server/webkit/wkPage.ts | 3 +-- tests/page/page-network-response.spec.ts | 9 +++++++++ 7 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 00728875609a4..843c1fa6a7055 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -743,9 +743,7 @@ export class Response extends ChannelOwner implements } async httpVersion(): Promise { - return this._wrapApiCall(async () => { - return (await this._channel.httpVersion()).value; - }, { internal: true }); + return (await this._channel.httpVersion()).value; } } diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index f6d33847091c9..f64571fac1105 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -426,7 +426,8 @@ export class CRNetworkManager { responseStart: -1, }; } - const response = new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody, !!responsePayload.fromServiceWorker, responsePayload.protocol); + const response = new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody, !!responsePayload.fromServiceWorker); + response._setHttpVersion(responsePayload?.protocol ?? null); if (responsePayload?.remoteIPAddress && typeof responsePayload?.remotePort === 'number') { response._serverAddrFinished({ ipAddress: responsePayload.remoteIPAddress, diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index bebe6670dd223..b7540245dec35 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -144,8 +144,7 @@ export class FFNetworkManager { this._requests.delete(request._id); response._requestFinished(responseEndTime); } - if (event.protocolVersion) - response._setHttpVersion(event.protocolVersion); + response._setHttpVersion(event.protocolVersion ?? null); this._page.frameManager.reportRequestFinished(request.request, response); } diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 56e00ab2a2ac0..ed68a5fd372a5 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -442,7 +442,7 @@ export class HarTracer { } } - private async _onResponse(response: network.Response) { + private _onResponse(response: network.Response) { const harEntry = this._entryForRequest(response.request()); if (!harEntry) return; diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index bbd6f194e0a84..b39d668d14a15 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -508,13 +508,13 @@ export class Response extends SdkObject { private _serverAddrPromise = new ManualPromise(); private _securityDetailsPromise = new ManualPromise(); private _rawResponseHeadersPromise = new ManualPromise(); - private _httpVersionPromise = new ManualPromise(); + private _httpVersionPromise = new ManualPromise(); private _fromServiceWorker: boolean; private _encodedBodySizePromise = new ManualPromise(); private _transferSizePromise = new ManualPromise(); private _responseHeadersSizePromise = new ManualPromise(); - constructor(request: Request, status: number, statusText: string, headers: HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback, fromServiceWorker: boolean, httpVersion?: string) { + constructor(request: Request, status: number, statusText: string, headers: HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback, fromServiceWorker: boolean) { super(request.frame() || request._context, 'response'); this._request = request; this._timing = timing; @@ -526,8 +526,6 @@ export class Response extends SdkObject { this._headersMap.set(name.toLowerCase(), value); this._getResponseBodyCallback = getResponseBodyCallback; this._request._setResponse(this); - if (httpVersion) - this._httpVersionPromise.resolve(httpVersion); this._fromServiceWorker = fromServiceWorker; } @@ -547,7 +545,7 @@ export class Response extends SdkObject { this._finishedPromise.resolve(); } - _setHttpVersion(httpVersion: string) { + _setHttpVersion(httpVersion: string | null) { this._httpVersionPromise.resolve(httpVersion); } @@ -640,8 +638,6 @@ export class Response extends SdkObject { return 'HTTP/1.1'; if (httpVersion === 'h2') return 'HTTP/2.0'; - if (httpVersion === 'h3') - return 'HTTP/3.0'; return httpVersion; } diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 2e67685dc5b2d..82aa97e97fa22 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -1161,8 +1161,7 @@ export class WKPage implements PageDelegate { validFrom: responseReceivedPayload?.response.security?.certificate?.validFrom, validTo: responseReceivedPayload?.response.security?.certificate?.validUntil, }); - if (event.metrics?.protocol) - response._setHttpVersion(event.metrics.protocol); + response._setHttpVersion(event.metrics?.protocol ?? null); response.setEncodedBodySize(event.metrics?.responseBodyBytesReceived ?? null); response.setResponseHeadersSize(event.metrics?.responseHeaderBytesReceived ?? null); diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index daeeed6281945..434ee9859f436 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -17,7 +17,11 @@ import fs from 'fs'; import url from 'url'; +import type { AddressInfo } from 'net'; import { expect, test as it } from './pageTest'; +import { TestServer } from '../config/testserver'; + +const { createHttp2Server } = require('../../packages/playwright-core/lib/utils'); it('should work @smoke', async ({ page, server }) => { server.setRoute('/empty.html', (req, res) => { @@ -421,3 +425,8 @@ it('request.existingResponse should return the response after it is received', a const request = response.request(); expect(request.existingResponse()).toBe(response); }); + +it('should return http version', async ({ page, server }) => { + const response = await page.goto(server.EMPTY_PAGE); + expect(await response.httpVersion()).toBe('HTTP/1.1'); +}); From 6c371463466f95874a5f83ac1dab64da16ad083a Mon Sep 17 00:00:00 2001 From: smckee-r7 Date: Tue, 3 Mar 2026 12:29:08 +0000 Subject: [PATCH 4/4] PR comments --- .../playwright-core/src/server/firefox/ffNetworkManager.ts | 1 + packages/playwright-core/src/server/har/harTracer.ts | 6 +----- packages/playwright-core/src/server/webkit/wkPage.ts | 1 + tests/page/page-network-response.spec.ts | 4 ---- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index b7540245dec35..de33acb6a2a80 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -158,6 +158,7 @@ export class FFNetworkManager { response.setTransferSize(null); response.setEncodedBodySize(null); response._requestFinished(-1); + response._setHttpVersion(null); } request.request._setFailureText(event.errorCode); this._page.frameManager.requestFailed(request.request, event.errorCode === 'NS_BINDING_ABORTED'); diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index ed68a5fd372a5..dc6399abf7fe3 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -323,11 +323,6 @@ export class HarTracer { })); } - this._addBarrier(page || request.serviceWorker(), response.httpVersion().then(httpVersion => { - harEntry.request.httpVersion = httpVersion; - harEntry.response.httpVersion = httpVersion; - })); - const compressionCalculationBarrier = this._options.omitSizes ? undefined : { _encodedBodySize: -1, _decodedBodySize: -1, @@ -468,6 +463,7 @@ export class HarTracer { }; this._addBarrier(page || request.serviceWorker(), response.httpVersion().then(httpVersion => { + harEntry.request.httpVersion = httpVersion; harEntry.response.httpVersion = httpVersion; })); diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 82aa97e97fa22..4751ae522babe 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -1195,6 +1195,7 @@ export class WKPage implements PageDelegate { if (response) { response._serverAddrFinished(); response._securityDetailsFinished(); + response._setHttpVersion(null); response.setResponseHeadersSize(null); response.setEncodedBodySize(null); response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp)); diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index 434ee9859f436..849d7c22cbe2f 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -17,11 +17,7 @@ import fs from 'fs'; import url from 'url'; -import type { AddressInfo } from 'net'; import { expect, test as it } from './pageTest'; -import { TestServer } from '../config/testserver'; - -const { createHttp2Server } = require('../../packages/playwright-core/lib/utils'); it('should work @smoke', async ({ page, server }) => { server.setRoute('/empty.html', (req, res) => {