Skip to content

Commit 2b1c702

Browse files
authored
feat(api): add Request.existingResponse() method (#39077)
1 parent 5c749f5 commit 2b1c702

File tree

10 files changed

+74
-18
lines changed

10 files changed

+74
-18
lines changed

docs/src/api/class-request.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ following: `document`, `stylesheet`, `image`, `media`, `font`, `script`, `texttr
285285

286286
Returns the matching [Response] object, or `null` if the response was not received due to error.
287287

288+
## method: Request.existingResponse
289+
* since: v1.59
290+
- returns: <[null]|[Response]>
291+
292+
Returns the [Response] object if the response has already been received, `null` otherwise.
293+
294+
Unlike [`method: Request.response`], this method does not wait for the response to arrive. It returns
295+
immediately with the response object if the response has been received, or `null` if the response
296+
has not been received yet.
297+
288298
## method: Request.serviceWorker
289299
* since: v1.24
290300
* langs: js, python

packages/playwright-client/types/types.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20769,6 +20769,16 @@ export interface Request {
2076920769
*/
2077020770
allHeaders(): Promise<{ [key: string]: string; }>;
2077120771

20772+
/**
20773+
* Returns the [Response](https://playwright.dev/docs/api/class-response) object if the response has already been
20774+
* received, `null` otherwise.
20775+
*
20776+
* Unlike [request.response()](https://playwright.dev/docs/api/class-request#request-response), this method does not
20777+
* wait for the response to arrive. It returns immediately with the response object if the response has been received,
20778+
* or `null` if the response has not been received yet.
20779+
*/
20780+
existingResponse(): null|Response;
20781+
2077220782
/**
2077320783
* The method returns `null` unless this request has failed, as reported by `requestfailed` event.
2077420784
*

packages/playwright-core/src/client/network.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
8787
private _redirectedFrom: Request | null = null;
8888
private _redirectedTo: Request | null = null;
8989
_failureText: string | null = null;
90+
_response: Response | null = null;
9091
private _provisionalHeaders: RawHeaders;
9192
private _actualHeadersPromise: Promise<RawHeaders> | undefined;
9293
_timing: ResourceTiming;
9394
private _fallbackOverrides: SerializedFallbackOverrides = {};
94-
_hasResponse = false;
9595

9696
static from(request: channels.RequestChannel): Request {
9797
return (request as any)._object;
@@ -118,8 +118,6 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
118118
responseStart: -1,
119119
responseEnd: -1,
120120
};
121-
this._hasResponse = this._initializer.hasResponse;
122-
this._channel.on('response', () => this._hasResponse = true);
123121
}
124122

125123
url(): string {
@@ -204,6 +202,10 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
204202
return Response.fromNullable((await this._channel.response()).response);
205203
}
206204

205+
existingResponse(): Response | null {
206+
return this._response;
207+
}
208+
207209
frame(): Frame {
208210
if (!this._initializer.frame) {
209211
assert(this.serviceWorker());
@@ -649,6 +651,7 @@ export class Response extends ChannelOwner<channels.ResponseChannel> implements
649651
super(parent, type, guid, initializer);
650652
this._provisionalHeaders = new RawHeaders(initializer.headers);
651653
this._request = Request.from(this._initializer.request);
654+
this._request._response = this;
652655
Object.assign(this._request._timing, this._initializer.timing);
653656
}
654657

packages/playwright-core/src/protocol/validator.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2277,9 +2277,7 @@ scheme.RequestInitializer = tObject({
22772277
headers: tArray(tType('NameValue')),
22782278
isNavigationRequest: tBoolean,
22792279
redirectedFrom: tOptional(tChannel(['Request'])),
2280-
hasResponse: tBoolean,
22812280
});
2282-
scheme.RequestResponseEvent = tOptional(tObject({}));
22832281
scheme.RequestResponseParams = tOptional(tObject({}));
22842282
scheme.RequestResponseResult = tObject({
22852283
response: tOptional(tChannel(['Response'])),

packages/playwright-core/src/server/dispatchers/networkDispatchers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
5959
headers: request.headers(),
6060
isNavigationRequest: request.isNavigationRequest(),
6161
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
62-
hasResponse: !!request._existingResponse(),
6362
});
6463
this._type_Request = true;
6564
this._browserContextDispatcher = scope;
66-
this.addObjectListener(Request.Events.Response, () => this._dispatchEvent('response', {}));
65+
// Push existing response to the client if it exists.
66+
ResponseDispatcher.fromNullable(scope, request._existingResponse());
6767
}
6868

6969
async rawRequestHeaders(params: channels.RequestRawRequestHeadersParams, progress: Progress): Promise<channels.RequestRawRequestHeadersResult> {
@@ -79,8 +79,8 @@ export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseCh
7979
_type_Response = true;
8080

8181
static from(scope: BrowserContextDispatcher, response: Response): ResponseDispatcher {
82-
const result = scope.connection.existingDispatcher<ResponseDispatcher>(response);
8382
const requestDispatcher = RequestDispatcher.from(scope, response.request());
83+
const result = scope.connection.existingDispatcher<ResponseDispatcher>(response);
8484
return result || new ResponseDispatcher(requestDispatcher, response);
8585
}
8686

packages/playwright-core/types/types.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20769,6 +20769,16 @@ export interface Request {
2076920769
*/
2077020770
allHeaders(): Promise<{ [key: string]: string; }>;
2077120771

20772+
/**
20773+
* Returns the [Response](https://playwright.dev/docs/api/class-response) object if the response has already been
20774+
* received, `null` otherwise.
20775+
*
20776+
* Unlike [request.response()](https://playwright.dev/docs/api/class-request#request-response), this method does not
20777+
* wait for the response to arrive. It returns immediately with the response object if the response has been received,
20778+
* or `null` if the response has not been received yet.
20779+
*/
20780+
existingResponse(): null|Response;
20781+
2077220782
/**
2077320783
* The method returns `null` unless this request has failed, as reported by `requestfailed` event.
2077420784
*

packages/playwright/src/mcp/browser/tools/network.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { z } from 'playwright-core/lib/mcpBundle';
1818
import { defineTabTool } from './tool';
1919

2020
import type * as playwright from 'playwright-core';
21-
import type { Request } from '../../../../../playwright-core/src/client/network';
2221

2322
const requests = defineTabTool({
2423
capability: 'core',
@@ -62,7 +61,7 @@ const networkClear = defineTabTool({
6261
});
6362

6463
async function renderRequest(request: playwright.Request, includeStatic: boolean): Promise<string | undefined> {
65-
const response = (request as Request)._hasResponse ? await request.response() : undefined;
64+
const response = request.existingResponse();
6665
const isStaticRequest = ['document', 'stylesheet', 'image', 'media', 'font', 'script', 'manifest'].includes(request.resourceType());
6766
const isSuccessfulRequest = !response || response.status() < 400;
6867

packages/protocol/src/channels.d.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3903,17 +3903,14 @@ export type RequestInitializer = {
39033903
headers: NameValue[],
39043904
isNavigationRequest: boolean,
39053905
redirectedFrom?: RequestChannel,
3906-
hasResponse: boolean,
39073906
};
39083907
export interface RequestEventTarget {
3909-
on(event: 'response', callback: (params: RequestResponseEvent) => void): this;
39103908
}
39113909
export interface RequestChannel extends RequestEventTarget, Channel {
39123910
_type_Request: boolean;
39133911
response(params?: RequestResponseParams, progress?: Progress): Promise<RequestResponseResult>;
39143912
rawRequestHeaders(params?: RequestRawRequestHeadersParams, progress?: Progress): Promise<RequestRawRequestHeadersResult>;
39153913
}
3916-
export type RequestResponseEvent = {};
39173914
export type RequestResponseParams = {};
39183915
export type RequestResponseOptions = {};
39193916
export type RequestResponseResult = {
@@ -3926,7 +3923,6 @@ export type RequestRawRequestHeadersResult = {
39263923
};
39273924

39283925
export interface RequestEvents {
3929-
'response': RequestResponseEvent;
39303926
}
39313927

39323928
// ----------- Route -----------

packages/protocol/src/protocol.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3451,7 +3451,6 @@ Request:
34513451
items: NameValue
34523452
isNavigationRequest: boolean
34533453
redirectedFrom: Request?
3454-
hasResponse: boolean
34553454

34563455
commands:
34573456

@@ -3467,9 +3466,6 @@ Request:
34673466
type: array
34683467
items: NameValue
34693468

3470-
events:
3471-
response:
3472-
34733469
Route:
34743470
type: interface
34753471

tests/page/page-network-response.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,3 +387,37 @@ it('should bypass disk cache when context interception is enabled', async ({ pag
387387
}
388388
}
389389
});
390+
391+
it('request.existingResponse should return null before response is received', async ({ page, server }) => {
392+
await page.goto(server.EMPTY_PAGE);
393+
let serverResponse = null;
394+
server.setRoute('/get', (req, res) => {
395+
serverResponse = res;
396+
// Don't end the response yet
397+
});
398+
399+
const [request] = await Promise.all([
400+
page.waitForEvent('request'),
401+
server.waitForRequest('/get'),
402+
page.evaluate(() => { void fetch('./get', { method: 'GET' }); }),
403+
]);
404+
405+
// Response hasn't been received yet
406+
expect(request.existingResponse()).toBe(null);
407+
408+
// Now send the response
409+
serverResponse.setHeader('Content-Type', 'text/plain; charset=utf-8');
410+
serverResponse.end('done');
411+
await page.waitForEvent('response');
412+
413+
// After response is received, existingResponse should return the response
414+
const existingResponse = request.existingResponse();
415+
expect(existingResponse).not.toBe(null);
416+
expect(existingResponse.status()).toBe(200);
417+
});
418+
419+
it('request.existingResponse should return the response after it is received', async ({ page, server }) => {
420+
const response = await page.goto(server.EMPTY_PAGE);
421+
const request = response.request();
422+
expect(request.existingResponse()).toBe(response);
423+
});

0 commit comments

Comments
 (0)