From e2f9ad045879e5520372d315f7515e22e6b07e2e Mon Sep 17 00:00:00 2001 From: suraj-self Date: Sun, 11 Oct 2026 22:32:47 +0530 Subject: [PATCH 1/2] fix: validate mutually-exclusive body options under python -O --- playwright/_impl/_fetch.py | 5 ++-- tests/async/test_fetch_global.py | 41 +++++++++++++++++++++++++++++++- tests/sync/test_fetch_global.py | 38 ++++++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index 2694ee5e0..ea1c88e74 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -391,11 +391,12 @@ async def _inner_fetch( ) -> "APIResponse": if self._close_reason: raise TargetClosedError(self._close_reason) - assert ( + if ( (1 if data is not None else 0) + (1 if form is not None else 0) + (1 if multipart is not None else 0) - ) <= 1, "Only one of 'data', 'form' or 'multipart' can be specified" + ) > 1: + raise Error("Only one of 'data', 'form' or 'multipart' can be specified") assert ( maxRedirects is None or maxRedirects >= 0 ), "'max_redirects' must be greater than or equal to '0'" diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index 5a0849c97..59606f723 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -16,6 +16,7 @@ import base64 import json import re +import subprocess import sys from pathlib import Path from typing import Any, List @@ -507,7 +508,7 @@ async def test_should_reject_empty_data_with_form( ) -> None: request = await playwright.request.new_context() with pytest.raises( - AssertionError, + Error, match="Only one of 'data', 'form' or 'multipart' can be specified", ): await request.post( @@ -518,6 +519,44 @@ async def test_should_reject_empty_data_with_form( await request.dispose() +async def test_should_reject_multiple_body_options_under_python_optimization( + server: Server, +) -> None: + script = f""" +import asyncio +from playwright.async_api import async_playwright, Error + +async def main(): + async with async_playwright() as p: + request = await p.request.new_context() + try: + await request.post( + "{server.EMPTY_PAGE}", + data="", + form={{"name": "value"}}, + ) + except Error as e: + expected = "Only one of 'data', 'form' or 'multipart' can be specified" + if expected not in str(e): + raise + else: + raise RuntimeError("Conflicting body options were accepted") + finally: + await request.dispose() + +asyncio.run(main()) +""" + + result = subprocess.run( + [sys.executable, "-O", "-c", script], + capture_output=True, + text=True, + timeout=60, + ) + + assert result.returncode == 0, result.stdout + result.stderr + + async def test_should_retry_ECONNRESET(playwright: Playwright, server: Server) -> None: request_count = 0 diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index 604a87a07..79cab2f4e 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -14,6 +14,8 @@ import json import re +import subprocess +import sys from pathlib import Path from typing import List from urllib.parse import urlparse @@ -351,7 +353,7 @@ def test_should_reject_empty_data_with_form( ) -> None: request = playwright.request.new_context() with pytest.raises( - AssertionError, + Error, match="Only one of 'data', 'form' or 'multipart' can be specified", ): request.post( @@ -362,6 +364,40 @@ def test_should_reject_empty_data_with_form( request.dispose() +def test_should_reject_multiple_body_options_under_python_optimization( + server: Server, +) -> None: + script = f""" +from playwright.sync_api import sync_playwright, Error + +with sync_playwright() as p: + request = p.request.new_context() + try: + request.post( + "{server.EMPTY_PAGE}", + data="", + form={{"name": "value"}}, + ) + except Error as e: + expected = "Only one of 'data', 'form' or 'multipart' can be specified" + if expected not in str(e): + raise + else: + raise RuntimeError("Conflicting body options were accepted") + finally: + request.dispose() +""" + + result = subprocess.run( + [sys.executable, "-O", "-c", script], + capture_output=True, + text=True, + timeout=60, + ) + + assert result.returncode == 0, result.stdout + result.stderr + + def test_should_throw_when_fail_on_status_code_is_true( playwright: Playwright, server: Server ) -> None: From 414c016ad2defb7344558dd0eee49c60467d2efd Mon Sep 17 00:00:00 2001 From: suraj-self Date: Mon, 12 Oct 2026 23:54:54 +0530 Subject: [PATCH 2/2] fix: preserve body option validation under python -O --- playwright/_impl/_fetch.py | 4 +++- tests/async/test_fetch_global.py | 41 +------------------------------- tests/sync/test_fetch_global.py | 38 +---------------------------- 3 files changed, 5 insertions(+), 78 deletions(-) diff --git a/playwright/_impl/_fetch.py b/playwright/_impl/_fetch.py index ea1c88e74..3005096eb 100644 --- a/playwright/_impl/_fetch.py +++ b/playwright/_impl/_fetch.py @@ -396,7 +396,9 @@ async def _inner_fetch( + (1 if form is not None else 0) + (1 if multipart is not None else 0) ) > 1: - raise Error("Only one of 'data', 'form' or 'multipart' can be specified") + raise AssertionError( + "Only one of 'data', 'form' or 'multipart' can be specified" + ) assert ( maxRedirects is None or maxRedirects >= 0 ), "'max_redirects' must be greater than or equal to '0'" diff --git a/tests/async/test_fetch_global.py b/tests/async/test_fetch_global.py index 59606f723..5a0849c97 100644 --- a/tests/async/test_fetch_global.py +++ b/tests/async/test_fetch_global.py @@ -16,7 +16,6 @@ import base64 import json import re -import subprocess import sys from pathlib import Path from typing import Any, List @@ -508,7 +507,7 @@ async def test_should_reject_empty_data_with_form( ) -> None: request = await playwright.request.new_context() with pytest.raises( - Error, + AssertionError, match="Only one of 'data', 'form' or 'multipart' can be specified", ): await request.post( @@ -519,44 +518,6 @@ async def test_should_reject_empty_data_with_form( await request.dispose() -async def test_should_reject_multiple_body_options_under_python_optimization( - server: Server, -) -> None: - script = f""" -import asyncio -from playwright.async_api import async_playwright, Error - -async def main(): - async with async_playwright() as p: - request = await p.request.new_context() - try: - await request.post( - "{server.EMPTY_PAGE}", - data="", - form={{"name": "value"}}, - ) - except Error as e: - expected = "Only one of 'data', 'form' or 'multipart' can be specified" - if expected not in str(e): - raise - else: - raise RuntimeError("Conflicting body options were accepted") - finally: - await request.dispose() - -asyncio.run(main()) -""" - - result = subprocess.run( - [sys.executable, "-O", "-c", script], - capture_output=True, - text=True, - timeout=60, - ) - - assert result.returncode == 0, result.stdout + result.stderr - - async def test_should_retry_ECONNRESET(playwright: Playwright, server: Server) -> None: request_count = 0 diff --git a/tests/sync/test_fetch_global.py b/tests/sync/test_fetch_global.py index 79cab2f4e..604a87a07 100644 --- a/tests/sync/test_fetch_global.py +++ b/tests/sync/test_fetch_global.py @@ -14,8 +14,6 @@ import json import re -import subprocess -import sys from pathlib import Path from typing import List from urllib.parse import urlparse @@ -353,7 +351,7 @@ def test_should_reject_empty_data_with_form( ) -> None: request = playwright.request.new_context() with pytest.raises( - Error, + AssertionError, match="Only one of 'data', 'form' or 'multipart' can be specified", ): request.post( @@ -364,40 +362,6 @@ def test_should_reject_empty_data_with_form( request.dispose() -def test_should_reject_multiple_body_options_under_python_optimization( - server: Server, -) -> None: - script = f""" -from playwright.sync_api import sync_playwright, Error - -with sync_playwright() as p: - request = p.request.new_context() - try: - request.post( - "{server.EMPTY_PAGE}", - data="", - form={{"name": "value"}}, - ) - except Error as e: - expected = "Only one of 'data', 'form' or 'multipart' can be specified" - if expected not in str(e): - raise - else: - raise RuntimeError("Conflicting body options were accepted") - finally: - request.dispose() -""" - - result = subprocess.run( - [sys.executable, "-O", "-c", script], - capture_output=True, - text=True, - timeout=60, - ) - - assert result.returncode == 0, result.stdout + result.stderr - - def test_should_throw_when_fail_on_status_code_is_true( playwright: Playwright, server: Server ) -> None: