Repository: sanic-org/sanic

Issue:
Unexpected HTTP 405 errors when using shorthand route decorators
Hello Sanic folks,

I've been playing with Sanic recently and stumbled on getting weird 405 HTTP errors on endpoints that have methods defined. This only happens when 1) I use the `app.<http_method>` (e.g. `app.post`) decorator and 2) when I try to query and endpoint with trailing slash and the `strict_slash` option is set to `False` (the default).

I tried to reproduce the error with a simple case, which I have done as a test [here](https://travis-ci.org/bow/sanic/jobs/296017905). As you can see, the builds are all failing with a similar unexpected HTTP status code error.

After digging a little bit deeper, I think I've pinned this down to [this block of code](https://github.com/channelcat/sanic/blob/master/sanic/router.py#L133) which checks for an existing route using only the URI but without checking the HTTP method. The reason this causes the bug and only when `strict_slashes=False` is because when adding the route for the non-first HTTP method with the slash appended, both boolean checks evaluate to `False`, so the route is not added. However on an actual query, a method check is done and since the route for that method is missing, we have a 405.

Anyway, [I added the extra HTTP method check](https://github.com/bow/sanic/commit/7bde17aed54fd6b2ed5b48833ba8adc52fc78d21) on a my own branch and [it seems to make the test case pass without breaking anything else](https://travis-ci.org/bow/sanic/builds/296020569).

Would this fix be useful for Sanic?

By the way, I am opening this as an issue since I'm not sure the code and test I've written fit. I'm open to changing it if you think it would fit the rest of the code better and then submit a proper PR :).


Discussion:

Resolving patch:
diff --git a/sanic/router.py b/sanic/router.py
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -130,8 +130,15 @@ def add(self, uri, methods, handler, host=None, strict_slashes=False,
             return
 
         # Add versions with and without trailing /
+        slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
+        if isinstance(methods, Iterable):
+            _slash_is_missing = all(method in slashed_methods for
+                                    method in methods)
+        else:
+            _slash_is_missing = methods in slashed_methods
+
         slash_is_missing = (
-            not uri[-1] == '/' and not self.routes_all.get(uri + '/', False)
+            not uri[-1] == '/' and not _slash_is_missing
         )
         without_slash_is_missing = (
             uri[-1] == '/' and not


Test patch:
diff --git a/tests/test_routes.py b/tests/test_routes.py
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -44,6 +44,24 @@ def handler(request):
     request, response = app.test_client.post('/get')
     assert response.status == 405
 
+def test_shorthand_routes_multiple():
+    app = Sanic('test_shorthand_routes_multiple')
+
+    @app.get('/get')
+    def get_handler(request):
+        return text('OK')
+
+    @app.options('/get')
+    def options_handler(request):
+        return text('')
+
+    request, response = app.test_client.get('/get/')
+    assert response.status == 200
+    assert response.text == 'OK'
+
+    request, response = app.test_client.options('/get/')
+    assert response.status == 200
+
 def test_route_strict_slash():
     app = Sanic('test_route_strict_slash')
 
@@ -431,7 +449,7 @@ async def handler(request, ws):
         'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
         'Sec-WebSocket-Version': '13'})
     assert response.status == 101
-    
+
     assert results == ['bar', 'bar', None, None]
 
 
@@ -754,6 +772,7 @@ async def handler(request):
     assert response.status == 200
 
     app.remove_route('/test', clean_cache=True)
+    app.remove_route('/test/', clean_cache=True)
 
     request, response = app.test_client.get('/test')
     assert response.status == 404