Bare and percent-encoded pluses in path variables become spaces in match_info #1816
Description
Long story short
When match info is constructed for variable resource routes, '+' characters are converted to spaces. I do not believe this is the expected behavior.
Furthermore, even '%2B' codes are converted to spaces, limiting workarounds.
Note that similar behavior yarl's path parsing, reported in yarl/issues/59, has been corrected in 88799aec. A similar fix is needed in match_info parsing.
Example
In our use case, the version string matched by the route is a semantic version that may include a plus sign followed by the build name, which runs afoul of the eager plus-to-space conversion.
import aiohttp.web
app = aiohttp.web.Application()
async def handler(request):
print(request.url)
print('path: ' + request.url.path)
print('match: ' + request.match_info['version'])
# This assertion does not hold IF(F?) version contains a plus sign
if request.url.path.endswith(request.match_info['version']):
return aiohttp.web.HTTPAccepted()
else:
return aiohttp.web.HTTPExpectationFailed()
app.router.add_get('/resource/{version}', handler)
aiohttp.web.run_app(app, host='127.0.0.1', port=8088)Suppose the handler is reached via a URL('http://server/resources/1.0.0+build'):
curl http://127.0.0.1:8088/resource/1.0.0+build
# Note: this workaround fails too:
curl http://127.0.0.1:8088/resource/1.0.0%2BbuildExpected behavior
Inside the handler, I expect the plus sign to be preserved in the portion of the URL path that has been captured by match info:
request.url.raw_path == '/resources/1.0.0+build'
request.url.path == '/resources/1.0.0+build'
request.match_info['version'] == '1.0.0+build'Actual behavior
Plus sign is preserved in the parsed path, but replaced by space in the match info:
request.url.raw_path == '/resources/1.0.0+build'
request.url.path == '/resources/1.0.0+build'
request.match_info['version'] == '1.0.0 build' # UNEXPECTEDYour environment
Xubuntu 16.04, python3.5, yarl==0.10.0, aiohttp==1.2.0 (but confirmed with aiohttp==2.0.7)
Code inspection / suggested fix
The implementation of DynamicResource._match in aiohttp/web_urldispatcher.py is causing the problem.
aiohttp/aiohttp/web_urldispatcher.py
Line 349 in bcbceb5
def _match(self, path):
match = self._pattern.fullmatch(path)
if match is None:
return None
else:
return {key: unquote(value) for key, value in
match.groupdict().items()}Similarly to 88799aec in yarl, if the unquote(value) is replaced with unquote(value, unsafe='+'), then the issue I demonstrated above is fixed. I am unclear on whether '+:' should be used here as well. Nor am I confident that the same fix is needed in StaticResource (I believe it is) or other resources.
If someone more familiar with the code validates the approach and helps determine which resources need this, I am more than happy to compose and file a PR.