Skip to content

fix(network): request.postData() returns null for empty string body override#41882

Merged
dcrousso merged 3 commits into
microsoft:mainfrom
tsushanth:fix-postdata-empty-string-null
Jul 22, 2026
Merged

fix(network): request.postData() returns null for empty string body override#41882
dcrousso merged 3 commits into
microsoft:mainfrom
tsushanth:fix-postdata-empty-string-null

Conversation

@tsushanth

Copy link
Copy Markdown
Contributor

Summary

request.postData() incorrectly returns null when the POST body is set to an empty string via route.continue({ postData: '' }) or route.fallback({ postData: '' }).

Root cause

In packages/playwright-core/src/client/network.ts, the postData() method uses || null at the end:

// before
postData(): string | null {
  return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.toString('utf-8') || null;
}

When the override is set to an empty string, Buffer.from('', 'utf-8') is stored in _fallbackOverrides.postDataBuffer. Since an empty Buffer is a truthy object, the left-hand side resolves correctly to the empty buffer, and .toString('utf-8') yields ''. However, '' || null evaluates to null, discarding the empty-string body.

This is inconsistent with postDataBuffer(), which returns the correct empty Buffer for the same input.

Fix

Switch the trailing || null to ?? null (nullish coalescing), which only falls back to null when the value is null or undefined, not when it is an empty string:

// after
postData(): string | null {
  return (this._fallbackOverrides.postDataBuffer ?? this._initializer.postData)?.toString('utf-8') ?? null;
}

The inner || is similarly changed to ?? so that a zero-length override buffer isn't accidentally skipped in favour of the original request's body.

Reproduction

test('postData returns empty string for empty body override', async ({ page }) => {
  await page.route('**/api', async route => {
    await route.continue({ postData: '' });
  });

  page.on('request', request => {
    // Before fix: null  After fix: ''
    expect(request.postData()).toBe('');
  });

  await page.evaluate(() => fetch('/api', { method: 'POST', body: 'original' }));
});

@dcrousso dcrousso left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this looks like a good fix (minus the \n issue) but it needs a test

Comment on lines +224 to +225
].join('
'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

oops? i think you may have accidentally replaced all \n with an actual newline elsewhere in this file

@tsushanth

Copy link
Copy Markdown
Contributor Author

Thanks for catching those — addressed in the latest push:

  • Restored the three escaped \n sequences that were accidentally converted to literal newlines (.join('\n'), rewriteErrorMessage, get() header join)
  • Added a test in tests/page/page-request-continue.spec.ts that overrides a POST body with '' and asserts request.postData() returns '' rather than null

Comment on lines +1022 to +1025
page.on('request', request => {
if (request.url().includes('empty-post'))
captured.push(request.postData());
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i dont think this will work as im pretty sure it runs before the route applies

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@tsushanth

Copy link
Copy Markdown
Contributor Author

Fixed the test — was using page.waitForRequest('**') (too broad, could catch background requests) and waiting for the client fetch to resolve (hangs until server responds). Switched to the pattern used by all other postData tests in this file: server.waitForRequest('/sleep.zzz') + check serverRequest.postBody. CI should pass now.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "tests 1"

2 flaky ⚠️ [chromium-library] › library/chromium/chromium.spec.ts:371 › should produce network events, routing, and annotations for Service Worker `@chromium-ubuntu-22.04-node24`
⚠️ [firefox-page] › page/page-goto.spec.ts:81 › should work with Cross-Origin-Opener-Policy `@firefox-ubuntu-22.04-node20`

50006 passed, 1189 skipped


Merge workflow run.

@github-actions

Copy link
Copy Markdown
Contributor

Test results for "MCP"

48 failed
❌ [firefox] › mcp/annotate.spec.ts:173 › user-initiated annotate downloads zip with feedback.md @mcp-macos-latest-firefox
❌ [firefox] › mcp/annotate.spec.ts:446 › should switch screencast to -s session on show --annotate @mcp-macos-latest-firefox
❌ [webkit] › mcp/cookies.spec.ts:19 › browser_cookie_list unavailable without storage capability @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:34 › browser_cookie_list shows no cookies when empty @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:55 › browser_cookie_set and browser_cookie_get @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:82 › browser_cookie_list shows cookies @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:116 › browser_cookie_list filters by domain @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:150 › browser_cookie_get returns not found for missing cookie @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:170 › browser_cookie_set with all options @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:215 › browser_cookie_delete removes cookie @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:257 › browser_cookie_delete for nonexistent cookie does not error @mcp-macos-latest-webkit
❌ [webkit] › mcp/cookies.spec.ts:290 › browser_cookie_clear removes all cookies @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:21 › browser_navigate @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:33 › browser_navigate surfaces non-2xx HTTP status @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:60 › browser_navigate blocks file:// URLs by default @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:70 › browser_navigate allows about:, data: and javascript: protocols @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:90 › browser_navigate can navigate to file:// URLs allowUnrestrictedFileAccess is true @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:116 › browser_navigate_back does not time out when load never fires @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:142 › browser_select_option @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:170 › browser_select_option (multiple) @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:201 › browser_resize @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:230 › old locator error message @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:272 › visibility: hidden > visible should be shown @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:293 › snapshot depth @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:332 › snapshot with boxes @mcp-macos-latest-webkit
❌ [webkit] › mcp/core.spec.ts:357 › snapshot by ref @mcp-macos-latest-webkit
❌ [webkit] › mcp/dashboard.spec.ts:54 › should show one row per context for a single browser @mcp-macos-latest-webkit
❌ [webkit] › mcp/dashboard.spec.ts:69 › should show current workspace sessions first @mcp-macos-latest-webkit
❌ [webkit] › mcp/dashboard.spec.ts:108 › should activate session when show is called with -s @mcp-macos-latest-webkit
❌ [webkit] › mcp/dashboard.spec.ts:139 › should allow typing in omnibox in interactive mode @mcp-macos-latest-webkit
❌ [webkit] › mcp/dashboard.spec.ts:161 › save recording streams WebM bytes to the chosen file @mcp-macos-latest-webkit
❌ [webkit] › mcp/device.spec.ts:29 › --device should work @mcp-macos-latest-webkit
❌ [webkit] › mcp/device.spec.ts:49 › --mobile emulates a mobile viewport @mcp-macos-latest-webkit
❌ [webkit] › mcp/devtools.spec.ts:21 › browser_highlight @mcp-macos-latest-webkit
❌ [webkit] › mcp/devtools.spec.ts:42 › browser_highlight with style @mcp-macos-latest-webkit
❌ [webkit] › mcp/devtools.spec.ts:74 › browser_hide_highlight @mcp-macos-latest-webkit
❌ [webkit] › mcp/devtools.spec.ts:96 › browser_resume completes when context closes @mcp-macos-latest-webkit
❌ [webkit] › mcp/devtools.spec.ts:119 › browser_hide_highlight all @mcp-macos-latest-webkit
❌ [webkit] › mcp/dialogs.spec.ts:19 › alert dialog @mcp-macos-latest-webkit
❌ [webkit] › mcp/dialogs.spec.ts:61 › two alert dialogs @mcp-macos-latest-webkit
❌ [webkit] › mcp/dialogs.spec.ts:110 › confirm dialog (true) @mcp-macos-latest-webkit
❌ [webkit] › mcp/dialogs.spec.ts:145 › confirm dialog (false) @mcp-macos-latest-webkit
❌ [webkit] › mcp/dialogs.spec.ts:180 › prompt dialog @mcp-macos-latest-webkit
❌ [webkit] › mcp/dialogs.spec.ts:218 › alert dialog w/ race @mcp-macos-latest-webkit
❌ [webkit] › mcp/evaluate.spec.ts:19 › browser_evaluate @mcp-macos-latest-webkit
❌ [webkit] › mcp/evaluate.spec.ts:38 › browser_evaluate (element) @mcp-macos-latest-webkit
❌ [webkit] › mcp/evaluate.spec.ts:60 › browser_evaluate object @mcp-macos-latest-webkit
❌ [webkit] › mcp/evaluate.spec.ts:79 › browser_evaluate expression @mcp-macos-latest-webkit

7712 passed, 1249 skipped


Merge workflow run.

@dcrousso
dcrousso merged commit f5fa967 into microsoft:main Jul 22, 2026
46 of 48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants