From 96be436763110ccca65f731addfb1d453d6aac74 Mon Sep 17 00:00:00 2001 From: CHINMAY <89741289+Das-Chinmay@users.noreply.github.com> Date: Thu, 26 Mar 2026 22:03:37 -0700 Subject: [PATCH 1/6] gh-47005: fix do_open() to let regular headers override unredirected headers AbstractHTTPHandler.do_open() was building the request header dict by starting with unredirected_hdrs and only inserting regular headers that were not already present, giving unredirected headers priority. This contradicts get_header() and header_items(), both of which give regular headers the higher priority. Fix by unconditionally updating with req.headers so that a header set via add_header() always overrides one set via add_unredirected_header(). --- Lib/test/test_urllib2.py | 17 +++++++++++++++++ Lib/urllib/request.py | 3 +-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 3a77b9e5ab7928..b3cbb20557e37b 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -980,6 +980,23 @@ def test_http(self): self.assertEqual(req.unredirected_hdrs["Host"], "baz") self.assertEqual(req.unredirected_hdrs["Spam"], "foo") + def test_http_header_priority(self): + # gh-47005: regular headers set via add_header() must override + # unredirected headers with the same name in do_open(), consistent + # with get_header() and header_items(). + h = urllib.request.AbstractHTTPHandler() + h.parent = MockOpener() + + req = Request("http://example.com/", headers={"Content-Type": "application/json"}) + req.timeout = None + req.add_unredirected_header("Content-Type", "application/x-www-form-urlencoded") + + http = MockHTTPClass() + h.do_open(http, req) + + sent_headers = dict(http.req_headers) + self.assertEqual(sent_headers["Content-Type"], "application/json") + def test_http_body_file(self): # A regular file - chunked encoding is used unless Content Length is # already set. diff --git a/Lib/urllib/request.py b/Lib/urllib/request.py index f5f17f223a4585..660301fef61258 100644 --- a/Lib/urllib/request.py +++ b/Lib/urllib/request.py @@ -1293,8 +1293,7 @@ def do_open(self, http_class, req, **http_conn_args): h.set_debuglevel(self._debuglevel) headers = dict(req.unredirected_hdrs) - headers.update({k: v for k, v in req.headers.items() - if k not in headers}) + headers.update(req.headers) # TODO(jhylton): Should this be redesigned to handle # persistent connections? From 35a8c08cf40673898187cf04fa0402dd21a8ff29 Mon Sep 17 00:00:00 2001 From: CHINMAY <89741289+Das-Chinmay@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:10:27 -0700 Subject: [PATCH 2/6] gh-47005: add news entry --- .../Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst b/Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst new file mode 100644 index 00000000000000..a7c65e7d234bbc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst @@ -0,0 +1,4 @@ +Fix :meth:`urllib.request.AbstractHTTPHandler.do_open` to give regular +headers set via :meth:`~urllib.request.Request.add_header` priority over +unredirected headers, consistent with :meth:`~urllib.request.Request.get_header` +and :meth:`~urllib.request.Request.header_items`. From 28eb8ff2cf65eff0421bdd72d93df87385810bdb Mon Sep 17 00:00:00 2001 From: CHINMAY <89741289+Das-Chinmay@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:25:33 -0700 Subject: [PATCH 3/6] gh-47005: trigger CI rerun From a08e13f5205e6589e2e195152c6f059081b68e0f Mon Sep 17 00:00:00 2001 From: CHINMAY <89741289+Das-Chinmay@users.noreply.github.com> Date: Thu, 26 Mar 2026 23:29:58 -0700 Subject: [PATCH 4/6] gh-47005: fix invalid cross-reference in news entry --- .../next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst b/Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst index a7c65e7d234bbc..646367f0fa069d 100644 --- a/Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst +++ b/Misc/NEWS.d/next/Library/2026-03-26-00-00-00.gh-issue-47005.xxc89c.rst @@ -1,4 +1,4 @@ -Fix :meth:`urllib.request.AbstractHTTPHandler.do_open` to give regular +Fix :meth:`!urllib.request.AbstractHTTPHandler.do_open` to give regular headers set via :meth:`~urllib.request.Request.add_header` priority over unredirected headers, consistent with :meth:`~urllib.request.Request.get_header` and :meth:`~urllib.request.Request.header_items`. From f7eab12562e2964042322cb8f3ccbef6fb40fd47 Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Wed, 8 Jul 2026 17:07:45 -0700 Subject: [PATCH 5/6] Convert the test case into a table driven tests with additional test cases. --- Lib/test/test_urllib2.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index b3cbb20557e37b..a4e4fd411f6d33 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -984,18 +984,29 @@ def test_http_header_priority(self): # gh-47005: regular headers set via add_header() must override # unredirected headers with the same name in do_open(), consistent # with get_header() and header_items(). + cases = [ + ("Content-Type", "application/json", "application/x-www-form-urlencoded"), + ("Content-Length", "99", "0"), + ("Host", "override.example.com", "internal.example.com"), + ("Authorization", "Bearer user-token", "Basic stale="), + ("Cookie", "a=1", "b=2"), + ("User-Agent", "MyApp/1.0", "Python-urllib/test"), + ] h = urllib.request.AbstractHTTPHandler() h.parent = MockOpener() - req = Request("http://example.com/", headers={"Content-Type": "application/json"}) - req.timeout = None - req.add_unredirected_header("Content-Type", "application/x-www-form-urlencoded") + for key, regular, unredirected in cases: + req = Request("http://example.com/", headers={key: regular}) + req.timeout = None + req.add_unredirected_header(key, unredirected) - http = MockHTTPClass() - h.do_open(http, req) + http = MockHTTPClass() + h.do_open(http, req) - sent_headers = dict(http.req_headers) - self.assertEqual(sent_headers["Content-Type"], "application/json") + sent_headers = dict(http.req_headers) + self.assertEqual(sent_headers[key], regular) + self.assertEqual(req.get_header(key), regular) + self.assertEqual(dict(req.header_items())[key], regular) def test_http_body_file(self): # A regular file - chunked encoding is used unless Content Length is From 2ac3d2a93b4b5972c4174beda6f5ea16ca3475df Mon Sep 17 00:00:00 2001 From: Senthil Kumaran Date: Wed, 8 Jul 2026 19:12:09 -0700 Subject: [PATCH 6/6] Fix the test case for the key capitalized by add_header and add_unredirected_header. --- Lib/test/test_urllib2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index a4e4fd411f6d33..d2fd111f6d9de0 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -1005,8 +1005,9 @@ def test_http_header_priority(self): sent_headers = dict(http.req_headers) self.assertEqual(sent_headers[key], regular) - self.assertEqual(req.get_header(key), regular) - self.assertEqual(dict(req.header_items())[key], regular) + # key is capitalized by add_header() and add_unredirected_header() calls + self.assertEqual(req.get_header(key.capitalize()), regular) + self.assertEqual(dict(req.header_items())[key.capitalize()], regular) def test_http_body_file(self): # A regular file - chunked encoding is used unless Content Length is