https://github.com/python/cpython/commit/faac627e47f72797f5b7a65134bf4cdce6575ee9
commit: faac627e47f72797f5b7a65134bf4cdce6575ee9
branch: main
author: Bénédikt Tran <10796600+picn...@users.noreply.github.com>
committer: picnixz <10796600+picn...@users.noreply.github.com>
date: 2025-05-17T09:58:16+02:00
summary:

gh-133810: remove `http.server.CGIHTTPRequestHandler` and `--cgi` flag (#133811)

The CGI HTTP request handler has been deprecated since Python 3.13.

files:
A Misc/NEWS.d/next/Library/2025-05-10-11-04-47.gh-issue-133810.03WhnK.rst
M Doc/deprecations/pending-removal-in-3.15.rst
M Doc/library/http.server.rst
M Doc/whatsnew/3.13.rst
M Doc/whatsnew/3.15.rst
M Lib/_compat_pickle.py
M Lib/http/client.py
M Lib/http/server.py
M Lib/test/test_httpservers.py
M Lib/wsgiref/handlers.py
M Misc/NEWS.d/3.13.0a1.rst

diff --git a/Doc/deprecations/pending-removal-in-3.15.rst 
b/Doc/deprecations/pending-removal-in-3.15.rst
index 707253a91ecd40..a76d06cce1278a 100644
--- a/Doc/deprecations/pending-removal-in-3.15.rst
+++ b/Doc/deprecations/pending-removal-in-3.15.rst
@@ -20,7 +20,7 @@ Pending removal in Python 3.15
 
 * :mod:`http.server`:
 
-  * The obsolete and rarely used :class:`~http.server.CGIHTTPRequestHandler`
+  * The obsolete and rarely used :class:`!CGIHTTPRequestHandler`
     has been deprecated since Python 3.13.
     No direct replacement exists.
     *Anything* is better than CGI to interface
diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst
index 02016c789b24b4..063344e0284258 100644
--- a/Doc/library/http.server.rst
+++ b/Doc/library/http.server.rst
@@ -458,55 +458,6 @@ such as using different index file names by overriding the 
class attribute
 :attr:`index_pages`.
 
 
-.. class:: CGIHTTPRequestHandler(request, client_address, server)
-
-   This class is used to serve either files or output of CGI scripts from the
-   current directory and below. Note that mapping HTTP hierarchic structure to
-   local directory structure is exactly as in 
:class:`SimpleHTTPRequestHandler`.
-
-   .. note::
-
-      CGI scripts run by the :class:`CGIHTTPRequestHandler` class cannot 
execute
-      redirects (HTTP code 302), because code 200 (script output follows) is
-      sent prior to execution of the CGI script.  This pre-empts the status
-      code.
-
-   The class will however, run the CGI script, instead of serving it as a file,
-   if it guesses it to be a CGI script.  Only directory-based CGI are used ---
-   the other common server configuration is to treat special extensions as
-   denoting CGI scripts.
-
-   The :func:`do_GET` and :func:`do_HEAD` functions are modified to run CGI 
scripts
-   and serve the output, instead of serving files, if the request leads to
-   somewhere below the ``cgi_directories`` path.
-
-   The :class:`CGIHTTPRequestHandler` defines the following data member:
-
-   .. attribute:: cgi_directories
-
-      This defaults to ``['/cgi-bin', '/htbin']`` and describes directories to
-      treat as containing CGI scripts.
-
-   The :class:`CGIHTTPRequestHandler` defines the following method:
-
-   .. method:: do_POST()
-
-      This method serves the ``'POST'`` request type, only allowed for CGI
-      scripts.  Error 501, "Can only POST to CGI scripts", is output when 
trying
-      to POST to a non-CGI url.
-
-   Note that CGI scripts will be run with UID of user nobody, for security
-   reasons.  Problems with the CGI script will be translated to error 403.
-
-   .. deprecated-removed:: 3.13 3.15
-
-      :class:`CGIHTTPRequestHandler` is being removed in 3.15.  CGI has not
-      been considered a good way to do things for well over a decade. This code
-      has been unmaintained for a while now and sees very little practical use.
-      Retaining it could lead to further :ref:`security considerations
-      <http.server-security>`.
-
-
 .. _http-server-cli:
 
 Command-line interface
@@ -563,24 +514,6 @@ The following options are accepted:
 
    .. versionadded:: 3.11
 
-.. option:: --cgi
-
-   :class:`CGIHTTPRequestHandler` can be enabled in the command line by passing
-   the ``--cgi`` option::
-
-      python -m http.server --cgi
-
-   .. deprecated-removed:: 3.13 3.15
-
-      :mod:`http.server` command line ``--cgi`` support is being removed
-      because :class:`CGIHTTPRequestHandler` is being removed.
-
-.. warning::
-
-   :class:`CGIHTTPRequestHandler` and the ``--cgi`` command-line option
-   are not intended for use by untrusted clients and may be vulnerable
-   to exploitation. Always use within a secure environment.
-
 .. option:: --tls-cert
 
    Specifies a TLS certificate chain for HTTPS connections::
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index e64eb19bddb522..023c279979d842 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -1871,7 +1871,7 @@ New Deprecations
 
 * :mod:`http.server`:
 
-  * Deprecate :class:`~http.server.CGIHTTPRequestHandler`,
+  * Deprecate :class:`!CGIHTTPRequestHandler`,
     to be removed in Python 3.15.
     Process-based CGI HTTP servers have been out of favor for a very long time.
     This code was outdated, unmaintained, and rarely used.
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 9e9a168db0e725..987cf944972329 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -121,6 +121,15 @@ Deprecated
 Removed
 =======
 
+http.server
+-----------
+
+* Removed the :class:`!CGIHTTPRequestHandler` class
+  and the ``--cgi`` flag from the :program:`python -m http.server`
+  command-line interface. They were deprecated in Python 3.13.
+  (Contributed by Bénédikt Tran in :gh:`133810`.)
+
+
 platform
 --------
 
diff --git a/Lib/_compat_pickle.py b/Lib/_compat_pickle.py
index 439f8c02f4b586..a981326432429b 100644
--- a/Lib/_compat_pickle.py
+++ b/Lib/_compat_pickle.py
@@ -175,7 +175,6 @@
     'SimpleDialog': 'tkinter.simpledialog',
     'DocXMLRPCServer': 'xmlrpc.server',
     'SimpleHTTPServer': 'http.server',
-    'CGIHTTPServer': 'http.server',
     # For compatibility with broken pickles saved in old Python 3 versions
     'UserDict': 'collections',
     'UserList': 'collections',
@@ -217,8 +216,6 @@
         ('DocXMLRPCServer', 'DocCGIXMLRPCRequestHandler'),
     ('http.server', 'SimpleHTTPRequestHandler'):
         ('SimpleHTTPServer', 'SimpleHTTPRequestHandler'),
-    ('http.server', 'CGIHTTPRequestHandler'):
-        ('CGIHTTPServer', 'CGIHTTPRequestHandler'),
     ('_socket', 'socket'): ('socket', '_socketobject'),
 })
 
diff --git a/Lib/http/client.py b/Lib/http/client.py
index 33a858d34ae1ba..e7a1c7bc3b2ae1 100644
--- a/Lib/http/client.py
+++ b/Lib/http/client.py
@@ -181,11 +181,10 @@ def _strip_ipv6_iface(enc_name: bytes) -> bytes:
     return enc_name
 
 class HTTPMessage(email.message.Message):
-    # XXX The only usage of this method is in
-    # http.server.CGIHTTPRequestHandler.  Maybe move the code there so
-    # that it doesn't need to be part of the public API.  The API has
-    # never been defined so this could cause backwards compatibility
-    # issues.
+
+    # The getallmatchingheaders() method was only used by the CGI handler
+    # that was removed in Python 3.15. However, since the public API was not
+    # properly defined, it will be kept for backwards compatibility reasons.
 
     def getallmatchingheaders(self, name):
         """Find all header lines matching a given header name.
diff --git a/Lib/http/server.py b/Lib/http/server.py
index 8be1903743a9a2..f42e9a375e479a 100644
--- a/Lib/http/server.py
+++ b/Lib/http/server.py
@@ -1,29 +1,10 @@
 """HTTP server classes.
 
 Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see
-SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST,
-and (deprecated) CGIHTTPRequestHandler for CGI scripts.
+SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST.
 
 It does, however, optionally implement HTTP/1.1 persistent connections.
 
-Notes on CGIHTTPRequestHandler
-------------------------------
-
-This class is deprecated. It implements GET and POST requests to cgi-bin 
scripts.
-
-If the os.fork() function is not present (Windows), subprocess.Popen() is used,
-with slightly altered but never documented semantics.  Use from a threaded
-process is likely to trigger a warning at os.fork() time.
-
-In all cases, the implementation is intentionally naive -- all
-requests are executed synchronously.
-
-SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL
--- it may execute arbitrary Python code or external programs.
-
-Note that status code 200 is sent prior to execution of a CGI script, so
-scripts cannot send other status codes such as 302 (redirect).
-
 XXX To do:
 
 - log requests even later (to capture byte count)
@@ -86,10 +67,8 @@
     "HTTPServer", "ThreadingHTTPServer",
     "HTTPSServer", "ThreadingHTTPSServer",
     "BaseHTTPRequestHandler", "SimpleHTTPRequestHandler",
-    "CGIHTTPRequestHandler",
 ]
 
-import copy
 import datetime
 import email.utils
 import html
@@ -99,7 +78,6 @@
 import mimetypes
 import os
 import posixpath
-import select
 import shutil
 import socket
 import socketserver
@@ -953,56 +931,6 @@ def guess_type(self, path):
         return 'application/octet-stream'
 
 
-# Utilities for CGIHTTPRequestHandler
-
-def _url_collapse_path(path):
-    """
-    Given a URL path, remove extra '/'s and '.' path elements and collapse
-    any '..' references and returns a collapsed path.
-
-    Implements something akin to RFC-2396 5.2 step 6 to parse relative paths.
-    The utility of this function is limited to is_cgi method and helps
-    preventing some security attacks.
-
-    Returns: The reconstituted URL, which will always start with a '/'.
-
-    Raises: IndexError if too many '..' occur within the path.
-
-    """
-    # Query component should not be involved.
-    path, _, query = path.partition('?')
-    path = urllib.parse.unquote(path)
-
-    # Similar to os.path.split(os.path.normpath(path)) but specific to URL
-    # path semantics rather than local operating system semantics.
-    path_parts = path.split('/')
-    head_parts = []
-    for part in path_parts[:-1]:
-        if part == '..':
-            head_parts.pop() # IndexError if more '..' than prior parts
-        elif part and part != '.':
-            head_parts.append( part )
-    if path_parts:
-        tail_part = path_parts.pop()
-        if tail_part:
-            if tail_part == '..':
-                head_parts.pop()
-                tail_part = ''
-            elif tail_part == '.':
-                tail_part = ''
-    else:
-        tail_part = ''
-
-    if query:
-        tail_part = '?'.join((tail_part, query))
-
-    splitpath = ('/' + '/'.join(head_parts), tail_part)
-    collapsed_path = "/".join(splitpath)
-
-    return collapsed_path
-
-
-
 nobody = None
 
 def nobody_uid():
@@ -1026,274 +954,6 @@ def executable(path):
     return os.access(path, os.X_OK)
 
 
-class CGIHTTPRequestHandler(SimpleHTTPRequestHandler):
-
-    """Complete HTTP server with GET, HEAD and POST commands.
-
-    GET and HEAD also support running CGI scripts.
-
-    The POST command is *only* implemented for CGI scripts.
-
-    """
-
-    def __init__(self, *args, **kwargs):
-        import warnings
-        warnings._deprecated("http.server.CGIHTTPRequestHandler",
-                             remove=(3, 15))
-        super().__init__(*args, **kwargs)
-
-    # Determine platform specifics
-    have_fork = hasattr(os, 'fork')
-
-    # Make rfile unbuffered -- we need to read one line and then pass
-    # the rest to a subprocess, so we can't use buffered input.
-    rbufsize = 0
-
-    def do_POST(self):
-        """Serve a POST request.
-
-        This is only implemented for CGI scripts.
-
-        """
-
-        if self.is_cgi():
-            self.run_cgi()
-        else:
-            self.send_error(
-                HTTPStatus.NOT_IMPLEMENTED,
-                "Can only POST to CGI scripts")
-
-    def send_head(self):
-        """Version of send_head that support CGI scripts"""
-        if self.is_cgi():
-            return self.run_cgi()
-        else:
-            return SimpleHTTPRequestHandler.send_head(self)
-
-    def is_cgi(self):
-        """Test whether self.path corresponds to a CGI script.
-
-        Returns True and updates the cgi_info attribute to the tuple
-        (dir, rest) if self.path requires running a CGI script.
-        Returns False otherwise.
-
-        If any exception is raised, the caller should assume that
-        self.path was rejected as invalid and act accordingly.
-
-        The default implementation tests whether the normalized url
-        path begins with one of the strings in self.cgi_directories
-        (and the next character is a '/' or the end of the string).
-
-        """
-        collapsed_path = _url_collapse_path(self.path)
-        dir_sep = collapsed_path.find('/', 1)
-        while dir_sep > 0 and not collapsed_path[:dir_sep] in 
self.cgi_directories:
-            dir_sep = collapsed_path.find('/', dir_sep+1)
-        if dir_sep > 0:
-            head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:]
-            self.cgi_info = head, tail
-            return True
-        return False
-
-
-    cgi_directories = ['/cgi-bin', '/htbin']
-
-    def is_executable(self, path):
-        """Test whether argument path is an executable file."""
-        return executable(path)
-
-    def is_python(self, path):
-        """Test whether argument path is a Python script."""
-        head, tail = os.path.splitext(path)
-        return tail.lower() in (".py", ".pyw")
-
-    def run_cgi(self):
-        """Execute a CGI script."""
-        dir, rest = self.cgi_info
-        path = dir + '/' + rest
-        i = path.find('/', len(dir)+1)
-        while i >= 0:
-            nextdir = path[:i]
-            nextrest = path[i+1:]
-
-            scriptdir = self.translate_path(nextdir)
-            if os.path.isdir(scriptdir):
-                dir, rest = nextdir, nextrest
-                i = path.find('/', len(dir)+1)
-            else:
-                break
-
-        # find an explicit query string, if present.
-        rest, _, query = rest.partition('?')
-
-        # dissect the part after the directory name into a script name &
-        # a possible additional path, to be stored in PATH_INFO.
-        i = rest.find('/')
-        if i >= 0:
-            script, rest = rest[:i], rest[i:]
-        else:
-            script, rest = rest, ''
-
-        scriptname = dir + '/' + script
-        scriptfile = self.translate_path(scriptname)
-        if not os.path.exists(scriptfile):
-            self.send_error(
-                HTTPStatus.NOT_FOUND,
-                "No such CGI script (%r)" % scriptname)
-            return
-        if not os.path.isfile(scriptfile):
-            self.send_error(
-                HTTPStatus.FORBIDDEN,
-                "CGI script is not a plain file (%r)" % scriptname)
-            return
-        ispy = self.is_python(scriptname)
-        if self.have_fork or not ispy:
-            if not self.is_executable(scriptfile):
-                self.send_error(
-                    HTTPStatus.FORBIDDEN,
-                    "CGI script is not executable (%r)" % scriptname)
-                return
-
-        # Reference: https://www6.uniovi.es/~antonio/ncsa_httpd/cgi/env.html
-        # XXX Much of the following could be prepared ahead of time!
-        env = copy.deepcopy(os.environ)
-        env['SERVER_SOFTWARE'] = self.version_string()
-        env['SERVER_NAME'] = self.server.server_name
-        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
-        env['SERVER_PROTOCOL'] = self.protocol_version
-        env['SERVER_PORT'] = str(self.server.server_port)
-        env['REQUEST_METHOD'] = self.command
-        uqrest = urllib.parse.unquote(rest)
-        env['PATH_INFO'] = uqrest
-        env['PATH_TRANSLATED'] = self.translate_path(uqrest)
-        env['SCRIPT_NAME'] = scriptname
-        env['QUERY_STRING'] = query
-        env['REMOTE_ADDR'] = self.client_address[0]
-        authorization = self.headers.get("authorization")
-        if authorization:
-            authorization = authorization.split()
-            if len(authorization) == 2:
-                import base64, binascii
-                env['AUTH_TYPE'] = authorization[0]
-                if authorization[0].lower() == "basic":
-                    try:
-                        authorization = authorization[1].encode('ascii')
-                        authorization = base64.decodebytes(authorization).\
-                                        decode('ascii')
-                    except (binascii.Error, UnicodeError):
-                        pass
-                    else:
-                        authorization = authorization.split(':')
-                        if len(authorization) == 2:
-                            env['REMOTE_USER'] = authorization[0]
-        # XXX REMOTE_IDENT
-        if self.headers.get('content-type') is None:
-            env['CONTENT_TYPE'] = self.headers.get_content_type()
-        else:
-            env['CONTENT_TYPE'] = self.headers['content-type']
-        length = self.headers.get('content-length')
-        if length:
-            env['CONTENT_LENGTH'] = length
-        referer = self.headers.get('referer')
-        if referer:
-            env['HTTP_REFERER'] = referer
-        accept = self.headers.get_all('accept', ())
-        env['HTTP_ACCEPT'] = ','.join(accept)
-        ua = self.headers.get('user-agent')
-        if ua:
-            env['HTTP_USER_AGENT'] = ua
-        co = filter(None, self.headers.get_all('cookie', []))
-        cookie_str = ', '.join(co)
-        if cookie_str:
-            env['HTTP_COOKIE'] = cookie_str
-        # XXX Other HTTP_* headers
-        # Since we're setting the env in the parent, provide empty
-        # values to override previously set values
-        for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
-                  'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
-            env.setdefault(k, "")
-
-        self.send_response(HTTPStatus.OK, "Script output follows")
-        self.flush_headers()
-
-        decoded_query = query.replace('+', ' ')
-
-        if self.have_fork:
-            # Unix -- fork as we should
-            args = [script]
-            if '=' not in decoded_query:
-                args.append(decoded_query)
-            nobody = nobody_uid()
-            self.wfile.flush() # Always flush before forking
-            pid = os.fork()
-            if pid != 0:
-                # Parent
-                pid, sts = os.waitpid(pid, 0)
-                # throw away additional data [see bug #427345]
-                while select.select([self.rfile], [], [], 0)[0]:
-                    if not self.rfile.read(1):
-                        break
-                exitcode = os.waitstatus_to_exitcode(sts)
-                if exitcode:
-                    self.log_error(f"CGI script exit code {exitcode}")
-                return
-            # Child
-            try:
-                try:
-                    os.setuid(nobody)
-                except OSError:
-                    pass
-                os.dup2(self.rfile.fileno(), 0)
-                os.dup2(self.wfile.fileno(), 1)
-                os.execve(scriptfile, args, env)
-            except:
-                self.server.handle_error(self.request, self.client_address)
-                os._exit(127)
-
-        else:
-            # Non-Unix -- use subprocess
-            import subprocess
-            cmdline = [scriptfile]
-            if self.is_python(scriptfile):
-                interp = sys.executable
-                if interp.lower().endswith("w.exe"):
-                    # On Windows, use python.exe, not pythonw.exe
-                    interp = interp[:-5] + interp[-4:]
-                cmdline = [interp, '-u'] + cmdline
-            if '=' not in query:
-                cmdline.append(query)
-            self.log_message("command: %s", subprocess.list2cmdline(cmdline))
-            try:
-                nbytes = int(length)
-            except (TypeError, ValueError):
-                nbytes = 0
-            p = subprocess.Popen(cmdline,
-                                 stdin=subprocess.PIPE,
-                                 stdout=subprocess.PIPE,
-                                 stderr=subprocess.PIPE,
-                                 env = env
-                                 )
-            if self.command.lower() == "post" and nbytes > 0:
-                data = self.rfile.read(nbytes)
-            else:
-                data = None
-            # throw away additional data [see bug #427345]
-            while select.select([self.rfile._sock], [], [], 0)[0]:
-                if not self.rfile._sock.recv(1):
-                    break
-            stdout, stderr = p.communicate(data)
-            self.wfile.write(stdout)
-            if stderr:
-                self.log_error('%s', stderr)
-            p.stderr.close()
-            p.stdout.close()
-            status = p.returncode
-            if status:
-                self.log_error("CGI script exit status %#x", status)
-            else:
-                self.log_message("CGI script exited OK")
-
-
 def _get_best_family(*address):
     infos = socket.getaddrinfo(
         *address,
@@ -1336,13 +996,12 @@ def test(HandlerClass=BaseHTTPRequestHandler,
             print("\nKeyboard interrupt received, exiting.")
             sys.exit(0)
 
+
 if __name__ == '__main__':
     import argparse
     import contextlib
 
     parser = argparse.ArgumentParser(color=True)
-    parser.add_argument('--cgi', action='store_true',
-                        help='run as CGI server')
     parser.add_argument('-b', '--bind', metavar='ADDRESS',
                         help='bind to this address '
                              '(default: all interfaces)')
@@ -1378,11 +1037,6 @@ def test(HandlerClass=BaseHTTPRequestHandler,
         except OSError as e:
             parser.error(f"Failed to read TLS password file: {e}")
 
-    if args.cgi:
-        handler_class = CGIHTTPRequestHandler
-    else:
-        handler_class = SimpleHTTPRequestHandler
-
     # ensure dual-stack is not disabled; ref #38907
     class DualStackServer(ThreadingHTTPServer):
 
@@ -1398,7 +1052,7 @@ def finish_request(self, request, client_address):
                                      directory=args.directory)
 
     test(
-        HandlerClass=handler_class,
+        HandlerClass=SimpleHTTPRequestHandler,
         ServerClass=DualStackServer,
         port=args.port,
         bind=args.bind,
diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py
index 557e698aa3481c..11c74a02bf2903 100644
--- a/Lib/test/test_httpservers.py
+++ b/Lib/test/test_httpservers.py
@@ -3,16 +3,15 @@
 Written by Cody A.W. Somerville <cody-somervi...@ubuntu.com>,
 Josip Dzolonga, and Michael Otteneder for the 2007/08 GHOP contest.
 """
-from collections import OrderedDict
+
 from http.server import BaseHTTPRequestHandler, HTTPServer, HTTPSServer, \
-     SimpleHTTPRequestHandler, CGIHTTPRequestHandler
+     SimpleHTTPRequestHandler
 from http import server, HTTPStatus
 
 import os
 import socket
 import sys
 import re
-import base64
 import ntpath
 import pathlib
 import shutil
@@ -31,7 +30,7 @@
 import unittest
 from test import support
 from test.support import (
-    is_apple, import_helper, os_helper, requires_subprocess, threading_helper
+    is_apple, import_helper, os_helper, threading_helper
 )
 
 try:
@@ -820,329 +819,6 @@ def test_path_without_leading_slash(self):
                          self.tempdir_name + "/?hi=1")
 
 
-cgi_file1 = """\
-#!%s
-
-print("Content-type: text/html")
-print()
-print("Hello World")
-"""
-
-cgi_file2 = """\
-#!%s
-import os
-import sys
-import urllib.parse
-
-print("Content-type: text/html")
-print()
-
-content_length = int(os.environ["CONTENT_LENGTH"])
-query_string = sys.stdin.buffer.read(content_length)
-params = {key.decode("utf-8"): val.decode("utf-8")
-            for key, val in urllib.parse.parse_qsl(query_string)}
-
-print("%%s, %%s, %%s" %% (params["spam"], params["eggs"], params["bacon"]))
-"""
-
-cgi_file4 = """\
-#!%s
-import os
-
-print("Content-type: text/html")
-print()
-
-print(os.environ["%s"])
-"""
-
-cgi_file6 = """\
-#!%s
-import os
-
-print("X-ambv: was here")
-print("Content-type: text/html")
-print()
-print("<pre>")
-for k, v in os.environ.items():
-    try:
-        k.encode('ascii')
-        v.encode('ascii')
-    except UnicodeEncodeError:
-        continue  # see: BPO-44647
-    print(f"{k}={v}")
-print("</pre>")
-"""
-
-
-@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
-        "This test can't be run reliably as root (issue #13308).")
-@requires_subprocess()
-class CGIHTTPServerTestCase(BaseTestCase):
-    class request_handler(NoLogRequestHandler, CGIHTTPRequestHandler):
-        _test_case_self = None  # populated by each setUp() method call.
-
-        def __init__(self, *args, **kwargs):
-            with self._test_case_self.assertWarnsRegex(
-                    DeprecationWarning,
-                    r'http\.server\.CGIHTTPRequestHandler'):
-                # This context also happens to catch and silence the
-                # threading DeprecationWarning from os.fork().
-                super().__init__(*args, **kwargs)
-
-    linesep = os.linesep.encode('ascii')
-
-    def setUp(self):
-        self.request_handler._test_case_self = self  # practical, but yuck.
-        BaseTestCase.setUp(self)
-        self.cwd = os.getcwd()
-        self.parent_dir = tempfile.mkdtemp()
-        self.cgi_dir = os.path.join(self.parent_dir, 'cgi-bin')
-        self.cgi_child_dir = os.path.join(self.cgi_dir, 'child-dir')
-        self.sub_dir_1 = os.path.join(self.parent_dir, 'sub')
-        self.sub_dir_2 = os.path.join(self.sub_dir_1, 'dir')
-        self.cgi_dir_in_sub_dir = os.path.join(self.sub_dir_2, 'cgi-bin')
-        os.mkdir(self.cgi_dir)
-        os.mkdir(self.cgi_child_dir)
-        os.mkdir(self.sub_dir_1)
-        os.mkdir(self.sub_dir_2)
-        os.mkdir(self.cgi_dir_in_sub_dir)
-        self.nocgi_path = None
-        self.file1_path = None
-        self.file2_path = None
-        self.file3_path = None
-        self.file4_path = None
-        self.file5_path = None
-
-        # The shebang line should be pure ASCII: use symlink if possible.
-        # See issue #7668.
-        self._pythonexe_symlink = None
-        if os_helper.can_symlink():
-            self.pythonexe = os.path.join(self.parent_dir, 'python')
-            self._pythonexe_symlink = 
support.PythonSymlink(self.pythonexe).__enter__()
-        else:
-            self.pythonexe = sys.executable
-
-        try:
-            # The python executable path is written as the first line of the
-            # CGI Python script. The encoding cookie cannot be used, and so the
-            # path should be encodable to the default script encoding (utf-8)
-            self.pythonexe.encode('utf-8')
-        except UnicodeEncodeError:
-            self.tearDown()
-            self.skipTest("Python executable path is not encodable to utf-8")
-
-        self.nocgi_path = os.path.join(self.parent_dir, 'nocgi.py')
-        with open(self.nocgi_path, 'w', encoding='utf-8') as fp:
-            fp.write(cgi_file1 % self.pythonexe)
-        os.chmod(self.nocgi_path, 0o777)
-
-        self.file1_path = os.path.join(self.cgi_dir, 'file1.py')
-        with open(self.file1_path, 'w', encoding='utf-8') as file1:
-            file1.write(cgi_file1 % self.pythonexe)
-        os.chmod(self.file1_path, 0o777)
-
-        self.file2_path = os.path.join(self.cgi_dir, 'file2.py')
-        with open(self.file2_path, 'w', encoding='utf-8') as file2:
-            file2.write(cgi_file2 % self.pythonexe)
-        os.chmod(self.file2_path, 0o777)
-
-        self.file3_path = os.path.join(self.cgi_child_dir, 'file3.py')
-        with open(self.file3_path, 'w', encoding='utf-8') as file3:
-            file3.write(cgi_file1 % self.pythonexe)
-        os.chmod(self.file3_path, 0o777)
-
-        self.file4_path = os.path.join(self.cgi_dir, 'file4.py')
-        with open(self.file4_path, 'w', encoding='utf-8') as file4:
-            file4.write(cgi_file4 % (self.pythonexe, 'QUERY_STRING'))
-        os.chmod(self.file4_path, 0o777)
-
-        self.file5_path = os.path.join(self.cgi_dir_in_sub_dir, 'file5.py')
-        with open(self.file5_path, 'w', encoding='utf-8') as file5:
-            file5.write(cgi_file1 % self.pythonexe)
-        os.chmod(self.file5_path, 0o777)
-
-        self.file6_path = os.path.join(self.cgi_dir, 'file6.py')
-        with open(self.file6_path, 'w', encoding='utf-8') as file6:
-            file6.write(cgi_file6 % self.pythonexe)
-        os.chmod(self.file6_path, 0o777)
-
-        os.chdir(self.parent_dir)
-
-    def tearDown(self):
-        self.request_handler._test_case_self = None
-        try:
-            os.chdir(self.cwd)
-            if self._pythonexe_symlink:
-                self._pythonexe_symlink.__exit__(None, None, None)
-            if self.nocgi_path:
-                os.remove(self.nocgi_path)
-            if self.file1_path:
-                os.remove(self.file1_path)
-            if self.file2_path:
-                os.remove(self.file2_path)
-            if self.file3_path:
-                os.remove(self.file3_path)
-            if self.file4_path:
-                os.remove(self.file4_path)
-            if self.file5_path:
-                os.remove(self.file5_path)
-            if self.file6_path:
-                os.remove(self.file6_path)
-            os.rmdir(self.cgi_child_dir)
-            os.rmdir(self.cgi_dir)
-            os.rmdir(self.cgi_dir_in_sub_dir)
-            os.rmdir(self.sub_dir_2)
-            os.rmdir(self.sub_dir_1)
-            # The 'gmon.out' file can be written in the current working
-            # directory if C-level code profiling with gprof is enabled.
-            os_helper.unlink(os.path.join(self.parent_dir, 'gmon.out'))
-            os.rmdir(self.parent_dir)
-        finally:
-            BaseTestCase.tearDown(self)
-
-    def test_url_collapse_path(self):
-        # verify tail is the last portion and head is the rest on proper urls
-        test_vectors = {
-            '': '//',
-            '..': IndexError,
-            '/.//..': IndexError,
-            '/': '//',
-            '//': '//',
-            '/\\': '//\\',
-            '/.//': '//',
-            'cgi-bin/file1.py': '/cgi-bin/file1.py',
-            '/cgi-bin/file1.py': '/cgi-bin/file1.py',
-            'a': '//a',
-            '/a': '//a',
-            '//a': '//a',
-            './a': '//a',
-            './C:/': '/C:/',
-            '/a/b': '/a/b',
-            '/a/b/': '/a/b/',
-            '/a/b/.': '/a/b/',
-            '/a/b/c/..': '/a/b/',
-            '/a/b/c/../d': '/a/b/d',
-            '/a/b/c/../d/e/../f': '/a/b/d/f',
-            '/a/b/c/../d/e/../../f': '/a/b/f',
-            '/a/b/c/../d/e/.././././..//f': '/a/b/f',
-            '../a/b/c/../d/e/.././././..//f': IndexError,
-            '/a/b/c/../d/e/../../../f': '/a/f',
-            '/a/b/c/../d/e/../../../../f': '//f',
-            '/a/b/c/../d/e/../../../../../f': IndexError,
-            '/a/b/c/../d/e/../../../../f/..': '//',
-            '/a/b/c/../d/e/../../../../f/../.': '//',
-        }
-        for path, expected in test_vectors.items():
-            if isinstance(expected, type) and issubclass(expected, Exception):
-                self.assertRaises(expected,
-                                  server._url_collapse_path, path)
-            else:
-                actual = server._url_collapse_path(path)
-                self.assertEqual(expected, actual,
-                                 msg='path = %r\nGot:    %r\nWanted: %r' %
-                                 (path, actual, expected))
-
-    def test_headers_and_content(self):
-        res = self.request('/cgi-bin/file1.py')
-        self.assertEqual(
-            (res.read(), res.getheader('Content-type'), res.status),
-            (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK))
-
-    def test_issue19435(self):
-        res = self.request('///////////nocgi.py/../cgi-bin/nothere.sh')
-        self.assertEqual(res.status, HTTPStatus.NOT_FOUND)
-
-    def test_post(self):
-        params = urllib.parse.urlencode(
-            {'spam' : 1, 'eggs' : 'python', 'bacon' : 123456})
-        headers = {'Content-type' : 'application/x-www-form-urlencoded'}
-        res = self.request('/cgi-bin/file2.py', 'POST', params, headers)
-
-        self.assertEqual(res.read(), b'1, python, 123456' + self.linesep)
-
-    def test_invaliduri(self):
-        res = self.request('/cgi-bin/invalid')
-        res.read()
-        self.assertEqual(res.status, HTTPStatus.NOT_FOUND)
-
-    def test_authorization(self):
-        headers = {b'Authorization' : b'Basic ' +
-                   base64.b64encode(b'username:pass')}
-        res = self.request('/cgi-bin/file1.py', 'GET', headers=headers)
-        self.assertEqual(
-            (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
-            (res.read(), res.getheader('Content-type'), res.status))
-
-    def test_no_leading_slash(self):
-        # http://bugs.python.org/issue2254
-        res = self.request('cgi-bin/file1.py')
-        self.assertEqual(
-            (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
-            (res.read(), res.getheader('Content-type'), res.status))
-
-    def test_os_environ_is_not_altered(self):
-        signature = "Test CGI Server"
-        os.environ['SERVER_SOFTWARE'] = signature
-        res = self.request('/cgi-bin/file1.py')
-        self.assertEqual(
-            (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
-            (res.read(), res.getheader('Content-type'), res.status))
-        self.assertEqual(os.environ['SERVER_SOFTWARE'], signature)
-
-    def test_urlquote_decoding_in_cgi_check(self):
-        res = self.request('/cgi-bin%2ffile1.py')
-        self.assertEqual(
-            (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
-            (res.read(), res.getheader('Content-type'), res.status))
-
-    def test_nested_cgi_path_issue21323(self):
-        res = self.request('/cgi-bin/child-dir/file3.py')
-        self.assertEqual(
-            (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
-            (res.read(), res.getheader('Content-type'), res.status))
-
-    def test_query_with_multiple_question_mark(self):
-        res = self.request('/cgi-bin/file4.py?a=b?c=d')
-        self.assertEqual(
-            (b'a=b?c=d' + self.linesep, 'text/html', HTTPStatus.OK),
-            (res.read(), res.getheader('Content-type'), res.status))
-
-    def test_query_with_continuous_slashes(self):
-        res = self.request('/cgi-bin/file4.py?k=aa%2F%2Fbb&//q//p//=//a//b//')
-        self.assertEqual(
-            (b'k=aa%2F%2Fbb&//q//p//=//a//b//' + self.linesep,
-             'text/html', HTTPStatus.OK),
-            (res.read(), res.getheader('Content-type'), res.status))
-
-    def test_cgi_path_in_sub_directories(self):
-        try:
-            CGIHTTPRequestHandler.cgi_directories.append('/sub/dir/cgi-bin')
-            res = self.request('/sub/dir/cgi-bin/file5.py')
-            self.assertEqual(
-                (b'Hello World' + self.linesep, 'text/html', HTTPStatus.OK),
-                (res.read(), res.getheader('Content-type'), res.status))
-        finally:
-            CGIHTTPRequestHandler.cgi_directories.remove('/sub/dir/cgi-bin')
-
-    def test_accept(self):
-        browser_accept = \
-                    
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
-        tests = (
-            ((('Accept', browser_accept),), browser_accept),
-            ((), ''),
-            # Hack case to get two values for the one header
-            ((('Accept', 'text/html'), ('ACCEPT', 'text/plain')),
-               'text/html,text/plain'),
-        )
-        for headers, expected in tests:
-            headers = OrderedDict(headers)
-            with self.subTest(headers):
-                res = self.request('/cgi-bin/file6.py', 'GET', headers=headers)
-                self.assertEqual(http.HTTPStatus.OK, res.status)
-                expected = f"HTTP_ACCEPT={expected}".encode('ascii')
-                self.assertIn(expected, res.read())
-
-
 class SocketlessRequestHandler(SimpleHTTPRequestHandler):
     def __init__(self, directory=None):
         request = mock.Mock()
@@ -1162,6 +838,7 @@ def do_GET(self):
     def log_message(self, format, *args):
         pass
 
+
 class RejectingSocketlessRequestHandler(SocketlessRequestHandler):
     def handle_expect_100(self):
         self.send_error(HTTPStatus.EXPECTATION_FAILED)
diff --git a/Lib/wsgiref/handlers.py b/Lib/wsgiref/handlers.py
index cafe872c7aae9b..9353fb678625b3 100644
--- a/Lib/wsgiref/handlers.py
+++ b/Lib/wsgiref/handlers.py
@@ -69,7 +69,8 @@ def read_environ():
 
                 # Python 3's http.server.CGIHTTPRequestHandler decodes
                 # using the urllib.unquote default of UTF-8, amongst other
-                # issues.
+                # issues. While the CGI handler is removed in 3.15, this
+                # is kept for legacy reasons.
                 elif (
                     software.startswith('simplehttp/')
                     and 'python/3' in software
diff --git a/Misc/NEWS.d/3.13.0a1.rst b/Misc/NEWS.d/3.13.0a1.rst
index 304baf6ac8eea9..0a93cbcea0ffd2 100644
--- a/Misc/NEWS.d/3.13.0a1.rst
+++ b/Misc/NEWS.d/3.13.0a1.rst
@@ -2294,7 +2294,7 @@ superclass. Patch by James Hilton-Balfe
 .. nonce: VksX1D
 .. section: Library
 
-:class:`http.server.CGIHTTPRequestHandler` has been deprecated for removal
+:class:`!http.server.CGIHTTPRequestHandler` has been deprecated for removal
 in 3.15.  Its design is old and the web world has long since moved beyond
 CGI.
 
diff --git 
a/Misc/NEWS.d/next/Library/2025-05-10-11-04-47.gh-issue-133810.03WhnK.rst 
b/Misc/NEWS.d/next/Library/2025-05-10-11-04-47.gh-issue-133810.03WhnK.rst
new file mode 100644
index 00000000000000..4073974e364a1c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-05-10-11-04-47.gh-issue-133810.03WhnK.rst
@@ -0,0 +1,3 @@
+Remove :class:`!http.server.CGIHTTPRequestHandler` and ``--cgi`` flag from the
+:program:`python -m http.server` command-line interface. They were
+deprecated in Python 3.13. Patch by Bénédikt Tran.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to