Skip to content

gh-135056: Add a --cors CLI argument to http.server #135057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

aisipos
Copy link

@aisipos aisipos commented Jun 3, 2025

As proposed in #135056, Add a --cors command line argument to the stdlib http.server module, which will add an Access-Control-Allow-Origin: * header to all responses.

Invocation:

python -m http.server --cors

As part of this implementation, add a response_headers argument to SimpleHTTPRequestHandler and HTTPServer, which allows callers to add addition headers to the response. Ideally it would have been possible to just have made a CorsHttpServer class, but a couple of issues made that difficult:

  • The http.server CLI uses more than one HTTP Server class, in order to support TLS/HTTPS. So a single CorsHttpServer child class wouldn't work to support both use cases.
  • Much of the work in specifying HTTP behavior is handled by the various RequestHandler classes. However, the HttpServer classes didn't have an easy way to pass arguments down into the instantiated handlers.

As a result, this PR updates both HTTPServer and SimpleHTTPRequestHandler to accept a response_headers argument, which allows callers to specify an additional set of HTTP headers to pass in the response.

  • HTTPServer now overrides finish_request to pass this new kwarg down to its RequestHandler.
  • SimpleHTTPRequestHandler now accepts a resposnse_headers kwarg, to optionally specify a dictionary of additional headers to send in the response.

Care is taken to not pass the response_headers argument to any instance constructors when not provided, to ensure backwards compatibility. I tried to keep the implementation as short and simple as possible.

With the addition of a response_headers argument, we allow ourselves to have a future possible custom header http argument, such as:

python -m http.server -H 'custom-header: custom-value'

📚 Documentation preview 📚: https://cpython-previews--135057.org.readthedocs.build/

@python-cla-bot
Copy link

python-cla-bot bot commented Jun 3, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@bedevere-app
Copy link

bedevere-app bot commented Jun 3, 2025

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

Add a --cors command line argument to the stdlib http.server module, which will
add an `Access-Control-Allow-Origin: *` header to all responses. As part of this
implementation, add a `response_headers` argument to SimpleHTTPRequestHandler
and HttpServer, which allows callers to add addition headers to the response.
@aisipos aisipos force-pushed the https-server-cors-issue-135056 branch from 3f11652 to 0d02fbe Compare June 3, 2025 05:24
Copy link
Member

@hugovk hugovk left a comment

Choose a reason for hiding this comment

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

(I'd prefer a general headers option, but will comment on the issue or Discourse topic)

@aisipos
Copy link
Author

aisipos commented Jun 3, 2025

test_wsgiref is failing. I'll look into it.

This fixes the breakage to HttpServer as used by wsgiref.
@aisipos
Copy link
Author

aisipos commented Jun 3, 2025

test_wsgiref fixed in a3256fd. This should fix any backwards incompatibility errors erroneously introduced in the first commit.

@donBarbos
Copy link
Contributor

I think it's worth adding to this What's New entry (./Doc/whatsnew/3.15.rst)

@Zheaoli
Copy link
Contributor

Zheaoli commented Jun 6, 2025

For me, I don't think add --cors CLI argument would be a good idea. Base on following reasons:

  1. The CORS policy is a complicated idea. Six response headers are included by the word(If I'm correct). If you set the Access-Control-Allow-Origin, and now the people may need Access-Control-Allow-Methods( Allow for OPTION method). What is the next argument we need to add?
  2. The CLI for http.server is just for a debug target. So we design the CLI as simple as we can. The developer don't need to care about any extra detail when they run just a simple debug server.
  3. if we need CORS policy in the future. I suggest we setup all the header for developer and don't need to add cli for it.

@aisipos aisipos requested a review from hugovk June 6, 2025 23:05
@picnixz picnixz self-requested a review June 7, 2025 23:15
@hugovk
Copy link
Member

hugovk commented Jun 9, 2025

@hugovk
Copy link
Member

hugovk commented Jun 9, 2025

(I'd prefer a general headers option, but will comment on the issue or Discourse topic)

https://discuss.python.org/t/any-interest-in-adding-a-cors-option-to-python-m-http-server/92120/24

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

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

I'm not very fond of how the HTTP server class is growing more and more with more __init__ parameters, but I don't have a better idea for now. Maybe a generic configuration object but this would be an overkill for this class in particular I think.

@@ -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).

@@ -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

@@ -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()

Comment on lines +133 to +138
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)

Comment on lines +708 to +712
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

@@ -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

Comment on lines +1078 to +1082
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)

@@ -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

Comment on lines +832 to +834
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': '*'}
}

server_kwargs = dict(
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):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants