diff --git a/docs/src/api/class-request.md b/docs/src/api/class-request.md index 8552af4670025..3d0ef0f58c459 100644 --- a/docs/src/api/class-request.md +++ b/docs/src/api/class-request.md @@ -285,6 +285,16 @@ 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.existingResponse +* since: v1.59 +- returns: <[null]|[Response]> + +Returns the [Response] object if the response has already been received, `null` otherwise. + +Unlike [`method: Request.response`], this method does not wait for the response to arrive. It returns +immediately with the response object if the response has been received, or `null` if the response +has not been received yet. + ## method: Request.serviceWorker * since: v1.24 * langs: js, python diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index d907c86570656..a91a9ad57624a 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -20769,6 +20769,16 @@ export interface Request { */ allHeaders(): Promise<{ [key: string]: string; }>; + /** + * Returns the [Response](https://playwright.dev/docs/api/class-response) object if the response has already been + * received, `null` otherwise. + * + * Unlike [request.response()](https://playwright.dev/docs/api/class-request#request-response), this method does not + * wait for the response to arrive. It returns immediately with the response object if the response has been received, + * or `null` if the response has not been received yet. + */ + existingResponse(): null|Response; + /** * The method returns `null` unless this request has failed, as reported by `requestfailed` event. * diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 4541decac5679..e43e3d4ed75be 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -87,11 +87,11 @@ export class Request extends ChannelOwner implements ap private _redirectedFrom: Request | null = null; private _redirectedTo: Request | null = null; _failureText: string | null = null; + _response: Response | null = null; private _provisionalHeaders: RawHeaders; private _actualHeadersPromise: Promise | undefined; _timing: ResourceTiming; private _fallbackOverrides: SerializedFallbackOverrides = {}; - _hasResponse = false; static from(request: channels.RequestChannel): Request { return (request as any)._object; @@ -118,8 +118,6 @@ export class Request extends ChannelOwner implements ap responseStart: -1, responseEnd: -1, }; - this._hasResponse = this._initializer.hasResponse; - this._channel.on('response', () => this._hasResponse = true); } url(): string { @@ -204,6 +202,10 @@ export class Request extends ChannelOwner implements ap return Response.fromNullable((await this._channel.response()).response); } + existingResponse(): Response | null { + return this._response; + } + frame(): Frame { if (!this._initializer.frame) { assert(this.serviceWorker()); @@ -649,6 +651,7 @@ export class Response extends ChannelOwner implements super(parent, type, guid, initializer); this._provisionalHeaders = new RawHeaders(initializer.headers); this._request = Request.from(this._initializer.request); + this._request._response = this; Object.assign(this._request._timing, this._initializer.timing); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 2c94b0b45ae3f..f15c8fd198f64 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2277,9 +2277,7 @@ scheme.RequestInitializer = tObject({ headers: tArray(tType('NameValue')), isNavigationRequest: tBoolean, redirectedFrom: tOptional(tChannel(['Request'])), - hasResponse: tBoolean, }); -scheme.RequestResponseEvent = tOptional(tObject({})); scheme.RequestResponseParams = tOptional(tObject({})); scheme.RequestResponseResult = tObject({ response: tOptional(tChannel(['Response'])), diff --git a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts index 286a11ae575f8..41b8618109695 100644 --- a/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/server/dispatchers/networkDispatchers.ts @@ -59,11 +59,11 @@ export class RequestDispatcher extends Dispatcher this._dispatchEvent('response', {})); + // Push existing response to the client if it exists. + ResponseDispatcher.fromNullable(scope, request._existingResponse()); } async rawRequestHeaders(params: channels.RequestRawRequestHeadersParams, progress: Progress): Promise { @@ -79,8 +79,8 @@ export class ResponseDispatcher extends Dispatcher(response); const requestDispatcher = RequestDispatcher.from(scope, response.request()); + const result = scope.connection.existingDispatcher(response); return result || new ResponseDispatcher(requestDispatcher, response); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d907c86570656..a91a9ad57624a 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -20769,6 +20769,16 @@ export interface Request { */ allHeaders(): Promise<{ [key: string]: string; }>; + /** + * Returns the [Response](https://playwright.dev/docs/api/class-response) object if the response has already been + * received, `null` otherwise. + * + * Unlike [request.response()](https://playwright.dev/docs/api/class-request#request-response), this method does not + * wait for the response to arrive. It returns immediately with the response object if the response has been received, + * or `null` if the response has not been received yet. + */ + existingResponse(): null|Response; + /** * The method returns `null` unless this request has failed, as reported by `requestfailed` event. * diff --git a/packages/playwright/src/mcp/browser/tools/network.ts b/packages/playwright/src/mcp/browser/tools/network.ts index 4038732e83944..a22562ea2cc72 100644 --- a/packages/playwright/src/mcp/browser/tools/network.ts +++ b/packages/playwright/src/mcp/browser/tools/network.ts @@ -18,7 +18,6 @@ import { z } from 'playwright-core/lib/mcpBundle'; import { defineTabTool } from './tool'; import type * as playwright from 'playwright-core'; -import type { Request } from '../../../../../playwright-core/src/client/network'; const requests = defineTabTool({ capability: 'core', @@ -62,7 +61,7 @@ const networkClear = defineTabTool({ }); async function renderRequest(request: playwright.Request, includeStatic: boolean): Promise { - const response = (request as Request)._hasResponse ? await request.response() : undefined; + const response = request.existingResponse(); const isStaticRequest = ['document', 'stylesheet', 'image', 'media', 'font', 'script', 'manifest'].includes(request.resourceType()); const isSuccessfulRequest = !response || response.status() < 400; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index b65e5cbd8fba4..babc372c46404 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3903,17 +3903,14 @@ export type RequestInitializer = { headers: NameValue[], isNavigationRequest: boolean, redirectedFrom?: RequestChannel, - hasResponse: boolean, }; export interface RequestEventTarget { - on(event: 'response', callback: (params: RequestResponseEvent) => void): this; } export interface RequestChannel extends RequestEventTarget, Channel { _type_Request: boolean; response(params?: RequestResponseParams, progress?: Progress): Promise; rawRequestHeaders(params?: RequestRawRequestHeadersParams, progress?: Progress): Promise; } -export type RequestResponseEvent = {}; export type RequestResponseParams = {}; export type RequestResponseOptions = {}; export type RequestResponseResult = { @@ -3926,7 +3923,6 @@ export type RequestRawRequestHeadersResult = { }; export interface RequestEvents { - 'response': RequestResponseEvent; } // ----------- Route ----------- diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 13f13ca3b4bce..69eea4c63ba33 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3451,7 +3451,6 @@ Request: items: NameValue isNavigationRequest: boolean redirectedFrom: Request? - hasResponse: boolean commands: @@ -3467,9 +3466,6 @@ Request: type: array items: NameValue - events: - response: - Route: type: interface diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index fd2ebbfb0b16c..daeeed6281945 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -387,3 +387,37 @@ it('should bypass disk cache when context interception is enabled', async ({ pag } } }); + +it('request.existingResponse should return null before response is received', async ({ page, server }) => { + await page.goto(server.EMPTY_PAGE); + let serverResponse = null; + server.setRoute('/get', (req, res) => { + serverResponse = res; + // Don't end the response yet + }); + + const [request] = await Promise.all([ + page.waitForEvent('request'), + server.waitForRequest('/get'), + page.evaluate(() => { void fetch('./get', { method: 'GET' }); }), + ]); + + // Response hasn't been received yet + expect(request.existingResponse()).toBe(null); + + // Now send the response + serverResponse.setHeader('Content-Type', 'text/plain; charset=utf-8'); + serverResponse.end('done'); + await page.waitForEvent('response'); + + // After response is received, existingResponse should return the response + const existingResponse = request.existingResponse(); + expect(existingResponse).not.toBe(null); + expect(existingResponse.status()).toBe(200); +}); + +it('request.existingResponse should return the response after it is received', async ({ page, server }) => { + const response = await page.goto(server.EMPTY_PAGE); + const request = response.request(); + expect(request.existingResponse()).toBe(response); +});