]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-113471: Add custom default Content-Type to http.server (#113475)
authorJohn Comeau <jc@unternet.net>
Thu, 30 Apr 2026 15:00:48 +0000 (08:00 -0700)
committerGitHub <noreply@github.com>
Thu, 30 Apr 2026 15:00:48 +0000 (18:00 +0300)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: donBarbos <donbarbos@proton.me>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Doc/library/http.server.rst
Doc/whatsnew/3.15.rst
Lib/http/server.py
Lib/test/test_httpservers.py
Misc/NEWS.d/next/Library/2023-12-25-19-14-07.gh-issue-113471.ZQMpbI.rst [new file with mode: 0644]

index 33ecaae5c87b0123f0e8f05cee533a26ebd29032..5f325df55705bd4bc79a9238ef6a467cced856f0 100644 (file)
@@ -390,6 +390,14 @@ instantiation, of which this module provides three different variants:
       This will be ``"SimpleHTTP/" + __version__``, where ``__version__`` is
       defined at the module level.
 
+   .. attribute:: default_content_type
+
+      Specifies the Content-Type header value sent when the MIME type
+      cannot be guessed from the file extension of the requested URL.
+      By default, it is set to ``'application/octet-stream'``.
+
+      .. versionadded:: next
+
    .. attribute:: extensions_map
 
       A dictionary mapping suffixes into MIME types, contains custom overrides
@@ -528,6 +536,18 @@ The following options are accepted:
 
    .. versionadded:: 3.11
 
+.. option:: --content-type <content_type>
+
+   Specifies the default Content-Type HTTP header used when the MIME type
+   cannot be guessed from the URL's file extension. By default, the server
+   uses ``'application/octet-stream'``:
+
+   .. code-block:: bash
+
+      python -m http.server --content-type text/html
+
+   .. versionadded:: next
+
 .. option:: --tls-cert
 
    Specifies a TLS certificate chain for HTTPS connections:
index 59b9688c18e1ee0b769036ed989a7492de4b8bbb..90d24bf96afeb47a00bdb435317357a79f3849d2 100644 (file)
@@ -964,6 +964,12 @@ http.server
   <using-on-controlling-color>`.
   (Contributed by Hugo van Kemenade in :gh:`146292`.)
 
+* Added :attr:`~http.server.SimpleHTTPRequestHandler.default_content_type`
+  and the :option:`--content-type <http.server --content-type>` command-line
+  option to allow customizing the default ``Content-Type`` header
+  for files with unknown extensions.
+  (Contributed by John Comeau and Hugo van Kemenade in :gh:`113471`.)
+
 
 inspect
 -------
index 568d3bb38deb6c4d542331697f1dace826665343..27ab37303a085c8197c50a350de2936e6ebb1ce2 100644 (file)
@@ -727,6 +727,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
     """
 
     server_version = "SimpleHTTP"
+    default_content_type = "application/octet-stream"
     index_pages = ("index.html", "index.htm")
     extensions_map = _encodings_map_default = {
         '.gz': 'application/gzip',
@@ -974,7 +975,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
         guess, _ = mimetypes.guess_file_type(path)
         if guess:
             return guess
-        return 'application/octet-stream'
+        return self.default_content_type
 
 
 nobody = None
@@ -1010,9 +1011,10 @@ def _get_best_family(*address):
     return family, sockaddr
 
 
-def test(HandlerClass=BaseHTTPRequestHandler,
+def test(HandlerClass=SimpleHTTPRequestHandler,
          ServerClass=ThreadingHTTPServer,
          protocol="HTTP/1.0", port=8000, bind=None,
+         content_type=SimpleHTTPRequestHandler.default_content_type,
          tls_cert=None, tls_key=None, tls_password=None):
     """Test the HTTP request handler class.
 
@@ -1021,6 +1023,7 @@ def test(HandlerClass=BaseHTTPRequestHandler,
     """
     ServerClass.address_family, addr = _get_best_family(bind, port)
     HandlerClass.protocol_version = protocol
+    HandlerClass.default_content_type = content_type
 
     if tls_cert:
         server = ServerClass(addr, HandlerClass, certfile=tls_cert,
@@ -1060,6 +1063,10 @@ def _main(args=None):
                         default='HTTP/1.0',
                         help='conform to this HTTP version '
                              '(default: %(default)s)')
+    parser.add_argument('--content-type',
+                        default=SimpleHTTPRequestHandler.default_content_type,
+                        help='default content type for unknown extensions '
+                             '(default: %(default)s)')
     parser.add_argument('--tls-cert', metavar='PATH',
                         help='path to the TLS certificate chain file')
     parser.add_argument('--tls-key', metavar='PATH',
@@ -1112,6 +1119,7 @@ def _main(args=None):
         port=args.port,
         bind=args.bind,
         protocol=args.protocol,
+        content_type=args.content_type,
         tls_cert=args.tls_cert,
         tls_key=args.tls_key,
         tls_password=tls_key_password,
index d78b94e3a373d44941473bc0a62024bfad605b8e..1f7a5a42fdaeb71a6e74d567b5dffcbc8afc5eec 100644 (file)
@@ -1379,6 +1379,7 @@ class CommandLineTestCase(unittest.TestCase):
         'protocol': default_protocol,
         'port': default_port,
         'bind': default_bind,
+        'content_type': 'application/octet-stream',
         'tls_cert': None,
         'tls_key': None,
         'tls_password': None,
@@ -1447,6 +1448,16 @@ class CommandLineTestCase(unittest.TestCase):
                     mock_func.assert_called_once_with(**call_args)
                     mock_func.reset_mock()
 
+    @mock.patch('http.server.test')
+    def test_content_type_flag(self, mock_func):
+        content_types = ['text/html', 'text/plain', 'application/json']
+        for content_type in content_types:
+            with self.subTest(content_type=content_type):
+                self.invoke_httpd('--content-type', content_type)
+                call_args = self.args | dict(content_type=content_type)
+                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/2023-12-25-19-14-07.gh-issue-113471.ZQMpbI.rst b/Misc/NEWS.d/next/Library/2023-12-25-19-14-07.gh-issue-113471.ZQMpbI.rst
new file mode 100644 (file)
index 0000000..99ba9bd
--- /dev/null
@@ -0,0 +1,2 @@
+Allow :mod:`http.server` to set a default content-type when serving
+files with an unknown or missing extension.