From 0d02fbe1a3566985f2d17934988a9fe7b62135f4 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 26 May 2025 21:15:56 -0700 Subject: [PATCH 01/12] gh-135056: Add a --cors CLI argument to http.server Add a --cors command line argument to the stdlib http.server module, which will add an `Access-Control-Allow-Origin: *` header to all responses. As part of this implementation, add a `response_headers` argument to SimpleHTTPRequestHandler and HttpServer, which allows callers to add addition headers to the response. --- Doc/library/http.server.rst | 20 ++++++++- Lib/http/server.py | 42 +++++++++++++++---- Lib/test/test_httpservers.py | 32 ++++++++++++-- ...-06-02-22-23-38.gh-issue-135056.yz3dSs.rst | 2 + 4 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 063344e0284258..0f40214b771d2f 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -362,7 +362,7 @@ instantiation, of which this module provides three different variants: delays, it now always returns the IP address. -.. class:: SimpleHTTPRequestHandler(request, client_address, server, directory=None) +.. class:: SimpleHTTPRequestHandler(request, client_address, server, directory=None, response_headers=None) This class serves files from the directory *directory* and below, or the current directory if *directory* is not provided, directly @@ -374,6 +374,10 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.9 The *directory* parameter accepts a :term:`path-like object`. + .. versionchanged:: 3.15 + The *response_headers* parameter accepts an optional dictionary of + additional HTTP headers to add to each response. + A lot of the work, such as parsing the request, is done by the base class :class:`BaseHTTPRequestHandler`. This class implements the :func:`do_GET` and :func:`do_HEAD` functions. @@ -428,6 +432,9 @@ instantiation, of which this module provides three different variants: followed by a ``'Content-Length:'`` header with the file's size and a ``'Last-Modified:'`` header with the file's modification time. + The headers specified in the dictionary instance argument + ``response_headers`` are each individually sent in the response. + Then follows a blank line signifying the end of the headers, and then the contents of the file are output. @@ -437,6 +444,9 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.7 Support of the ``'If-Modified-Since'`` header. + .. versionchanged:: 3.15 + Support ``response_headers`` as an instance argument. + The :class:`SimpleHTTPRequestHandler` class can be used in the following manner in order to create a very basic webserver serving files relative to the current directory:: @@ -543,6 +553,14 @@ The following options are accepted: .. versionadded:: 3.14 +.. option:: --cors + + Adds an additional CORS (Cross-Origin Resource sharing) header to each response:: + + Access-Control-Allow-Origin: * + + .. versionadded:: 3.15 + .. _http.server-security: diff --git a/Lib/http/server.py b/Lib/http/server.py index ef10d185932633..152a4275d32ab5 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -117,6 +117,10 @@ class HTTPServer(socketserver.TCPServer): allow_reuse_address = True # Seems to make sense in testing environment allow_reuse_port = True + def __init__(self, *args, response_headers=None, **kwargs): + self.response_headers = response_headers if response_headers is not None else {} + super().__init__(*args, **kwargs) + def server_bind(self): """Override server_bind to store the server name.""" socketserver.TCPServer.server_bind(self) @@ -124,6 +128,11 @@ def server_bind(self): self.server_name = socket.getfqdn(host) self.server_port = port + def finish_request(self, request, client_address): + """Finish one request by instantiating RequestHandlerClass.""" + args = (request, client_address, self) + kwargs = dict(response_headers=self.response_headers) if self.response_headers else dict() + self.RequestHandlerClass(*args, **kwargs) class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): daemon_threads = True @@ -132,7 +141,7 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): class HTTPSServer(HTTPServer): def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, *, certfile, keyfile=None, - password=None, alpn_protocols=None): + password=None, alpn_protocols=None, response_headers=None): try: import ssl except ImportError: @@ -150,7 +159,8 @@ def __init__(self, server_address, RequestHandlerClass, super().__init__(server_address, RequestHandlerClass, - bind_and_activate) + bind_and_activate, + response_headers=response_headers) def server_activate(self): """Wrap the socket in SSLSocket.""" @@ -692,10 +702,11 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): '.xz': 'application/x-xz', } - def __init__(self, *args, directory=None, **kwargs): + def __init__(self, *args, directory=None, response_headers=None, **kwargs): if directory is None: directory = os.getcwd() self.directory = os.fspath(directory) + self.response_headers = response_headers or {} super().__init__(*args, **kwargs) def do_GET(self): @@ -736,6 +747,10 @@ def send_head(self): new_url = urllib.parse.urlunsplit(new_parts) self.send_header("Location", new_url) self.send_header("Content-Length", "0") + # User specified response_headers + if self.response_headers is not None: + for header, value in self.response_headers.items(): + self.send_header(header, value) self.end_headers() return None for index in self.index_pages: @@ -795,6 +810,9 @@ def send_head(self): self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + if self.response_headers is not None: + for header, value in self.response_headers.items(): + self.send_header(header, value) self.end_headers() return f except: @@ -970,7 +988,7 @@ def _get_best_family(*address): def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=ThreadingHTTPServer, protocol="HTTP/1.0", port=8000, bind=None, - tls_cert=None, tls_key=None, tls_password=None): + tls_cert=None, tls_key=None, tls_password=None, response_headers=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). @@ -981,9 +999,10 @@ def test(HandlerClass=BaseHTTPRequestHandler, if tls_cert: server = ServerClass(addr, HandlerClass, certfile=tls_cert, - keyfile=tls_key, password=tls_password) + keyfile=tls_key, password=tls_password, + response_headers=response_headers) else: - server = ServerClass(addr, HandlerClass) + server = ServerClass(addr, HandlerClass, response_headers=response_headers) with server as httpd: host, port = httpd.socket.getsockname()[:2] @@ -1024,6 +1043,8 @@ def _main(args=None): parser.add_argument('port', default=8000, type=int, nargs='?', help='bind to this port ' '(default: %(default)s)') + parser.add_argument('--cors', action='store_true', + help='Enable Access-Control-Allow-Origin: * header') args = parser.parse_args(args) if not args.tls_cert and args.tls_key: @@ -1051,8 +1072,11 @@ def server_bind(self): return super().server_bind() def finish_request(self, request, client_address): - self.RequestHandlerClass(request, client_address, self, - directory=args.directory) + handler_args = (request, client_address, self) + handler_kwargs = dict(directory=args.directory) + if self.response_headers: + handler_kwargs['response_headers'] = self.response_headers + self.RequestHandlerClass(*handler_args, **handler_kwargs) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass @@ -1060,6 +1084,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): pass ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer + response_headers = {'Access-Control-Allow-Origin': '*'} if args.cors else None test( HandlerClass=SimpleHTTPRequestHandler, @@ -1070,6 +1095,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): tls_cert=args.tls_cert, tls_key=args.tls_key, tls_password=tls_key_password, + response_headers=response_headers ) diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 2548a7c5f292f0..b1b711b0387db0 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -81,11 +81,12 @@ def test_https_server_raises_runtime_error(self): class TestServerThread(threading.Thread): - def __init__(self, test_object, request_handler, tls=None): + def __init__(self, test_object, request_handler, tls=None, server_kwargs=None): threading.Thread.__init__(self) self.request_handler = request_handler self.test_object = test_object self.tls = tls + self.server_kwargs = server_kwargs or {} def run(self): if self.tls: @@ -95,7 +96,8 @@ def run(self): request_handler=self.request_handler, ) else: - self.server = HTTPServer(('localhost', 0), self.request_handler) + self.server = HTTPServer(('localhost', 0), self.request_handler, + **self.server_kwargs) self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname() self.test_object.server_started.set() self.test_object = None @@ -113,12 +115,14 @@ class BaseTestCase(unittest.TestCase): # Optional tuple (certfile, keyfile, password) to use for HTTPS servers. tls = None + server_kwargs = None def setUp(self): self._threads = threading_helper.threading_setup() os.environ = os_helper.EnvironmentVarGuard() self.server_started = threading.Event() - self.thread = TestServerThread(self, self.request_handler, self.tls) + self.thread = TestServerThread(self, self.request_handler, self.tls, + self.server_kwargs) self.thread.start() self.server_started.wait() @@ -824,6 +828,16 @@ def test_path_without_leading_slash(self): self.tempdir_name + "/?hi=1") +class CorsHTTPServerTestCase(SimpleHTTPServerTestCase): + server_kwargs = dict( + response_headers = {'Access-Control-Allow-Origin': '*'} + ) + def test_cors(self): + response = self.request(self.base_url + '/test') + self.check_status_and_reason(response, HTTPStatus.OK) + self.assertEqual(response.getheader('Access-Control-Allow-Origin'), '*') + + class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self, directory=None): request = mock.Mock() @@ -1306,6 +1320,7 @@ class CommandLineTestCase(unittest.TestCase): 'tls_cert': None, 'tls_key': None, 'tls_password': None, + 'response_headers': None, } def setUp(self): @@ -1371,6 +1386,17 @@ def test_protocol_flag(self, mock_func): mock_func.assert_called_once_with(**call_args) mock_func.reset_mock() + @mock.patch('http.server.test') + def test_cors_flag(self, mock_func): + self.invoke_httpd('--cors') + call_args = self.args | dict( + response_headers={ + 'Access-Control-Allow-Origin': '*' + } + ) + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + @unittest.skipIf(ssl is None, "requires ssl") @mock.patch('http.server.test') def test_tls_cert_and_key_flags(self, mock_func): diff --git a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst new file mode 100644 index 00000000000000..d6fa033573e25b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst @@ -0,0 +1,2 @@ +Add a ``--cors`` cli option to ``python -m http.server``. Contributed by +Anton I. Sipos From 1838da7f9bf7ab91f7699120a9fc0d24c8501edd Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 2 Jun 2025 22:43:23 -0700 Subject: [PATCH 02/12] gh-issue-135056: Fix doc versionchanged and NEWS entries. --- Doc/library/http.server.rst | 6 +++--- .../Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 0f40214b771d2f..ab53f71c030c96 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -374,7 +374,7 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.9 The *directory* parameter accepts a :term:`path-like object`. - .. versionchanged:: 3.15 + .. versionchanged:: next The *response_headers* parameter accepts an optional dictionary of additional HTTP headers to add to each response. @@ -444,7 +444,7 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.7 Support of the ``'If-Modified-Since'`` header. - .. versionchanged:: 3.15 + .. versionchanged:: next Support ``response_headers`` as an instance argument. The :class:`SimpleHTTPRequestHandler` class can be used in the following @@ -559,7 +559,7 @@ The following options are accepted: Access-Control-Allow-Origin: * - .. versionadded:: 3.15 + .. versionadded:: next .. _http.server-security: diff --git a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst index d6fa033573e25b..929a4d08d19834 100644 --- a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst +++ b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst @@ -1,2 +1,2 @@ -Add a ``--cors`` cli option to ``python -m http.server``. Contributed by -Anton I. Sipos +Add a ``--cors`` cli option to :program:`python -m http.server`. Contributed by +Anton I. Sipos. From a3256fd21ba1d1c420e77a0007bea186a2f1f6a2 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 2 Jun 2025 23:09:49 -0700 Subject: [PATCH 03/12] gh-13056: Allow unspecified response_headers in HTTPServer. This fixes the breakage to HttpServer as used by wsgiref. --- Lib/http/server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index 152a4275d32ab5..1dfe735e688276 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -118,7 +118,7 @@ class HTTPServer(socketserver.TCPServer): allow_reuse_port = True def __init__(self, *args, response_headers=None, **kwargs): - self.response_headers = response_headers if response_headers is not None else {} + self.response_headers = response_headers super().__init__(*args, **kwargs) def server_bind(self): @@ -131,7 +131,10 @@ def server_bind(self): def finish_request(self, request, client_address): """Finish one request by instantiating RequestHandlerClass.""" args = (request, client_address, self) - kwargs = dict(response_headers=self.response_headers) if self.response_headers else dict() + kwargs = {} + response_headers = getattr(self, 'response_headers', None) + if response_headers: + kwargs['response_headers'] = self.response_headers self.RequestHandlerClass(*args, **kwargs) class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): From 77b5fff86aa309120c49c206e795765161753dab Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Thu, 19 Jun 2025 16:40:42 -0700 Subject: [PATCH 04/12] gh-135056: Simplifications and cleanups to http cors changes. --- Lib/http/server.py | 22 ++++++++++------------ Lib/socketserver.py | 2 +- Lib/test/test_httpservers.py | 8 +++++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index 1dfe735e688276..2e156919248e98 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -132,10 +132,9 @@ def finish_request(self, request, client_address): """Finish one request by instantiating RequestHandlerClass.""" args = (request, client_address, self) kwargs = {} - response_headers = getattr(self, 'response_headers', None) - if response_headers: + if hasattr(self, 'response_headers'): kwargs['response_headers'] = self.response_headers - self.RequestHandlerClass(*args, **kwargs) + self.RequestHandlerClass(request, client_address, self, **kwargs) class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): daemon_threads = True @@ -144,7 +143,7 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): class HTTPSServer(HTTPServer): def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, *, certfile, keyfile=None, - password=None, alpn_protocols=None, response_headers=None): + password=None, alpn_protocols=None, **http_server_kwargs): try: import ssl except ImportError: @@ -163,7 +162,7 @@ def __init__(self, server_address, RequestHandlerClass, super().__init__(server_address, RequestHandlerClass, bind_and_activate, - response_headers=response_headers) + **http_server_kwargs) def server_activate(self): """Wrap the socket in SSLSocket.""" @@ -709,7 +708,7 @@ def __init__(self, *args, directory=None, response_headers=None, **kwargs): if directory is None: directory = os.getcwd() self.directory = os.fspath(directory) - self.response_headers = response_headers or {} + self.response_headers = response_headers super().__init__(*args, **kwargs) def do_GET(self): @@ -991,7 +990,8 @@ def _get_best_family(*address): def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=ThreadingHTTPServer, protocol="HTTP/1.0", port=8000, bind=None, - tls_cert=None, tls_key=None, tls_password=None, response_headers=None): + tls_cert=None, tls_key=None, tls_password=None, + response_headers=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). @@ -1075,11 +1075,9 @@ def server_bind(self): return super().server_bind() def finish_request(self, request, client_address): - handler_args = (request, client_address, self) - handler_kwargs = dict(directory=args.directory) - if self.response_headers: - handler_kwargs['response_headers'] = self.response_headers - self.RequestHandlerClass(*handler_args, **handler_kwargs) + self.RequestHandlerClass(request, client_address, self, + directory=args.directory, + response_headers=self.response_headers) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass diff --git a/Lib/socketserver.py b/Lib/socketserver.py index 93b0a23be27f68..fdb312e7c38c1c 100644 --- a/Lib/socketserver.py +++ b/Lib/socketserver.py @@ -757,7 +757,7 @@ class BaseRequestHandler: """ - def __init__(self, request, client_address, server): + def __init__(self, request, client_address, server, **kwargs): self.request = request self.client_address = client_address self.server = server diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index b1b711b0387db0..60b75d5fa21faf 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -94,6 +94,7 @@ def run(self): self.server = create_https_server( certfile, keyfile, password, request_handler=self.request_handler, + **self.server_kwargs ) else: self.server = HTTPServer(('localhost', 0), self.request_handler, @@ -829,9 +830,10 @@ def test_path_without_leading_slash(self): class CorsHTTPServerTestCase(SimpleHTTPServerTestCase): - server_kwargs = dict( - response_headers = {'Access-Control-Allow-Origin': '*'} - ) + server_kwargs = { + 'response_headers': {'Access-Control-Allow-Origin': '*'} + } + def test_cors(self): response = self.request(self.base_url + '/test') self.check_status_and_reason(response, HTTPStatus.OK) From 5f89c97c15f7cd240cec2ba4fc2eec55d0d19f54 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Thu, 19 Jun 2025 18:43:38 -0700 Subject: [PATCH 05/12] gh-135056: Add a --header argument to http.server cli. --- Lib/http/server.py | 14 ++++++++++++-- Lib/test/test_httpservers.py | 12 ++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index 2e156919248e98..ebc82bfd8e99d3 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1048,6 +1048,11 @@ def _main(args=None): '(default: %(default)s)') parser.add_argument('--cors', action='store_true', help='Enable Access-Control-Allow-Origin: * header') + parser.add_argument('-H', '--header', nargs=2, action='append', + # metavar='HEADER VALUE', + metavar=('HEADER', 'VALUE'), + help='Add a custom response header ' + '(can be used multiple times)') args = parser.parse_args(args) if not args.tls_cert and args.tls_key: @@ -1085,7 +1090,12 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): pass ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer - response_headers = {'Access-Control-Allow-Origin': '*'} if args.cors else None + response_headers = {} + if args.cors: + response_headers['Access-Control-Allow-Origin'] = '*' + for header, value in args.header or []: + response_headers[header] = value + test( HandlerClass=SimpleHTTPRequestHandler, @@ -1096,7 +1106,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): tls_cert=args.tls_cert, tls_key=args.tls_key, tls_password=tls_key_password, - response_headers=response_headers + response_headers=response_headers or None ) diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 60b75d5fa21faf..3bfa14a6112400 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -1399,6 +1399,18 @@ def test_cors_flag(self, mock_func): mock_func.assert_called_once_with(**call_args) mock_func.reset_mock() + @mock.patch('http.server.test') + def test_header_flag(self, mock_func): + self.invoke_httpd('--header', 'h1', 'v1', '-H', 'h2', 'v2') + call_args = self.args | dict( + response_headers={ + 'h1': 'v1', + 'h2': 'v2' + } + ) + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + @unittest.skipIf(ssl is None, "requires ssl") @mock.patch('http.server.test') def test_tls_cert_and_key_flags(self, mock_func): From a3243fe679166211ab1db3ea57cadeb50a3fbbdb Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Sun, 6 Jul 2025 10:48:10 -0700 Subject: [PATCH 06/12] gh-135056: Remove --cors opt from http.server in favor of --header --- Doc/library/http.server.rst | 9 -------- Lib/http/server.py | 4 ---- Lib/test/test_httpservers.py | 22 ------------------- ...-06-02-22-23-38.gh-issue-135056.yz3dSs.rst | 2 +- 4 files changed, 1 insertion(+), 36 deletions(-) diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index ab53f71c030c96..36033be34f2042 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -553,15 +553,6 @@ The following options are accepted: .. versionadded:: 3.14 -.. option:: --cors - - Adds an additional CORS (Cross-Origin Resource sharing) header to each response:: - - Access-Control-Allow-Origin: * - - .. versionadded:: next - - .. _http.server-security: Security considerations diff --git a/Lib/http/server.py b/Lib/http/server.py index ebc82bfd8e99d3..b95d6094725112 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1046,8 +1046,6 @@ def _main(args=None): parser.add_argument('port', default=8000, type=int, nargs='?', help='bind to this port ' '(default: %(default)s)') - parser.add_argument('--cors', action='store_true', - help='Enable Access-Control-Allow-Origin: * header') parser.add_argument('-H', '--header', nargs=2, action='append', # metavar='HEADER VALUE', metavar=('HEADER', 'VALUE'), @@ -1091,8 +1089,6 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer response_headers = {} - if args.cors: - response_headers['Access-Control-Allow-Origin'] = '*' for header, value in args.header or []: response_headers[header] = value diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 3bfa14a6112400..e3aa8bb9e53ef9 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -829,17 +829,6 @@ def test_path_without_leading_slash(self): self.tempdir_name + "/?hi=1") -class CorsHTTPServerTestCase(SimpleHTTPServerTestCase): - server_kwargs = { - 'response_headers': {'Access-Control-Allow-Origin': '*'} - } - - def test_cors(self): - response = self.request(self.base_url + '/test') - self.check_status_and_reason(response, HTTPStatus.OK) - self.assertEqual(response.getheader('Access-Control-Allow-Origin'), '*') - - class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self, directory=None): request = mock.Mock() @@ -1388,17 +1377,6 @@ def test_protocol_flag(self, mock_func): mock_func.assert_called_once_with(**call_args) mock_func.reset_mock() - @mock.patch('http.server.test') - def test_cors_flag(self, mock_func): - self.invoke_httpd('--cors') - call_args = self.args | dict( - response_headers={ - 'Access-Control-Allow-Origin': '*' - } - ) - mock_func.assert_called_once_with(**call_args) - mock_func.reset_mock() - @mock.patch('http.server.test') def test_header_flag(self, mock_func): self.invoke_httpd('--header', 'h1', 'v1', '-H', 'h2', 'v2') diff --git a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst index 929a4d08d19834..8afb6307d49ef9 100644 --- a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst +++ b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst @@ -1,2 +1,2 @@ -Add a ``--cors`` cli option to :program:`python -m http.server`. Contributed by +Add a ``--header`` cli option to :program:`python -m http.server`. Contributed by Anton I. Sipos. From b1026d2a1faeec3c323467f5d7ca354f0712604a Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Sun, 6 Jul 2025 18:27:16 -0700 Subject: [PATCH 07/12] gh-135056: Use response_headers only in SimpleHTTPRequestHandler --- Doc/library/http.server.rst | 5 ++-- Lib/http/server.py | 51 ++++++++++++++---------------------- Lib/socketserver.py | 2 +- Lib/test/test_httpservers.py | 47 ++++++++++++++++++++++----------- 4 files changed, 54 insertions(+), 51 deletions(-) diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 36033be34f2042..8382b72856af6f 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -375,8 +375,9 @@ instantiation, of which this module provides three different variants: The *directory* parameter accepts a :term:`path-like object`. .. versionchanged:: next - The *response_headers* parameter accepts an optional dictionary of - additional HTTP headers to add to each response. + Added *response_headers*, which accepts an optional dictionary of + additional HTTP headers to add to each successful HTTP status 200 + response. All other status code responses will not include these headers. A lot of the work, such as parsing the request, is done by the base class :class:`BaseHTTPRequestHandler`. This class implements the :func:`do_GET` diff --git a/Lib/http/server.py b/Lib/http/server.py index b95d6094725112..ffaf176c395974 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -117,10 +117,6 @@ class HTTPServer(socketserver.TCPServer): allow_reuse_address = True # Seems to make sense in testing environment allow_reuse_port = True - def __init__(self, *args, response_headers=None, **kwargs): - self.response_headers = response_headers - super().__init__(*args, **kwargs) - def server_bind(self): """Override server_bind to store the server name.""" socketserver.TCPServer.server_bind(self) @@ -128,13 +124,6 @@ def server_bind(self): self.server_name = socket.getfqdn(host) self.server_port = port - def finish_request(self, request, client_address): - """Finish one request by instantiating RequestHandlerClass.""" - args = (request, client_address, self) - kwargs = {} - if hasattr(self, 'response_headers'): - kwargs['response_headers'] = self.response_headers - self.RequestHandlerClass(request, client_address, self, **kwargs) class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): daemon_threads = True @@ -143,7 +132,7 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): class HTTPSServer(HTTPServer): def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, *, certfile, keyfile=None, - password=None, alpn_protocols=None, **http_server_kwargs): + password=None, alpn_protocols=None): try: import ssl except ImportError: @@ -161,8 +150,7 @@ def __init__(self, server_address, RequestHandlerClass, super().__init__(server_address, RequestHandlerClass, - bind_and_activate, - **http_server_kwargs) + bind_and_activate) def server_activate(self): """Wrap the socket in SSLSocket.""" @@ -726,6 +714,13 @@ def do_HEAD(self): if f: f.close() + def send_custom_response_headers(self): + """Send the headers stored in self.response_headers""" + # User specified response_headers + if self.response_headers is not None: + for header, value in self.response_headers.items(): + self.send_header(header, value) + def send_head(self): """Common code for GET and HEAD commands. @@ -749,10 +744,6 @@ def send_head(self): new_url = urllib.parse.urlunsplit(new_parts) self.send_header("Location", new_url) self.send_header("Content-Length", "0") - # User specified response_headers - if self.response_headers is not None: - for header, value in self.response_headers.items(): - self.send_header(header, value) self.end_headers() return None for index in self.index_pages: @@ -812,9 +803,7 @@ def send_head(self): self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) - if self.response_headers is not None: - for header, value in self.response_headers.items(): - self.send_header(header, value) + self.send_custom_response_headers() self.end_headers() return f except: @@ -879,6 +868,7 @@ def list_directory(self, path): self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html; charset=%s" % enc) self.send_header("Content-Length", str(len(encoded))) + self.send_custom_response_headers() self.end_headers() return f @@ -990,8 +980,7 @@ def _get_best_family(*address): def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=ThreadingHTTPServer, protocol="HTTP/1.0", port=8000, bind=None, - tls_cert=None, tls_key=None, tls_password=None, - response_headers=None): + tls_cert=None, tls_key=None, tls_password=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). @@ -1002,10 +991,9 @@ def test(HandlerClass=BaseHTTPRequestHandler, if tls_cert: server = ServerClass(addr, HandlerClass, certfile=tls_cert, - keyfile=tls_key, password=tls_password, - response_headers=response_headers) + keyfile=tls_key, password=tls_password) else: - server = ServerClass(addr, HandlerClass, response_headers=response_headers) + server = ServerClass(addr, HandlerClass) with server as httpd: host, port = httpd.socket.getsockname()[:2] @@ -1067,6 +1055,10 @@ def _main(args=None): except OSError as e: parser.error(f"Failed to read TLS password file: {e}") + response_headers = {} + for header, value in args.header or []: + response_headers[header] = value + # ensure dual-stack is not disabled; ref #38907 class DualStackServerMixin: @@ -1080,7 +1072,7 @@ def server_bind(self): def finish_request(self, request, client_address): self.RequestHandlerClass(request, client_address, self, directory=args.directory, - response_headers=self.response_headers) + response_headers=response_headers) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass @@ -1088,10 +1080,6 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): pass ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer - response_headers = {} - for header, value in args.header or []: - response_headers[header] = value - test( HandlerClass=SimpleHTTPRequestHandler, @@ -1102,7 +1090,6 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): tls_cert=args.tls_cert, tls_key=args.tls_key, tls_password=tls_key_password, - response_headers=response_headers or None ) diff --git a/Lib/socketserver.py b/Lib/socketserver.py index fdb312e7c38c1c..93b0a23be27f68 100644 --- a/Lib/socketserver.py +++ b/Lib/socketserver.py @@ -757,7 +757,7 @@ class BaseRequestHandler: """ - def __init__(self, request, client_address, server, **kwargs): + def __init__(self, request, client_address, server): self.request = request self.client_address = client_address self.server = server diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index e3aa8bb9e53ef9..b55d4025a668c1 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -81,12 +81,11 @@ def test_https_server_raises_runtime_error(self): class TestServerThread(threading.Thread): - def __init__(self, test_object, request_handler, tls=None, server_kwargs=None): + def __init__(self, test_object, request_handler, tls=None): threading.Thread.__init__(self) self.request_handler = request_handler self.test_object = test_object self.tls = tls - self.server_kwargs = server_kwargs or {} def run(self): if self.tls: @@ -94,11 +93,9 @@ def run(self): self.server = create_https_server( certfile, keyfile, password, request_handler=self.request_handler, - **self.server_kwargs ) else: - self.server = HTTPServer(('localhost', 0), self.request_handler, - **self.server_kwargs) + self.server = HTTPServer(('localhost', 0), self.request_handler) self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname() self.test_object.server_started.set() self.test_object = None @@ -116,14 +113,12 @@ class BaseTestCase(unittest.TestCase): # Optional tuple (certfile, keyfile, password) to use for HTTPS servers. tls = None - server_kwargs = None def setUp(self): self._threads = threading_helper.threading_setup() os.environ = os_helper.EnvironmentVarGuard() self.server_started = threading.Event() - self.thread = TestServerThread(self, self.request_handler, self.tls, - self.server_kwargs) + self.thread = TestServerThread(self, self.request_handler, self.tls) self.thread.start() self.server_started.wait() @@ -470,8 +465,14 @@ def test_err(self): self.assertEndsWith(lines[1], '"ERROR / HTTP/1.1" 404 -') +class CustomHeaderSimpleHTTPRequestHandler(SimpleHTTPRequestHandler): + custom_headers = None + def __init__(self, *args, directory=None, response_headers=None, **kwargs): + super().__init__(*args, response_headers=self.custom_headers, **kwargs) + + class SimpleHTTPServerTestCase(BaseTestCase): - class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler): + class request_handler(NoLogRequestHandler, CustomHeaderSimpleHTTPRequestHandler): pass def setUp(self): @@ -828,6 +829,26 @@ def test_path_without_leading_slash(self): self.assertEqual(response.getheader("Location"), self.tempdir_name + "/?hi=1") + def test_custom_headers_list_dir(self): + with mock.patch.object(self.request_handler, 'custom_headers', new={ + 'X-Test1': 'test1', + 'X-Test2': 'test2', + }): + response = self.request(self.base_url + '/') + self.assertEqual(response.getheader("X-Test1"), 'test1') + self.assertEqual(response.getheader("X-Test2"), 'test2') + + def test_custom_headers_get_file(self): + with mock.patch.object(self.request_handler, 'custom_headers', new={ + 'X-Test1': 'test1', + 'X-Test2': 'test2', + }): + data = b"Dummy index file\r\n" + with open(os.path.join(self.tempdir_name, 'index.html'), 'wb') as f: + f.write(data) + response = self.request(self.base_url + '/') + self.assertEqual(response.getheader("X-Test1"), 'test1') + self.assertEqual(response.getheader("X-Test2"), 'test2') class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self, directory=None): @@ -1311,7 +1332,6 @@ class CommandLineTestCase(unittest.TestCase): 'tls_cert': None, 'tls_key': None, 'tls_password': None, - 'response_headers': None, } def setUp(self): @@ -1379,13 +1399,8 @@ def test_protocol_flag(self, mock_func): @mock.patch('http.server.test') def test_header_flag(self, mock_func): + call_args = self.args self.invoke_httpd('--header', 'h1', 'v1', '-H', 'h2', 'v2') - call_args = self.args | dict( - response_headers={ - 'h1': 'v1', - 'h2': 'v2' - } - ) mock_func.assert_called_once_with(**call_args) mock_func.reset_mock() From 6f88c13a4338dd33827e7cd929e7744ef63d3aca Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Wed, 9 Jul 2025 17:18:32 -0700 Subject: [PATCH 08/12] gh-135056: Add test for http.server cli --header argument --- Lib/http/server.py | 3 ++- Lib/test/test_httpservers.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index ffaf176c395974..bc1984b0262b51 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1008,6 +1008,7 @@ def test(HandlerClass=BaseHTTPRequestHandler, except KeyboardInterrupt: print("\nKeyboard interrupt received, exiting.") sys.exit(0) + return server def _main(args=None): @@ -1081,7 +1082,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer - test( + return test( HandlerClass=SimpleHTTPRequestHandler, ServerClass=ServerClass, port=args.port, diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index b55d4025a668c1..bdc0c0ff8b50c2 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -1487,6 +1487,34 @@ def test_unknown_flag(self, _): self.assertEqual(stdout.getvalue(), '') self.assertIn('error', stderr.getvalue()) + def test_response_headers_arg(self): + # with mock.patch.object( + # SimpleHTTPRequestHandler, '__init__' + # ) as mock_handler, \ + # mock.patch.object( + # HTTPServer, 'serve_forever' + # ) as mock_serve_forever: + with mock.patch.object( + HTTPServer, 'serve_forever' + ) as mock_serve_forever: + httpd = server._main( + ['-H', 'X-Test1', 'Test1', '-H', 'X-Test2', 'Test2', '8080'] + ) + request_handler_class = httpd.RequestHandlerClass + with mock.patch.object( + request_handler_class, '__init__' + ) as mock_handler_init: + mock_handler_init.return_value = None + # finish_request instantiates a request handler class, + # ensure response_headers are passed to it + httpd.finish_request(mock.Mock(), '127.0.0.1') + mock_handler_init.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, directory=mock.ANY, + response_headers={ + 'X-Test1': 'Test1', 'X-Test2': 'Test2' + } + ) + class CommandLineRunTimeTestCase(unittest.TestCase): served_data = os.urandom(32) From 7a793f2d4be5394645b3da17c16a4468f640a875 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Wed, 9 Jul 2025 22:57:23 -0700 Subject: [PATCH 09/12] gh-135056: Support multiple headers of the same name. --- Doc/library/http.server.rst | 8 ++++---- Lib/http/server.py | 6 +++--- Lib/test/test_httpservers.py | 30 ++++++++++++++++-------------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 8382b72856af6f..62df260e8f5eb2 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -375,8 +375,8 @@ instantiation, of which this module provides three different variants: The *directory* parameter accepts a :term:`path-like object`. .. versionchanged:: next - Added *response_headers*, which accepts an optional dictionary of - additional HTTP headers to add to each successful HTTP status 200 + Added *response_headers*, which accepts an optional iterable of + name/value pairs of HTTP headers to add to each successful HTTP status 200 response. All other status code responses will not include these headers. A lot of the work, such as parsing the request, is done by the base class @@ -433,8 +433,8 @@ instantiation, of which this module provides three different variants: followed by a ``'Content-Length:'`` header with the file's size and a ``'Last-Modified:'`` header with the file's modification time. - The headers specified in the dictionary instance argument - ``response_headers`` are each individually sent in the response. + The instance attribute ``response_headers`` is used as an iterable of + name/value pairs to set user specified custom response headers. Then follows a blank line signifying the end of the headers, and then the contents of the file are output. diff --git a/Lib/http/server.py b/Lib/http/server.py index bc1984b0262b51..b0787f59362c15 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -718,7 +718,7 @@ def send_custom_response_headers(self): """Send the headers stored in self.response_headers""" # User specified response_headers if self.response_headers is not None: - for header, value in self.response_headers.items(): + for header, value in self.response_headers: self.send_header(header, value) def send_head(self): @@ -1056,9 +1056,9 @@ def _main(args=None): except OSError as e: parser.error(f"Failed to read TLS password file: {e}") - response_headers = {} + response_headers = [] for header, value in args.header or []: - response_headers[header] = value + response_headers.append((header, value)) # ensure dual-stack is not disabled; ref #38907 class DualStackServerMixin: diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index bdc0c0ff8b50c2..d65aad6099d68b 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -830,25 +830,27 @@ def test_path_without_leading_slash(self): self.tempdir_name + "/?hi=1") def test_custom_headers_list_dir(self): - with mock.patch.object(self.request_handler, 'custom_headers', new={ - 'X-Test1': 'test1', - 'X-Test2': 'test2', - }): + with mock.patch.object(self.request_handler, 'custom_headers', new=[ + ('X-Test1', 'test1'), + ('X-Test2', 'test2'), + ]): response = self.request(self.base_url + '/') self.assertEqual(response.getheader("X-Test1"), 'test1') self.assertEqual(response.getheader("X-Test2"), 'test2') def test_custom_headers_get_file(self): - with mock.patch.object(self.request_handler, 'custom_headers', new={ - 'X-Test1': 'test1', - 'X-Test2': 'test2', - }): + with mock.patch.object(self.request_handler, 'custom_headers', new=[ + ('Set-Cookie', 'test1=value1'), + ('Set-Cookie', 'test2=value2'), + ('X-Test1', 'value3'), + ]): data = b"Dummy index file\r\n" with open(os.path.join(self.tempdir_name, 'index.html'), 'wb') as f: f.write(data) response = self.request(self.base_url + '/') - self.assertEqual(response.getheader("X-Test1"), 'test1') - self.assertEqual(response.getheader("X-Test2"), 'test2') + self.assertEqual(response.getheader("Set-Cookie"), + 'test1=value1, test2=value2') + self.assertEqual(response.getheader("X-Test1"), 'value3') class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self, directory=None): @@ -1498,7 +1500,7 @@ def test_response_headers_arg(self): HTTPServer, 'serve_forever' ) as mock_serve_forever: httpd = server._main( - ['-H', 'X-Test1', 'Test1', '-H', 'X-Test2', 'Test2', '8080'] + ['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2', '8080'] ) request_handler_class = httpd.RequestHandlerClass with mock.patch.object( @@ -1510,9 +1512,9 @@ def test_response_headers_arg(self): httpd.finish_request(mock.Mock(), '127.0.0.1') mock_handler_init.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY, directory=mock.ANY, - response_headers={ - 'X-Test1': 'Test1', 'X-Test2': 'Test2' - } + response_headers=[ + ('Set-Cookie', 'k=v'), ('Set-Cookie', 'k2=v2') + ] ) From 9450b868516e80d775449b7f496795e127ef3e42 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 14 Jul 2025 20:50:56 -0700 Subject: [PATCH 10/12] gh-135056: Remove some commented out and unused code. --- Lib/http/server.py | 1 - Lib/test/test_httpservers.py | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index b0787f59362c15..4aaa8c1fd4646d 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1036,7 +1036,6 @@ def _main(args=None): help='bind to this port ' '(default: %(default)s)') parser.add_argument('-H', '--header', nargs=2, action='append', - # metavar='HEADER VALUE', metavar=('HEADER', 'VALUE'), help='Add a custom response header ' '(can be used multiple times)') diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index d65aad6099d68b..dfc2a2a02dee07 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -1490,15 +1490,7 @@ def test_unknown_flag(self, _): self.assertIn('error', stderr.getvalue()) def test_response_headers_arg(self): - # with mock.patch.object( - # SimpleHTTPRequestHandler, '__init__' - # ) as mock_handler, \ - # mock.patch.object( - # HTTPServer, 'serve_forever' - # ) as mock_serve_forever: - with mock.patch.object( - HTTPServer, 'serve_forever' - ) as mock_serve_forever: + with mock.patch.object(HTTPServer, 'serve_forever'): httpd = server._main( ['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2', '8080'] ) From 5a30d914ad194bfac400bbaa5e699bd77f9785ed Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 14 Jul 2025 20:55:23 -0700 Subject: [PATCH 11/12] gh-135056: Capitalize CLI acronym in the docs. --- .../next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst index 8afb6307d49ef9..0565260dc443ec 100644 --- a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst +++ b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst @@ -1,2 +1,2 @@ -Add a ``--header`` cli option to :program:`python -m http.server`. Contributed by +Add a ``--header`` CLI option to :program:`python -m http.server`. Contributed by Anton I. Sipos. From d317cc2d333f7b8b340e5133378cdc64b5b377e7 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 14 Jul 2025 20:59:57 -0700 Subject: [PATCH 12/12] gh-135056: Simplify args.header processing. --- Lib/http/server.py | 6 +----- Lib/test/test_httpservers.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index 4aaa8c1fd4646d..ac22c6a76cdba3 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1055,10 +1055,6 @@ def _main(args=None): except OSError as e: parser.error(f"Failed to read TLS password file: {e}") - response_headers = [] - for header, value in args.header or []: - response_headers.append((header, value)) - # ensure dual-stack is not disabled; ref #38907 class DualStackServerMixin: @@ -1072,7 +1068,7 @@ def server_bind(self): def finish_request(self, request, client_address): self.RequestHandlerClass(request, client_address, self, directory=args.directory, - response_headers=response_headers) + response_headers=args.header) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index dfc2a2a02dee07..77b9ef1aa4d870 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -1505,7 +1505,7 @@ def test_response_headers_arg(self): mock_handler_init.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY, directory=mock.ANY, response_headers=[ - ('Set-Cookie', 'k=v'), ('Set-Cookie', 'k2=v2') + ['Set-Cookie', 'k=v'], ['Set-Cookie', 'k2=v2'] ] ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy