From 5cab790bdb6793497305b7c1cc7a76f7f021d7ac Mon Sep 17 00:00:00 2001 From: tsushanth <78000697+tsushanth@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:34:05 -0700 Subject: [PATCH 1/3] fix(network): use nullish coalescing in postData() to preserve empty string body --- packages/playwright-core/src/client/network.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 4d36f5dd0a8b1..5421060e2da90 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -137,7 +137,7 @@ export class Request extends ChannelOwner implements ap } postData(): string | null { - return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.toString('utf-8') || null; + return (this._fallbackOverrides.postDataBuffer ?? this._initializer.postData)?.toString('utf-8') ?? null; } postDataBuffer(): Buffer | null { @@ -221,7 +221,8 @@ export class Request extends ChannelOwner implements ap 'Frame for this navigation request is not available, because the request', 'was issued before the frame is created. You can check whether the request', 'is a navigation request by calling isNavigationRequest() method.', - ].join('\n')); + ].join(' +')); } return frame; } @@ -885,7 +886,9 @@ export class RouteHandler { if (isTargetClosedError(e)) { // We are failing in the handler because the target close closed. // Give user a hint! - rewriteErrorMessage(e, `"${e.message}" while running route callback.\nConsider awaiting \`await page.unrouteAll({ behavior: 'ignoreErrors' })\`\nbefore the end of the test to ignore remaining routes in flight.`); + rewriteErrorMessage(e, `"${e.message}" while running route callback. +Consider awaiting \`await page.unrouteAll({ behavior: 'ignoreErrors' })\` +before the end of the test to ignore remaining routes in flight.`); } throw e; } finally { @@ -949,7 +952,8 @@ export class RawHeaders { const values = this.getAll(name); if (!values || !values.length) return null; - return values.join(name.toLowerCase() === 'set-cookie' ? '\n' : ', '); + return values.join(name.toLowerCase() === 'set-cookie' ? ' +' : ', '); } getAll(name: string): string[] { From ee71fceb54d5539c2a38f6c6bb125ed49a4ede0f Mon Sep 17 00:00:00 2001 From: tsushanth Date: Mon, 20 Jul 2026 16:39:19 -0700 Subject: [PATCH 2/3] fix(network): postData() returns null for empty string override; restore escaped newlines --- packages/playwright-core/src/client/network.ts | 10 +++------- tests/page/page-request-continue.spec.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 5421060e2da90..82286526e8c9f 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -221,8 +221,7 @@ export class Request extends ChannelOwner implements ap 'Frame for this navigation request is not available, because the request', 'was issued before the frame is created. You can check whether the request', 'is a navigation request by calling isNavigationRequest() method.', - ].join(' -')); + ].join('\n')); } return frame; } @@ -886,9 +885,7 @@ export class RouteHandler { if (isTargetClosedError(e)) { // We are failing in the handler because the target close closed. // Give user a hint! - rewriteErrorMessage(e, `"${e.message}" while running route callback. -Consider awaiting \`await page.unrouteAll({ behavior: 'ignoreErrors' })\` -before the end of the test to ignore remaining routes in flight.`); + rewriteErrorMessage(e, `"${e.message}" while running route callback.\nConsider awaiting \`await page.unrouteAll({ behavior: 'ignoreErrors' })\`\nbefore the end of the test to ignore remaining routes in flight.`); } throw e; } finally { @@ -952,8 +949,7 @@ export class RawHeaders { const values = this.getAll(name); if (!values || !values.length) return null; - return values.join(name.toLowerCase() === 'set-cookie' ? ' -' : ', '); + return values.join(name.toLowerCase() === 'set-cookie' ? '\n' : ', '); } getAll(name: string): string[] { diff --git a/tests/page/page-request-continue.spec.ts b/tests/page/page-request-continue.spec.ts index 48f9458012b46..81740b49cab8e 100644 --- a/tests/page/page-request-continue.spec.ts +++ b/tests/page/page-request-continue.spec.ts @@ -1010,3 +1010,21 @@ it('should not forward Host header on cross-origin redirect', { expect(firstHost).toBe(new URL(server.PREFIX).host); expect(redirectedHost).toBe(new URL(server.CROSS_PROCESS_PREFIX).host); }); + +it('postData should return empty string when overriding body with empty string', async ({ page, server }) => { + server.setRoute('/empty-post', (req, res) => { req.resume(); res.end('OK'); }); + + const captured: (string | null)[] = []; + await page.route(`${server.PREFIX}/empty-post`, async route => { + await route.continue({ postData: '' }); + }); + + page.on('request', request => { + if (request.url().includes('empty-post')) + captured.push(request.postData()); + }); + + await page.evaluate(url => fetch(url, { method: 'POST', body: 'original' }), `${server.PREFIX}/empty-post`); + expect(captured).toHaveLength(1); + expect(captured[0]).toBe(''); +}); From 47590f0af77f3beb59c89c00bfea9a4bfe1153d3 Mon Sep 17 00:00:00 2001 From: tsushanth Date: Tue, 21 Jul 2026 06:51:49 -0700 Subject: [PATCH 3/3] test(network): fix postData empty-string test to use waitForRequest pattern --- tests/page/page-request-continue.spec.ts | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/page/page-request-continue.spec.ts b/tests/page/page-request-continue.spec.ts index 81740b49cab8e..a6b4137912db5 100644 --- a/tests/page/page-request-continue.spec.ts +++ b/tests/page/page-request-continue.spec.ts @@ -1012,19 +1012,13 @@ it('should not forward Host header on cross-origin redirect', { }); it('postData should return empty string when overriding body with empty string', async ({ page, server }) => { - server.setRoute('/empty-post', (req, res) => { req.resume(); res.end('OK'); }); - - const captured: (string | null)[] = []; - await page.route(`${server.PREFIX}/empty-post`, async route => { - await route.continue({ postData: '' }); - }); - - page.on('request', request => { - if (request.url().includes('empty-post')) - captured.push(request.postData()); + await page.goto(server.EMPTY_PAGE); + await page.route('**/*', route => { + void route.continue({ postData: '' }); }); - - await page.evaluate(url => fetch(url, { method: 'POST', body: 'original' }), `${server.PREFIX}/empty-post`); - expect(captured).toHaveLength(1); - expect(captured[0]).toBe(''); + const [request] = await Promise.all([ + page.waitForRequest('**'), + page.evaluate(({ url }) => fetch(url, { method: 'POST', body: 'original' }), { url: server.PREFIX + '/sleep.zzz' }), + ]); + expect(request.postData()).toBe(''); });