From b1fd3b5398de978632dc5f851bcafe18bf3b2b30 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:47:45 +0800 Subject: [PATCH 1/2] fix: keep blank-valued query params significant in query_string_matcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit query_string_matcher parsed both the request and the configured query with parse_qsl()'s default keep_blank_values=False, so a parameter with an empty value (b=, or a bare b) was dropped before comparison. As a result ?a=1 matched a request to ?a=1&b=, and a registration that required b= matched a request that omitted it. This auto-applies to any registered URL that carries a query string (via _should_match_querystring), so it silently mismatches on the default path. The request-param parser already uses keep_blank_values=True (__init__.py), so query_param_matcher treats b= as significant — the two query-matching paths disagreed on the same request. Parse both sides with keep_blank_values=True so blanks are compared, not discarded. Co-Authored-By: Claude Opus 4.8 (1M context) --- responses/matchers.py | 8 ++++++-- responses/tests/test_matchers.py | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/responses/matchers.py b/responses/matchers.py index 4b9d1f1b..d61decc1 100644 --- a/responses/matchers.py +++ b/responses/matchers.py @@ -259,8 +259,12 @@ def match(request: PreparedRequest) -> Tuple[bool, str]: data = parse_url(request.url or "") request_query = data.query - request_qsl = sorted(parse_qsl(request_query)) if request_query else {} - matcher_qsl = sorted(parse_qsl(query)) if query else {} + request_qsl = ( + sorted(parse_qsl(request_query, keep_blank_values=True)) + if request_query + else {} + ) + matcher_qsl = sorted(parse_qsl(query, keep_blank_values=True)) if query else {} valid = not query if request_query is None else request_qsl == matcher_qsl diff --git a/responses/tests/test_matchers.py b/responses/tests/test_matchers.py index 688e51d1..fc4fbc6f 100644 --- a/responses/tests/test_matchers.py +++ b/responses/tests/test_matchers.py @@ -69,6 +69,40 @@ def run(): assert_reset() +def test_query_string_matcher_blank_values(): + """A blank-valued query param (``foo=``) is significant: it must be present + to match, and an extra blank param must not be ignored.""" + + def run(): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + responses.GET, + "http://example.com", + body=b"test", + match=[matchers.query_string_matcher("test=1&foo=")], + ) + # Exact match, including the blank param, succeeds. + resp = requests.get("http://example.com?test=1&foo=") + assert_response(resp, "test") + # Omitting the required blank param must not match. + with pytest.raises(ConnectionError): + requests.get("http://example.com?test=1") + + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + rsps.add( + responses.GET, + "http://example.com", + body=b"test", + match=[matchers.query_string_matcher("test=1")], + ) + # An extra blank param must not be silently ignored. + with pytest.raises(ConnectionError): + requests.get("http://example.com?test=1&foo=") + + run() + assert_reset() + + def test_request_matches_post_params(): @responses.activate def run(deprecated): From 2f8ce936f9c33ce88867bb250a0303a6bb79f1cb Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:48:31 +0800 Subject: [PATCH 2/2] docs: add CHANGES entry for #804 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGES | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES b/CHANGES index b286c134..dcdce261 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,11 @@ +0.26.3 +------ + +* Fixed `query_string_matcher` (and the query matching auto-applied to a + registered URL's own query string) discarding blank-valued query params + (``b=``), which caused requests with an extra or missing blank param to + match incorrectly. See #804 + 0.26.2 ------