Merge pull request #512 from subyraman/fix-url-building
Fix `request.url` and other url properties
This commit is contained in:
commit
88bf78213f
|
@ -85,6 +85,12 @@ The following variables are accessible as properties on `Request` objects:
|
||||||
return json({'status': 'production'})
|
return json({'status': 'production'})
|
||||||
|
|
||||||
```
|
```
|
||||||
|
- `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar`
|
||||||
|
- `scheme`: The URL scheme associated with the request: `http` or `https`
|
||||||
|
- `host`: The host associated with the request: `localhost:8080`
|
||||||
|
- `path`: The path of the request: `/posts/1/`
|
||||||
|
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
|
||||||
|
|
||||||
|
|
||||||
## Accessing values using `get` and `getlist`
|
## Accessing values using `get` and `getlist`
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ TRACEBACK_WRAPPER_HTML = '''
|
||||||
{frame_html}
|
{frame_html}
|
||||||
<p class="summary">
|
<p class="summary">
|
||||||
<b>{exc_name}: {exc_value}</b>
|
<b>{exc_name}: {exc_value}</b>
|
||||||
while handling uri <code>{uri}</code>
|
while handling path <code>{path}</code>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -34,7 +34,7 @@ class ErrorHandler:
|
||||||
exc_name=exc_type.__name__,
|
exc_name=exc_type.__name__,
|
||||||
exc_value=exc_value,
|
exc_value=exc_value,
|
||||||
frame_html=''.join(frame_html),
|
frame_html=''.join(frame_html),
|
||||||
uri=request.url)
|
path=request.path)
|
||||||
|
|
||||||
def add(self, exception, handler):
|
def add(self, exception, handler):
|
||||||
self.handlers[exception] = handler
|
self.handlers[exception] = handler
|
||||||
|
|
|
@ -2,7 +2,7 @@ from cgi import parse_header
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from http.cookies import SimpleCookie
|
from http.cookies import SimpleCookie
|
||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs, urlunparse
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ujson import loads as json_loads
|
from ujson import loads as json_loads
|
||||||
|
@ -36,24 +36,20 @@ class RequestParameters(dict):
|
||||||
class Request(dict):
|
class Request(dict):
|
||||||
"""Properties of an HTTP request such as URL, headers, etc."""
|
"""Properties of an HTTP request such as URL, headers, etc."""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport',
|
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||||
'query_string', 'body',
|
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
'_ip', '_parsed_url',
|
||||||
'_ip',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, url_bytes, headers, version, method, transport):
|
def __init__(self, url_bytes, headers, version, method, transport):
|
||||||
# TODO: Content-Encoding detection
|
# TODO: Content-Encoding detection
|
||||||
url_parsed = parse_url(url_bytes)
|
self._parsed_url = parse_url(url_bytes)
|
||||||
self.app = None
|
self.app = None
|
||||||
self.url = url_parsed.path.decode('utf-8')
|
|
||||||
self.headers = headers
|
self.headers = headers
|
||||||
self.version = version
|
self.version = version
|
||||||
self.method = method
|
self.method = method
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self.query_string = None
|
|
||||||
if url_parsed.query:
|
|
||||||
self.query_string = url_parsed.query.decode('utf-8')
|
|
||||||
|
|
||||||
# Init but do not inhale
|
# Init but do not inhale
|
||||||
self.body = []
|
self.body = []
|
||||||
|
@ -144,6 +140,40 @@ class Request(dict):
|
||||||
self._ip = self.transport.get_extra_info('peername')
|
self._ip = self.transport.get_extra_info('peername')
|
||||||
return self._ip
|
return self._ip
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scheme(self):
|
||||||
|
if self.transport.get_extra_info('sslcontext'):
|
||||||
|
return 'https'
|
||||||
|
|
||||||
|
return 'http'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host(self):
|
||||||
|
# it appears that httptools doesn't return the host
|
||||||
|
# so pull it from the headers
|
||||||
|
return self.headers.get('Host', '')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self):
|
||||||
|
return self._parsed_url.path.decode('utf-8')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def query_string(self):
|
||||||
|
if self._parsed_url.query:
|
||||||
|
return self._parsed_url.query.decode('utf-8')
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return urlunparse((
|
||||||
|
self.scheme,
|
||||||
|
self.host,
|
||||||
|
self.path,
|
||||||
|
None,
|
||||||
|
self.query_string,
|
||||||
|
None))
|
||||||
|
|
||||||
|
|
||||||
File = namedtuple('File', ['type', 'body', 'name'])
|
File = namedtuple('File', ['type', 'body', 'name'])
|
||||||
|
|
||||||
|
|
|
@ -281,14 +281,14 @@ class Router:
|
||||||
"""
|
"""
|
||||||
# No virtual hosts specified; default behavior
|
# No virtual hosts specified; default behavior
|
||||||
if not self.hosts:
|
if not self.hosts:
|
||||||
return self._get(request.url, request.method, '')
|
return self._get(request.path, request.method, '')
|
||||||
# virtual hosts specified; try to match route to the host header
|
# virtual hosts specified; try to match route to the host header
|
||||||
try:
|
try:
|
||||||
return self._get(request.url, request.method,
|
return self._get(request.path, request.method,
|
||||||
request.headers.get("Host", ''))
|
request.headers.get("Host", ''))
|
||||||
# try default hosts
|
# try default hosts
|
||||||
except NotFound:
|
except NotFound:
|
||||||
return self._get(request.url, request.method, '')
|
return self._get(request.path, request.method, '')
|
||||||
|
|
||||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||||
def _get(self, url, method, host):
|
def _get(self, url, method, host):
|
||||||
|
|
|
@ -17,7 +17,9 @@ class SanicTestClient:
|
||||||
host=HOST, port=PORT, uri=uri)
|
host=HOST, port=PORT, uri=uri)
|
||||||
|
|
||||||
log.info(url)
|
log.info(url)
|
||||||
async with aiohttp.ClientSession(cookies=cookies) as session:
|
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||||
|
async with aiohttp.ClientSession(
|
||||||
|
cookies=cookies, connector=conn) as session:
|
||||||
async with getattr(
|
async with getattr(
|
||||||
session, method.lower())(url, *args, **kwargs) as response:
|
session, method.lower())(url, *args, **kwargs) as response:
|
||||||
response.text = await response.text()
|
response.text = await response.text()
|
||||||
|
|
22
tests/certs/selfsigned.cert
Normal file
22
tests/certs/selfsigned.cert
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||||
|
BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
|
||||||
|
aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF
|
||||||
|
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
|
||||||
|
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||||
|
CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh
|
||||||
|
V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE
|
||||||
|
vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3
|
||||||
|
h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1
|
||||||
|
w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf
|
||||||
|
qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix
|
||||||
|
9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4
|
||||||
|
NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV
|
||||||
|
BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF
|
||||||
|
MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7
|
||||||
|
ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ
|
||||||
|
7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj
|
||||||
|
teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+
|
||||||
|
mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5
|
||||||
|
zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8=
|
||||||
|
-----END CERTIFICATE-----
|
27
tests/certs/selfsigned.key
Normal file
27
tests/certs/selfsigned.key
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7
|
||||||
|
mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw
|
||||||
|
dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49
|
||||||
|
IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ
|
||||||
|
8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ
|
||||||
|
PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo
|
||||||
|
7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6
|
||||||
|
VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h
|
||||||
|
4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5
|
||||||
|
th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4
|
||||||
|
56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW
|
||||||
|
TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs
|
||||||
|
80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK
|
||||||
|
gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs
|
||||||
|
WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g
|
||||||
|
vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay
|
||||||
|
mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w
|
||||||
|
bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm
|
||||||
|
fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8
|
||||||
|
0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB
|
||||||
|
8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6
|
||||||
|
vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g
|
||||||
|
mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL
|
||||||
|
sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2
|
||||||
|
gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg==
|
||||||
|
-----END RSA PRIVATE KEY-----
|
|
@ -75,7 +75,7 @@ def test_html_traceback_output_in_debug_mode():
|
||||||
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
summary_text = " ".join(soup.select('.summary')[0].text.split())
|
||||||
assert (
|
assert (
|
||||||
"NameError: name 'bar' "
|
"NameError: name 'bar' "
|
||||||
"is not defined while handling uri /4") == summary_text
|
"is not defined while handling path /4") == summary_text
|
||||||
|
|
||||||
|
|
||||||
def test_inherited_exception_handler():
|
def test_inherited_exception_handler():
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
from json import loads as json_loads, dumps as json_dumps
|
from json import loads as json_loads, dumps as json_dumps
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.exceptions import ServerError
|
from sanic.exceptions import ServerError
|
||||||
from sanic.response import json, text, redirect
|
from sanic.response import json, text
|
||||||
|
|
||||||
|
from sanic.testing import HOST, PORT
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
@ -200,3 +205,61 @@ def test_post_form_multipart_form_data():
|
||||||
request, response = app.test_client.post(data=payload, headers=headers)
|
request, response = app.test_client.post(data=payload, headers=headers)
|
||||||
|
|
||||||
assert request.form.get('test') == 'OK'
|
assert request.form.get('test') == 'OK'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'path,query,expected_url', [
|
||||||
|
('/foo', '', 'http://{}:{}/foo'),
|
||||||
|
('/bar/baz', '', 'http://{}:{}/bar/baz'),
|
||||||
|
('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1')
|
||||||
|
])
|
||||||
|
def test_url_attributes_no_ssl(path, query, expected_url):
|
||||||
|
app = Sanic('test_url_attrs_no_ssl')
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, path)
|
||||||
|
|
||||||
|
request, response = app.test_client.get(path + '?{}'.format(query))
|
||||||
|
assert request.url == expected_url.format(HOST, PORT)
|
||||||
|
|
||||||
|
parsed = urlparse(request.url)
|
||||||
|
|
||||||
|
assert parsed.scheme == request.scheme
|
||||||
|
assert parsed.path == request.path
|
||||||
|
assert parsed.query == request.query_string
|
||||||
|
assert parsed.netloc == request.host
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'path,query,expected_url', [
|
||||||
|
('/foo', '', 'https://{}:{}/foo'),
|
||||||
|
('/bar/baz', '', 'https://{}:{}/bar/baz'),
|
||||||
|
('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1')
|
||||||
|
])
|
||||||
|
def test_url_attributes_with_ssl(path, query, expected_url):
|
||||||
|
app = Sanic('test_url_attrs_with_ssl')
|
||||||
|
|
||||||
|
current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
|
||||||
|
context.load_cert_chain(
|
||||||
|
os.path.join(current_dir, 'certs/selfsigned.cert'),
|
||||||
|
keyfile=os.path.join(current_dir, 'certs/selfsigned.key'))
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, path)
|
||||||
|
|
||||||
|
request, response = app.test_client.get(
|
||||||
|
'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query),
|
||||||
|
server_kwargs={'ssl': context})
|
||||||
|
assert request.url == expected_url.format(HOST, PORT)
|
||||||
|
|
||||||
|
parsed = urlparse(request.url)
|
||||||
|
|
||||||
|
assert parsed.scheme == request.scheme
|
||||||
|
assert parsed.path == request.path
|
||||||
|
assert parsed.query == request.query_string
|
||||||
|
assert parsed.netloc == request.host
|
||||||
|
|
Loading…
Reference in New Issue
Block a user