Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
55 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
526e499
Merge remote-tracking branch 'upstream/main'
aisipos Dec 12, 2025
46c1c91
Merge remote-tracking branch 'upstream/main' into https-server-cors-i…
aisipos Apr 12, 2026
c1fee3b
gh-035056: Minor documentation formating and punctuation fixes.
aisipos Apr 12, 2026
3a4fed6
gh-135056: Don't let extra headers overwrite default headers
aisipos Apr 12, 2026
9f0ed01
gh-135056: Fix attribute access to default_response_headers
aisipos Apr 12, 2026
1036a91
gh-135056: Use proper RST syntax for attribute references.
aisipos Apr 28, 2026
49ffc92
gh-135056: Make more new args and attrs private.
aisipos Apr 28, 2026
511d902
Update Lib/http/server.py
picnixz Apr 29, 2026
44205e0
Update Lib/http/server.py
picnixz Apr 29, 2026
280cea3
gh-135056: Add multithreaded test for extra_response_headers
aisipos Apr 30, 2026
d188977
gh-135056: Fix test_extra_response_headers_concurrent_requests
aisipos Apr 30, 2026
9f7ac47
Merge remote-tracking branch 'upstream/main'
aisipos May 1, 2026
9851622
gh-135056: Don't raise exceptions in threaded tests child threads.
aisipos May 1, 2026
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
23 changes: 22 additions & 1 deletion Doc/library/http.server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ 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, extra_response_headers=None)

This class serves files from the directory *directory* and below,
or the current directory if *directory* is not provided, directly
Expand All @@ -374,6 +375,9 @@ instantiation, of which this module provides three different variants:
.. versionchanged:: 3.9
The *directory* parameter accepts a :term:`path-like object`.

.. versionchanged:: next
Added *extra_response_headers* parameter.

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 All @@ -396,6 +400,12 @@ instantiation, of which this module provides three different variants:
This dictionary is no longer filled with the default system mappings,
but only contains overrides.

.. attribute:: extra_response_headers

A sequence of ``(name, value)`` pairs containing user-defined extra
HTTP response headers to add to each successful HTTP status 200 response.
These headers are not included in other status code responses.

The :class:`SimpleHTTPRequestHandler` class defines the following methods:

.. method:: do_HEAD()
Expand Down Expand Up @@ -428,6 +438,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 instance attribute ``extra_response_headers`` is a sequence of
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
The instance attribute ``extra_response_headers`` is a sequence of
The instance attribute :attr:`extra_response_headers` is a sequence of

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 1036a91

``(name, value)`` pairs containing user-defined extra response headers.

Then follows a blank line signifying the end of the headers, and then the
contents of the file are output.

Expand Down Expand Up @@ -543,6 +556,14 @@ The following options are accepted:

.. versionadded:: 3.14

.. option:: -H, --header <header> <value>
Comment thread
picnixz marked this conversation as resolved.

Specify an additional extra HTTP Response Header to send on successful HTTP
200 responses. Can be used multiple times to send additional custom response
headers.

.. versionadded:: next


Comment thread
picnixz marked this conversation as resolved.
.. _http.server-security:

Expand Down
13 changes: 13 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,19 @@ http.cookies
(Contributed by Nick Burns and Senthil Kumaran in :gh:`92936`.)


http.server
Comment thread
hugovk marked this conversation as resolved.
-----------

* Added a new ``extra_response_headers`` keyword argument to
:class:`~http.server.SimpleHTTPRequestHandler` to support custom headers in
HTTP responses.
(Contributed by Anton I. Sipos in :gh:`135057`.)

* Added a ``-H`` or ``--header`` flag to the :program:`python -m http.server`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* Added a ``-H`` or ``--header`` flag to the :program:`python -m http.server`
* Added the ``-H/--header`` option to the :program:`python -m http.server`

command-line interface to support custom headers in HTTP responses.
(Contributed by Anton I. Sipos in :gh:`135057`.)


inspect
-------

Expand Down
48 changes: 35 additions & 13 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,10 +699,11 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
'.xz': 'application/x-xz',
}

def __init__(self, *args, directory=None, **kwargs):
def __init__(self, *args, directory=None, extra_response_headers=None, **kwargs):
Comment thread
picnixz marked this conversation as resolved.
if directory is None:
directory = os.getcwd()
self.directory = os.fspath(directory)
self.extra_response_headers = extra_response_headers
super().__init__(*args, **kwargs)

def do_GET(self):
Expand All @@ -720,6 +721,12 @@ def do_HEAD(self):
if f:
f.close()

def _send_extra_response_headers(self):
"""Send the headers stored in self.extra_response_headers"""
Comment thread
picnixz marked this conversation as resolved.
Outdated
if self.extra_response_headers is not None:
for header, value in self.extra_response_headers:
self.send_header(header, value)

def send_head(self):
"""Common code for GET and HEAD commands.

Expand Down Expand Up @@ -802,6 +809,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))
self._send_extra_response_headers()
self.end_headers()
return f
except:
Expand Down Expand Up @@ -866,6 +874,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_extra_response_headers()
self.end_headers()
return f

Expand Down Expand Up @@ -974,25 +983,33 @@ def _get_best_family(*address):
return family, sockaddr


def _make_server(HandlerClass=BaseHTTPRequestHandler,
Comment thread
picnixz marked this conversation as resolved.
ServerClass=ThreadingHTTPServer,
protocol="HTTP/1.0", port=8000, bind=None,
tls_cert=None, tls_key=None, tls_password=None):
ServerClass.address_family, addr = _get_best_family(bind, port)
HandlerClass.protocol_version = protocol

if tls_cert:
return ServerClass(addr, HandlerClass, certfile=tls_cert,
keyfile=tls_key, password=tls_password)
else:
return ServerClass(addr, HandlerClass)


def test(HandlerClass=BaseHTTPRequestHandler,
ServerClass=ThreadingHTTPServer,
protocol="HTTP/1.0", port=8000, bind=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).

"""
ServerClass.address_family, addr = _get_best_family(bind, port)
HandlerClass.protocol_version = protocol

if tls_cert:
server = ServerClass(addr, HandlerClass, certfile=tls_cert,
keyfile=tls_key, password=tls_password)
else:
server = ServerClass(addr, HandlerClass)

with server as httpd:
with _make_server(
HandlerClass=HandlerClass, ServerClass=ServerClass,
protocol=protocol, port=port, bind=bind,
tls_cert=tls_cert, tls_key=tls_key, tls_password=tls_password
) as httpd:
host, port = httpd.socket.getsockname()[:2]
url_host = f'[{host}]' if ':' in host else host
protocol = 'HTTPS' if tls_cert else 'HTTP'
Expand Down Expand Up @@ -1031,6 +1048,10 @@ def _main(args=None):
parser.add_argument('port', default=8000, type=int, nargs='?',
help='bind to this port '
'(default: %(default)s)')
parser.add_argument('-H', '--header', nargs=2, action='append',
metavar=('HEADER', 'VALUE'),
help='Add a custom response header '
'(can be specified multiple times)')
args = parser.parse_args(args)

if not args.tls_cert and args.tls_key:
Expand Down Expand Up @@ -1059,7 +1080,8 @@ def server_bind(self):

def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self,
directory=args.directory)
directory=args.directory,
extra_response_headers=args.header)

class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer):
pass
Expand Down
88 changes: 87 additions & 1 deletion Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,16 @@ def test_err(self):
self.assertEndsWith(lines[1], '"ERROR / HTTP/1.1" 404 -')


class CustomHeaderSimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
extra_response_headers = None

def __init__(self, *args, **kwargs):
kwargs.setdefault('extra_response_headers', self.extra_response_headers)
super().__init__(*args, **kwargs)

Comment thread
hugovk marked this conversation as resolved.

class SimpleHTTPServerTestCase(BaseTestCase):
class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler):
class request_handler(NoLogRequestHandler, CustomHeaderSimpleHTTPRequestHandler):
pass

def setUp(self):
Expand Down Expand Up @@ -861,6 +869,39 @@ def test_path_without_leading_slash(self):
self.assertEqual(response.getheader("Location"),
self.tempdir_name + "/?hi=1")

def test_extra_response_headers_list_dir(self):
with mock.patch.object(self.request_handler, 'extra_response_headers', [
('X-Test1', 'test1'),
('X-Test2', 'test2'),
]):
response = self.request(self.base_url + '/')
self.assertEqual(response.status, 200)
self.assertEqual(response.getheader("X-Test1"), 'test1')
self.assertEqual(response.getheader("X-Test2"), 'test2')

def test_extra_response_headers_get_file(self):
with mock.patch.object(self.request_handler, 'extra_response_headers', [
('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.status, 200)
self.assertEqual(response.getheader("Set-Cookie"),
'test1=value1, test2=value2')
self.assertEqual(response.getheader("X-Test1"), 'value3')
Comment thread
hugovk marked this conversation as resolved.

def test_extra_response_headers_missing_on_404(self):
with mock.patch.object(self.request_handler, 'extra_response_headers', [
('X-Test1', 'value'),
]):
response = self.request(self.base_url + '/missing.html')
self.assertEqual(response.status, 404)
self.assertEqual(response.getheader("X-Test1"), None)


class SocketlessRequestHandler(SimpleHTTPRequestHandler):
def __init__(self, directory=None):
Expand Down Expand Up @@ -1409,6 +1450,21 @@ 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_header_flag(self, mock_func):
call_args = self.args
self.invoke_httpd('--header', 'h1', 'v1', '-H', 'h2', 'v2')
mock_func.assert_called_once_with(**call_args)
Comment thread
hugovk marked this conversation as resolved.
mock_func.reset_mock()

def test_extra_header_flag_too_few_args(self):
with self.assertRaises(SystemExit):
self.invoke_httpd('--header', 'h1')

def test_extra_header_flag_too_many_args(self):
with self.assertRaises(SystemExit):
self.invoke_httpd('--header', 'h1', 'v1', 'h2')

@unittest.skipIf(ssl is None, "requires ssl")
@mock.patch('http.server.test')
def test_tls_cert_and_key_flags(self, mock_func):
Expand Down Expand Up @@ -1492,6 +1548,36 @@ def test_unknown_flag(self, _):
self.assertEqual(stdout.getvalue(), '')
self.assertIn('error', stderr.getvalue())

@mock.patch('http.server._make_server', wraps=server._make_server)
@mock.patch.object(HTTPServer, 'serve_forever')
def test_extra_response_headers_arg(self, _, mock_make_server):
server._main(
['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2:v3 v4', '8080']
)
# Get an instance of the server / RequestHandler by using
# the spied call args, then calling _make_server with them.
args, kwargs = mock_make_server.call_args
httpd = server._make_server(*args, **kwargs)
self.addCleanup(httpd.server_close)

# Ensure the RequestHandler class is passed the correct response
# headers
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 extra_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,
extra_response_headers=[
['Set-Cookie', 'k=v'], ['Set-Cookie', 'k2=v2:v3 v4']
]
)


class CommandLineRunTimeTestCase(unittest.TestCase):
served_data = os.urandom(32)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add a ``-H`` or ``--header`` CLI option to :program:`python -m http.server`. Contributed by
Anton I. Sipos.
Loading