Add scheme to the request

This commit is contained in:
Miguel Grinberg
2025-11-26 00:42:26 +00:00
parent 3b77e5d6a1
commit 1c7020ca1a
5 changed files with 47 additions and 10 deletions

View File

@@ -123,7 +123,8 @@ class Microdot(BaseMicrodot):
headers, headers,
body=body, body=body,
stream=stream, stream=stream,
sock=(receive, send)) sock=(receive, send),
scheme=scope.get('scheme'))
req.asgi_scope = scope req.asgi_scope = scope
res = await self.dispatch_request(req) res = await self.dispatch_request(req)

View File

@@ -321,14 +321,17 @@ class Request:
def __init__(self, app, client_addr, method, url, http_version, headers, def __init__(self, app, client_addr, method, url, http_version, headers,
body=None, stream=None, sock=None, url_prefix='', body=None, stream=None, sock=None, url_prefix='',
subapp=None): subapp=None, scheme=None):
#: The application instance to which this request belongs. #: The application instance to which this request belongs.
self.app = app self.app = app
#: The address of the client, as a tuple (host, port). #: The address of the client, as a tuple (host, port).
self.client_addr = client_addr self.client_addr = client_addr
#: The HTTP method of the request. #: The HTTP method of the request.
self.method = method self.method = method
#: The request URL, including the path and query string. #: The scheme of the request, either `http` or `https`.
self.scheme = scheme or 'http'
#: The request URL, including the path and query string, but not the
#: scheme or the host, which is available in the ``Host`` header.
self.url = url self.url = url
#: The URL prefix, if the endpoint comes from a mounted #: The URL prefix, if the endpoint comes from a mounted
#: sub-application, or else ''. #: sub-application, or else ''.
@@ -379,7 +382,8 @@ class Request:
self.after_request_handlers = [] self.after_request_handlers = []
@staticmethod @staticmethod
async def create(app, client_reader, client_writer, client_addr): async def create(app, client_reader, client_writer, client_addr,
scheme=None):
"""Create a request object. """Create a request object.
:param app: The Microdot application instance. :param app: The Microdot application instance.
@@ -388,6 +392,7 @@ class Request:
:param client_writer: An output stream where the response data can be :param client_writer: An output stream where the response data can be
written. written.
:param client_addr: The address of the client, as a tuple. :param client_addr: The address of the client, as a tuple.
:param scheme: The scheme of the request, either 'http' or 'https'.
This method is a coroutine. It returns a newly created ``Request`` This method is a coroutine. It returns a newly created ``Request``
object. object.
@@ -424,7 +429,7 @@ class Request:
return Request(app, client_addr, method, url, http_version, headers, return Request(app, client_addr, method, url, http_version, headers,
body=body, stream=stream, body=body, stream=stream,
sock=(client_reader, client_writer)) sock=(client_reader, client_writer), scheme=scheme)
def _parse_urlencoded(self, urlencoded): def _parse_urlencoded(self, urlencoded):
data = MultiDict() data = MultiDict()
@@ -949,6 +954,7 @@ class Microdot:
self.after_error_request_handlers = [] self.after_error_request_handlers = []
self.error_handlers = {} self.error_handlers = {}
self.options_handler = self.default_options_handler self.options_handler = self.default_options_handler
self.ssl = False
self.debug = False self.debug = False
self.server = None self.server = None
@@ -1242,6 +1248,7 @@ class Microdot:
asyncio.run(main()) asyncio.run(main())
""" """
self.ssl = ssl
self.debug = debug self.debug = debug
async def serve(reader, writer): async def serve(reader, writer):

View File

@@ -117,6 +117,8 @@ class TestClient:
:param app: The Microdot application instance. :param app: The Microdot application instance.
:param cookies: A dictionary of cookies to use when sending requests to the :param cookies: A dictionary of cookies to use when sending requests to the
application. application.
:param scheme: The scheme to use for requests, either 'http' or 'https'.
:param host: The host to use for requests.
The following example shows how to create a test client for an application The following example shows how to create a test client for an application
and send a test request:: and send a test request::
@@ -137,9 +139,11 @@ class TestClient:
""" """
__test__ = False # remove this class from pytest's test collection __test__ = False # remove this class from pytest's test collection
def __init__(self, app, cookies=None): def __init__(self, app, cookies=None, scheme=None, host=None):
self.app = app self.app = app
self.cookies = cookies or {} self.cookies = cookies or {}
self.scheme = scheme
self.host = host or 'example.com:1234'
def _process_body(self, body, headers): def _process_body(self, body, headers):
if body is None: if body is None:
@@ -152,8 +156,6 @@ class TestClient:
body = body.encode() body = body.encode()
if body and 'Content-Length' not in headers: if body and 'Content-Length' not in headers:
headers['Content-Length'] = str(len(body)) headers['Content-Length'] = str(len(body))
if 'Host' not in headers: # pragma: no branch
headers['Host'] = 'example.com:1234'
return body, headers return body, headers
def _process_cookies(self, path, headers): def _process_cookies(self, path, headers):
@@ -176,6 +178,8 @@ class TestClient:
def _render_request(self, method, path, headers, body): def _render_request(self, method, path, headers, body):
request_bytes = '{method} {path} HTTP/1.0\n'.format( request_bytes = '{method} {path} HTTP/1.0\n'.format(
method=method, path=path) method=method, path=path)
if 'Host' not in headers: # pragma: no branch
request_bytes += 'Host: {host}\n'.format(host=self.host)
for header, value in headers.items(): for header, value in headers.items():
request_bytes += '{header}: {value}\n'.format( request_bytes += '{header}: {value}\n'.format(
header=header, value=value) header=header, value=value)
@@ -236,7 +240,7 @@ class TestClient:
writer = AsyncBytesIO(b'') writer = AsyncBytesIO(b'')
req = await Request.create(self.app, reader, writer, req = await Request.create(self.app, reader, writer,
('127.0.0.1', 1234)) ('127.0.0.1', 1234), scheme=self.scheme)
res = await self.app.dispatch_request(req) res = await self.app.dispatch_request(req)
if res == Response.already_handled: if res == Response.already_handled:
return TestResponse() return TestResponse()

View File

@@ -96,7 +96,8 @@ class Microdot(BaseMicrodot):
headers, headers,
body=body, body=body,
stream=stream, stream=stream,
sock=sock) sock=sock,
scheme=environ.get('wsgi.url_scheme'))
req.environ = environ req.environ = environ
res = self.loop.run_until_complete(self.dispatch_request(req)) res = self.loop.run_until_complete(self.dispatch_request(req))

View File

@@ -180,6 +180,30 @@ class TestMicrodot(unittest.TestCase):
'text/plain; charset=UTF-8') 'text/plain; charset=UTF-8')
self.assertEqual(res.text, method) self.assertEqual(res.text, method)
def test_http_host(self):
app = Microdot()
@app.route('/')
def index(req):
return req.scheme + "://" + req.headers['Host']
client = TestClient(app, host='foo.com')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'http://foo.com')
def test_https_host(self):
app = Microdot()
@app.route('/')
def index(req):
return req.scheme + "://" + req.headers['Host']
client = TestClient(app, scheme='https', host='foo.com')
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, 'https://foo.com')
def test_headers(self): def test_headers(self):
app = Microdot() app = Microdot()