From 6a369712924aae232d52d98f892dab387d2e675d Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 12 Oct 2026 09:42:41 -0700 Subject: [PATCH] fix(fetch): send empty form and multipart bodies `form={}` and `multipart={}` were ignored because empty dicts are falsy, so the request went out without a body or Content-Type. Check for None instead, like `data`, and match the Node client: an empty form is sent as application/x-www-form-urlencoded and an empty multipart as an empty multipart/form-data body. Fixes: microsoft/playwright#43286 --- playwright/_impl/_fetch.py | 6 +++--- tests/async/test_fetch_browser_context.py | 25 +++++++++++++++++++++++ tests/sync/test_fetch_browser_context.py | 22 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index 2694ee5e0..be8f783df 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -427,7 +427,7 @@ async def _inner_fetch( raise Error(str(e)) from None else: raise Error(f"Unsupported 'data' type: {type(data)}") - elif form: + elif form is not None: if isinstance(form, FormData): form_data = [] for fd_name, fd_value in form._fields: @@ -437,8 +437,8 @@ async def _inner_fetch( ) form_data.append(NameValue(name=fd_name, value=str(fd_value))) else: - form_data = object_to_array(form) - elif multipart: + form_data = object_to_array(form) or [] + elif multipart is not None: multipart_data = [] if isinstance(multipart, FormData): for fd_name, fd_value in multipart._fields: diff --git a/tests/async/test_fetch_browser_context.py b/tests/async/test_fetch_browser_context.py index 5f57b360a..c14c3f016 100644 --- a/tests/async/test_fetch_browser_context.py +++ b/tests/async/test_fetch_browser_context.py @@ -365,6 +365,31 @@ async def test_should_support_multipart_form_data( assert request.args[b"file"][0] == file["buffer"] +async def test_should_send_empty_form(context: BrowserContext, server: Server) -> None: + [request, _] = await asyncio.gather( + server.wait_for_request("/empty.html"), + context.request.post(server.EMPTY_PAGE, form={}), + ) + assert request.getHeader("Content-Type") == "application/x-www-form-urlencoded" + assert request.getHeader("Content-Length") == "0" + + +async def test_should_send_empty_multipart( + context: BrowserContext, server: Server +) -> None: + # The test server rejects an empty multipart body in POST requests before handling them. + [request, _] = await asyncio.gather( + server.wait_for_request("/empty.html"), + context.request.put(server.EMPTY_PAGE, multipart={}), + ) + assert cast(str, request.getHeader("Content-Type")).startswith( + "multipart/form-data; " + ) + assert must(request.getHeader("Content-Length")) == str( + len(must(request.post_body)) + ) + + async def test_should_support_form_data_with_repeated_keys( context: BrowserContext, server: Server ) -> None: diff --git a/tests/sync/test_fetch_browser_context.py b/tests/sync/test_fetch_browser_context.py index ca67f5d9e..f0f463d22 100644 --- a/tests/sync/test_fetch_browser_context.py +++ b/tests/sync/test_fetch_browser_context.py @@ -288,6 +288,28 @@ def test_should_support_multipart_form_data( assert server_req.value.args[b"file"][0] == file["buffer"] +def test_should_send_empty_form(context: BrowserContext, server: Server) -> None: + with server.expect_request("/empty.html") as server_req: + context.request.post(server.EMPTY_PAGE, form={}) + assert ( + server_req.value.getHeader("Content-Type") + == "application/x-www-form-urlencoded" + ) + assert server_req.value.getHeader("Content-Length") == "0" + + +def test_should_send_empty_multipart(context: BrowserContext, server: Server) -> None: + # The test server rejects an empty multipart body in POST requests before handling them. + with server.expect_request("/empty.html") as server_req: + context.request.put(server.EMPTY_PAGE, multipart={}) + content_type = server_req.value.getHeader("Content-Type") + assert content_type + assert content_type.startswith("multipart/form-data; ") + assert server_req.value.getHeader("Content-Length") == str( + len(must(server_req.value.post_body)) + ) + + def test_should_support_form_data_with_repeated_keys( context: BrowserContext, server: Server ) -> None: