diff --git a/packages/playwright-core/src/tools/backend/response.ts b/packages/playwright-core/src/tools/backend/response.ts index 54eb8c0cf6548..fcb2021e94cda 100644 --- a/packages/playwright-core/src/tools/backend/response.ts +++ b/packages/playwright-core/src/tools/backend/response.ts @@ -302,6 +302,9 @@ export function renderTabMarkdown(tab: TabHeader): string[] { lines.push(`- Page Title: ${tab.title}`); if (tab.crashed) lines.push(`- Page status: crashed`); + const status = tab.mainDocumentStatus; + if (status && (status.status < 200 || status.status >= 300)) + lines.push(`- HTTP status: ${status.status}${status.statusText ? ' ' + status.statusText : ''}`); if (tab.console.errors || tab.console.warnings) lines.push(`- Console: ${tab.console.errors} errors, ${tab.console.warnings} warnings`); return lines; diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index 65e85c1ae1004..a2ac803ca4862 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -77,6 +77,7 @@ export type TabHeader = { url: string; current: boolean; crashed: boolean; + mainDocumentStatus?: { status: number, statusText: string }; console: { total: number, warnings: number, errors: number }; }; @@ -93,6 +94,7 @@ export class Tab extends EventEmitter { private _lastHeader: TabHeader = { title: 'about:blank', url: 'about:blank', current: false, crashed: false, console: { total: 0, warnings: 0, errors: 0 } }; private _downloads: Download[] = []; private _requests: playwright.Request[] = []; + private _mainDocumentStatus: { status: number, statusText: string } | undefined; private _onPageClose: (tab: Tab) => void; crashed = false; private _modalStates: ModalState[] = []; @@ -219,6 +221,7 @@ export class Tab extends EventEmitter { private _clearCollectedArtifacts() { this._downloads.length = 0; this._requests.length = 0; + this._mainDocumentStatus = undefined; this._recentEventEntries.length = 0; this._resetLogs(); } @@ -238,9 +241,12 @@ export class Tab extends EventEmitter { } private _handleResponse(response: playwright.Response) { - const timing = response.request().timing(); + const request = response.request(); + if (request.isNavigationRequest() && response.frame() === this.page.mainFrame() && !request.redirectedTo()) + this._mainDocumentStatus = { status: response.status(), statusText: response.statusText() }; + const timing = request.timing(); const wallTime = timing.responseStart + timing.startTime; - this._addLogEntry({ type: 'request', wallTime, request: response.request() }); + this._addLogEntry({ type: 'request', wallTime, request }); } private _handleRequestFailed(request: playwright.Request) { @@ -284,6 +290,7 @@ export class Tab extends EventEmitter { url: this.page.url(), current: this.isCurrentTab(), crashed: this.crashed, + mainDocumentStatus: this._mainDocumentStatus, console: consoleCounts, }; @@ -583,6 +590,8 @@ function tabHeaderEquals(a: TabHeader, b: TabHeader): boolean { a.url === b.url && a.current === b.current && a.crashed === b.crashed && + a.mainDocumentStatus?.status === b.mainDocumentStatus?.status && + a.mainDocumentStatus?.statusText === b.mainDocumentStatus?.statusText && a.console.errors === b.console.errors && a.console.warnings === b.console.warnings && a.console.total === b.console.total; diff --git a/tests/mcp/core.spec.ts b/tests/mcp/core.spec.ts index 06423357b550d..61fd4627c28b2 100644 --- a/tests/mcp/core.spec.ts +++ b/tests/mcp/core.spec.ts @@ -30,6 +30,33 @@ test('browser_navigate', async ({ client, server }) => { }); }); +test('browser_navigate surfaces non-2xx HTTP status', async ({ client, server }) => { + server.setRoute('/locked', (req, res) => { + res.writeHead(402, { 'Content-Type': 'text/html' }); + res.end('Payment RequiredPay up'); + }); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX + '/locked' }, + })).toHaveResponse({ + page: expect.stringContaining(`- HTTP status: 402 Payment Required`), + }); + + // A redirect to a 2xx page must not carry a status line: the intermediate + // 302 hop must not leak, and the final 2xx landing renders nothing. + server.setRoute('/redirect', (req, res) => { + res.writeHead(302, { location: server.HELLO_WORLD }); + res.end(); + }); + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX + '/redirect' }, + })).not.toHaveResponse({ + page: expect.stringContaining('HTTP status'), + }); +}); + test('browser_navigate blocks file:// URLs by default', async ({ client }) => { expect(await client.callTool({ name: 'browser_navigate',