Skip to content
Open
Show file tree
Hide file tree
Changes from 28 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
24 changes: 23 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 *response_headers*.
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
Added *response_headers*.
Added the *extra_response_headers* parameter.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in 06a9977 and be78515


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,13 @@ 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 specified extra
HTTP response headers to add to each successful HTTP status 200 response.
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
A sequence of ``(name, value)`` pairs containing user specified extra
HTTP response headers to add to each successful HTTP status 200 response.
A sequence of ``(name, value)`` pairs containing user-defined extra
HTTP response headers to add to each successful HTTP status 200 response.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in 53965ff

All other status code responses will not include these headers.
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
All other status code responses will not include these headers.
These headers are not included in other status code responses.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in c280ed8



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

.. method:: do_HEAD()
Expand Down Expand Up @@ -428,6 +439,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
``(name, value)`` pairs containing user specified extra response headers.
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
``(name, value)`` pairs containing user specified extra response headers.
``(name, value)`` pairs containing user-defined extra response headers
sent for a HTTP 200 response.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in 53965ff


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 +557,14 @@ The following options are accepted:

.. versionadded:: 3.14

.. option:: -H, --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.

This is me being nitpicky but would it be preferrable to parse header=value or header value separate by a space? what about cURL/wget and other CLIs? (I think it's easier not to expect = as you did here because we could always say that -H something means -H something 1)

Copy link
Author

Choose a reason for hiding this comment

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

I had a short discussion in the thread on python-ideas with @hugovk about this, see here

In short, cURL and wget do specify headers on their CLI as --header "name: value". In the python-ideas thread I had opposed using a name:value syntax as a positional cli parameter, since it could possibly clash with a port syntax like 127.0.0.1:8000. However if we require an explicit --header or -H before each "name: value", I'm not opposed to the idea.

I think the argument in favor of the PR as it is with -H <name> <value> is simplicity of implementation - it fits right in with argparse and nargs=2. However, if similarity with cURL / wget is desired it isn't much effort to implement a "name: value" format. I have a draft implementation in a separate branch: see aisipos@d878bf6

I'll admit to a slight leaning towards changing to the name: value format, given the principle of least surprise and the popularity of cURL and wget. Thoughts?

Copy link
Member

@picnixz picnixz Oct 10, 2025

Choose a reason for hiding this comment

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

Let's keep the simple space-separated stuff. It will also ease terminal shortcuts where you only delete the previous word (ctrl+w) to change for instance the value of a header. It also simplifies maintenance.

Copy link
Author

Choose a reason for hiding this comment

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

@picnixz OK, I'm fine with keeping the space separated / nargs implementation as is in this PR. I think I've made all the other changes you requested. What else remains to do for this PR?


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


.. _http.server-security:

Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ difflib
(Contributed by Jiahao Li in :gh:`134580`.)


http.server
Copy link
Member

Choose a reason for hiding this comment

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

GitHub tells me that there is a conflict in this file, so you'll need to fix it.

Copy link
Author

Choose a reason for hiding this comment

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

Merged main back into this branch and fixed the conflict in fae21f9. See the new section in the fixed file here:

http.server

-----------

* Added a new ``extra_response_headers`` keyword argument to
:class:`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`
command-line interface to support custom headers in HTTP responses.
(Contributed by Anton I. Sipos in :gh:`135057`.)


math
----

Expand Down
48 changes: 35 additions & 13 deletions Lib/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,10 +692,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):
Copy link
Member

Choose a reason for hiding this comment

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

Do you think it's important for instances to know whether extra_response_headers was specified (and empty) or was left unspecified? I would say "no", but this may decide whether we want to allow extensibility or not (with None, it allows instances to decide whether what is given at construction time is the default or explicitly "no headers").

Copy link
Author

Choose a reason for hiding this comment

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

I suppose you are considering an alternative where we don't specify extra_response_headers as an explicit argument and pull it out of kwargs inside the method like so?

extra_response_headers = kwargs.pop('extra_response_headers', None)

I included extra_response_headers=None for backwards compatibility, and for clarity as a possibly useful argument as part of the public interface. So long as we think it should be part of the public interface, I think we can leave this line as is. As you say, I don't think I can find a use case where we'd need to distinguish between not passing the argument and sending it as None or [].

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 @@ -713,6 +714,12 @@ def do_HEAD(self):
if f:
f.close()

def _send_extra_response_headers(self):
"""Send the headers stored in self.extra_response_headers"""
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 @@ -795,6 +802,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 @@ -859,6 +867,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 @@ -967,25 +976,33 @@ def _get_best_family(*address):
return family, sockaddr


def _make_server(HandlerClass=BaseHTTPRequestHandler,
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
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
HandlerClass=HandlerClass, ServerClass=ServerClass,
protocol=protocol, port=port, bind=bind, tls_cert=tls_cert,
tls_key=tls_key, tls_password=tls_password
HandlerClass=HandlerClass, ServerClass=ServerClass,
protocol=protocol, port=port, bind=bind,
tls_cert=tls_cert, tls_key=tls_key, tls_password=tls_password,

I would prefer this split just to group the tls_* arguments together. I think it fits on 80 chars.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in 64122df. It ends up being only 68 chars so it fits easily under 80. I didn't leave in a trailing comma, if that matters I can add it in.

) 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 @@ -1024,6 +1041,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 used multiple times)')
Copy link
Member

Choose a reason for hiding this comment

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

The can be used multiple times is already implied with `action='append' I think.

Copy link
Author

Choose a reason for hiding this comment

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

I agree in the code the multiple hint is obvious, but I added this for users running --help in the CLI, which will show with python -m http.server --help:

  -H, --header HEADER VALUE
                        Add a custom response header (can be used multiple times)

argparse doesn't give us an indicator in the --help output automatically for action-'append', so I added an explicit hint.

Copy link
Member

Choose a reason for hiding this comment

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

Oh right. I think this should be improved in argparse then.

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
'(can be used multiple times)')
'(can be specified multiple times)')

The trace.py argparse uses "specified"

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in f0d1bac

args = parser.parse_args(args)

if not args.tls_cert and args.tls_key:
Expand Down Expand Up @@ -1052,7 +1073,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
76 changes: 75 additions & 1 deletion Lib/test/test_httpservers.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,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)


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

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

def test_extra_headers_list_dir(self):
Copy link
Member

Choose a reason for hiding this comment

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

Check that the extra headers are only sent for HTTP 200 responses and always check the response code in the tests.

Copy link
Author

Choose a reason for hiding this comment

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

See updates to the tests in 8baa875, 7856d27, and 303ab5b

with mock.patch.object(self.request_handler, 'extra_response_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_extra_response_headers_get_file(self):
with mock.patch.object(self.request_handler, 'extra_response_headers', new=[
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
with mock.patch.object(self.request_handler, 'extra_response_headers', new=[
with mock.patch.object(self.request_handler, 'extra_response_headers', [

no need for using the 'new' keyword (same for the previous test)

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in e99780e

('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("Set-Cookie"),
'test1=value1, test2=value2')
self.assertEqual(response.getheader("X-Test1"), 'value3')
Copy link
Member

Choose a reason for hiding this comment

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

ensure 2 blank lines around classes (there is one blank line missing after this)

Copy link
Author

Choose a reason for hiding this comment

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

Fixed in 2e829bb


class SocketlessRequestHandler(SimpleHTTPRequestHandler):
def __init__(self, directory=None):
Expand Down Expand Up @@ -1371,6 +1401,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)
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()

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 @@ -1454,6 +1499,35 @@ 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', '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)

# 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']
]
)


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.