Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/src/api/class-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
9 changes: 6 additions & 3 deletions packages/playwright-core/src/client/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ export class Request extends ChannelOwner<channels.RequestChannel> 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<RawHeaders> | undefined;
_timing: ResourceTiming;
private _fallbackOverrides: SerializedFallbackOverrides = {};
_hasResponse = false;

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

url(): string {
Expand Down Expand Up @@ -204,6 +202,10 @@ export class Request extends ChannelOwner<channels.RequestChannel> 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());
Expand Down Expand Up @@ -649,6 +651,7 @@ export class Response extends ChannelOwner<channels.ResponseChannel> 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);
}

Expand Down
2 changes: 0 additions & 2 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'])),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
headers: request.headers(),
isNavigationRequest: request.isNavigationRequest(),
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
hasResponse: !!request._existingResponse(),
});
this._type_Request = true;
this._browserContextDispatcher = scope;
this.addObjectListener(Request.Events.Response, () => this._dispatchEvent('response', {}));
// Push existing response to the client if it exists.
ResponseDispatcher.fromNullable(scope, request._existingResponse());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will create a state where for a request with the response on the server side, response-less request will be created on the client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like hasResponse is the only way to make it work w/o fixing channels.

}

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

static from(scope: BrowserContextDispatcher, response: Response): ResponseDispatcher {
const result = scope.connection.existingDispatcher<ResponseDispatcher>(response);
const requestDispatcher = RequestDispatcher.from(scope, response.request());
const result = scope.connection.existingDispatcher<ResponseDispatcher>(response);
return result || new ResponseDispatcher(requestDispatcher, response);
}

Expand Down
10 changes: 10 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
3 changes: 1 addition & 2 deletions packages/playwright/src/mcp/browser/tools/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -62,7 +61,7 @@ const networkClear = defineTabTool({
});

async function renderRequest(request: playwright.Request, includeStatic: boolean): Promise<string | undefined> {
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;

Expand Down
4 changes: 0 additions & 4 deletions packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestResponseResult>;
rawRequestHeaders(params?: RequestRawRequestHeadersParams, progress?: Progress): Promise<RequestRawRequestHeadersResult>;
}
export type RequestResponseEvent = {};
export type RequestResponseParams = {};
export type RequestResponseOptions = {};
export type RequestResponseResult = {
Expand All @@ -3926,7 +3923,6 @@ export type RequestRawRequestHeadersResult = {
};

export interface RequestEvents {
'response': RequestResponseEvent;
}

// ----------- Route -----------
Expand Down
4 changes: 0 additions & 4 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3451,7 +3451,6 @@ Request:
items: NameValue
isNavigationRequest: boolean
redirectedFrom: Request?
hasResponse: boolean

commands:

Expand All @@ -3467,9 +3466,6 @@ Request:
type: array
items: NameValue

events:
response:

Route:
type: interface

Expand Down
34 changes: 34 additions & 0 deletions tests/page/page-network-response.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading