Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0d02fbe
gh-135056: Add a --cors CLI argument to http.server
aisipos May 27, 2025
1838da7
gh-issue-135056: Fix doc versionchanged and NEWS entries.
aisipos Jun 3, 2025
a3256fd
gh-13056: Allow unspecified response_headers in HTTPServer.
aisipos Jun 3, 2025
77b5fff
gh-135056: Simplifications and cleanups to http cors changes.
aisipos Jun 19, 2025
5f89c97
gh-135056: Add a --header argument to http.server cli.
aisipos Jun 20, 2025
a3243fe
gh-135056: Remove --cors opt from http.server in favor of --header
aisipos Jul 6, 2025
b1026d2
gh-135056: Use response_headers only in SimpleHTTPRequestHandler
aisipos Jul 7, 2025
6f88c13
gh-135056: Add test for http.server cli --header argument
aisipos Jul 10, 2025
7a793f2
gh-135056: Support multiple headers of the same name.
aisipos Jul 10, 2025
9450b86
gh-135056: Remove some commented out and unused code.
aisipos Jul 15, 2025
5a30d91
gh-135056: Capitalize CLI acronym in the docs.
aisipos Jul 15, 2025
d317cc2
gh-135056: Simplify args.header processing.
aisipos Jul 15, 2025
5f1fb94
gh-135056: Factor out a _make_server function from test function.
aisipos Aug 12, 2025
c376a71
gh-135056: Document directory and custom_headers as keyword args.
aisipos Aug 12, 2025
89a89f0
gh-135056: Add whatsnew entries to 3.15.rst for custom headers.
aisipos Aug 12, 2025
9653710
gh-135056: Revert document directory + custom_headers as kwargs
aisipos Oct 6, 2025
f3ae904
gh-135056: Document response_headers as an instance_attribute.
aisipos Oct 6, 2025
44efbed
gh-135056: Revert blank line removal in http.server.rst
aisipos Oct 6, 2025
d47c5a7
gh-135056: Remove incorrect = sign from whatsnew argument entry.
aisipos Oct 6, 2025
8d1286a
gh-135056: Document -H, --header cli params in http.server.rst
aisipos Oct 6, 2025
db9de68
gh-135056: Rename custom headers to extra_response_headers.
aisipos Oct 6, 2025
e149708
gh-135056: Fix alignment of parameters to _make_server.
aisipos Oct 6, 2025
c16f4c9
gh-135056: Remove extraneous newline in docstring for test() method
aisipos Oct 6, 2025
777b5b6
gh-135056: Simplify kwargs to CustomHeaderSimpleHTTPRequestHandler
aisipos Oct 6, 2025
eac5c6a
gh-135056: Note both -H and --header in NEWS entries.
aisipos Oct 6, 2025
c9c8083
gh-135056: Put kwarg on its own line in mock assertion.
aisipos Oct 7, 2025
c2d6bb3
gh-135056: Add tests for bad usage of header arg.
aisipos Oct 7, 2025
3377cf7
gh-135056: Document SimpleHTTPRequestHandler params as keyword only.
aisipos Oct 7, 2025
06a9977
gh-135056: Fix missing renames of extra_response_headers.
aisipos Oct 7, 2025
fae21f9
gh-135056: Merge branch 'main' of github.com:python/cpython
aisipos Oct 9, 2025
be78515
gh-135056: Clarify extra_response_headers is a paramter
aisipos Oct 9, 2025
53965ff
gh-135056: Prefer user-defined to user specified in http.server docs
aisipos Oct 9, 2025
c280ed8
gh-135056: Clarify wording about non-200 response header logic
aisipos Oct 9, 2025
64122df
gh-135056: Keep TLS arguments to _make_server on the same line.
aisipos Oct 9, 2025
f0d1bac
gh-135056: Prefer "specified" to "use" in cli --help text.
aisipos Oct 9, 2025
e99780e
gh-135056: Change new arg to mock.patch.object to positional instead
aisipos Oct 9, 2025
2e829bb
gh-135056: Correct proper 2 line spacing after test class.
aisipos Oct 9, 2025
8baa875
gh-135056: Assert response.status is 200 in new tests.
aisipos Oct 9, 2025
7856d27
gh-135056: Add test_extra_response_headers_missing_on_404
aisipos Oct 9, 2025
303ab5b
gh-135056: Augment header test case to check colons and spaces
aisipos Oct 10, 2025
ed0b0b3
gh-135056: Fix ReST fully qualified ref to SimpleHTTPRequestHandler
aisipos Oct 10, 2025
79c577b
gh-135056: Fix socket closing in test_extra_response_headers_arg
aisipos Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Doc/library/http.server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document that directory and response_headers are keyword arguments actually.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c376a71


This class serves files from the directory *directory* and below,
or the current directory if *directory* is not provided, directly
Expand All @@ -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:: next
The *response_headers* parameter accepts an optional dictionary of
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In previous versions, this was not a valid parameter at all.

Suggested change
The *response_headers* parameter accepts an optional dictionary of
Added *response_headers*, which accepts an optional dictionary of

Also, did you consider accepting a list or iterable of (name, value) pairs instead, like returned by http.client.HTTPResponse.getheaders? That would be better for sending multiple Set-Cookie fields.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, sending multiple headers of the same name would indeed be necessary. I updated to use an iterable of name value pairs instead in 7a793f2

additional HTTP headers to add to each response.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth clarifying how these fields interact with other fields such as Server specified under BaseHTTPRequestHandler.send_response, and Last-Modified under do_GET.

Also clarify which responses the fields are included in, assuming it wasn’t your intention to include them for 404 Not Found, 304 Not Modified, lower-level errors, etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the latest commits, I've noted that the custom headers are only sent in success cases. What do you mean by interaction though? The custom headers currently get sent after Last-Modified, should I mention the placement of the custom headers and that they appear after Last-Modified?


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.
Expand Down Expand Up @@ -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.

Expand All @@ -437,6 +444,9 @@ instantiation, of which this module provides three different variants:
.. versionchanged:: 3.7
Support of the ``'If-Modified-Since'`` header.

.. versionchanged:: next
Support ``response_headers`` as an instance argument.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn’t this redundant with the entry already under the constructor heading?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps - it seems the constructor documentation is used to make a brief mention of each argument and when it was added, with more detail being filled in later. My latest commits make several changes requested elsewhere for other reasons, but if the current version is still too redundant in multiple places I can make some more edits.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should add this information. There is no notion of an "instance argument": it should rather be an instance attribute, and this should be documented through a .. attribute::, below .. attribute:: extensions_map


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::
Expand Down Expand Up @@ -543,6 +553,14 @@ The following options are accepted:

.. versionadded:: 3.14

.. option:: --cors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Hugo said, since we're anyway exposing response-headers, I think we should also expose it from the CLI. It could be useful for users in general (e.g., --add-header NAME VALUE with the -H alias).


Adds an additional CORS (Cross-Origin Resource sharing) header to each response::

Access-Control-Allow-Origin: *

.. versionadded:: next


.. _http.server-security:

Expand Down
45 changes: 37 additions & 8 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,25 @@ 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)
host, port = self.server_address[:2]
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 = {}
response_headers = getattr(self, 'response_headers', None)
if response_headers:
kwargs['response_headers'] = self.response_headers
self.RequestHandlerClass(*args, **kwargs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
args = (request, client_address, self)
kwargs = {}
response_headers = getattr(self, 'response_headers', None)
if response_headers:
kwargs['response_headers'] = self.response_headers
self.RequestHandlerClass(*args, **kwargs)
kwargs = {}
if hasattr(self, 'response_headers'):
kwargs['response_headers'] = self.response_headers
self.RequestHandlerClass(request, client_address, self, **kwargs)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@picnixz I made this requested change in 77b5fff. Note though that now HTTPServer will pass response_headers to the RequestHandler class even if response_headers is None or {}. Most RequestHandler implementation constructor implementations don't take this argument, so in order for this to work I added **kwargs as an argument to BaseRequestHandler.__init__. My earlier implementation was trying to prevent this, to keep any changes to only http/server.py and not need to touch anything in socketserver.py. Perhaps the **kwargs addition is ok, or I'm open to other solutions if we can think of better ones.


class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
daemon_threads = True
Expand All @@ -132,7 +144,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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
password=None, alpn_protocols=None, response_headers=None):
password=None, alpn_protocols=None, **http_server_kwargs):

And pass http_server_kwargs to super()

try:
import ssl
except ImportError:
Expand All @@ -150,7 +162,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."""
Expand Down Expand Up @@ -692,10 +705,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 {}
Copy link
Member

@picnixz picnixz Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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 {}
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

You're already checking for is not None later

super().__init__(*args, **kwargs)

def do_GET(self):
Expand Down Expand Up @@ -736,6 +750,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make it a private method, say self._add_custom_response_headers or something like that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is moving this to an extended send_response override an option? That way you would include the fields for all responses.

self.end_headers()
return None
for index in self.index_pages:
Expand Down Expand Up @@ -795,6 +813,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:
Expand Down Expand Up @@ -970,7 +991,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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tls_cert=None, tls_key=None, tls_password=None, response_headers=None):
tls_cert=None, tls_key=None, tls_password=None,
response_headers=None):

Let's group the parameters per purpose

"""Test the HTTP request handler class.

This runs an HTTP server on port 8000 (or the port argument).
Expand All @@ -981,9 +1002,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]
Expand Down Expand Up @@ -1024,6 +1046,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:
Expand Down Expand Up @@ -1051,15 +1075,19 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
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,
Expand All @@ -1070,6 +1098,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer):
tls_cert=args.tls_cert,
tls_key=args.tls_key,
tls_password=tls_key_password,
response_headers=response_headers
)


Expand Down
32 changes: 29 additions & 3 deletions Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You must also modify create_https_server appropriately

**self.server_kwargs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to only be testing the undocumented or internal HTTPServer parameter. It would be good to test the new documented SimpleHTTPRequestHandler parameter instead or as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed server_kwargs in the latest updates, and updated the tests. The only external change now is response_headers as an instance arg to SimpleHTTPRequestHandler

self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname()
self.test_object.server_started.set()
self.test_object = None
Expand All @@ -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()

Expand Down Expand Up @@ -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': '*'}
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
server_kwargs = dict(
response_headers = {'Access-Control-Allow-Origin': '*'}
)
server_kwargs = {
'response_headers': {'Access-Control-Allow-Origin': '*'}
}

def test_cors(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_cors(self):
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()
Expand Down Expand Up @@ -1306,6 +1320,7 @@ class CommandLineTestCase(unittest.TestCase):
'tls_cert': None,
'tls_key': None,
'tls_password': None,
'response_headers': None,
}

def setUp(self):
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to check this as --header is only used by the handler and not the server test server itself. Ideally, we should also extract the logic of making the parser to check that the parsed arguments are of the expected form (namely a sequence of pairs).

You can however check bad usages of --header (e.g., --header h1 should raise, and --header h1 h2 h3 as well).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some new tests to verify exceptions with incorrect header CLI usage in c2d6bb3.

I agree that test_header_flag doesn't quite do what we want. I only put it there to ensure all the argument parsing and logic in _main() doesn't raise in the presence of the new arguments before calling test(). I agree that refactoring out the parser logic would allow us to test this properly. However, I've been trying to keep the changes in this PR relatively small. If you feel though that a refactor is the better choice, I'm happy to do that.

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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a ``--cors`` cli option to :program:`python -m http.server`. Contributed by
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also update What's New/3.15.rst

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used blurb to make this entry in NEWS.d, not knowing when it's appropriate to edit the main 3.15.rst file. I think once we know if we're doing --cors / --header , or both, I can make the appropriate update to What's New/3.15.rst

Anton I. Sipos.
Loading