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

Changed files:
sanic/router.py, tests/test_routes.py

diff --git a/sanic/router.py b/sanic/router.py
index 21c98766..b601622c 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -130,8 +130,15 @@ class Router:
             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
diff --git a/tests/test_routes.py b/tests/test_routes.py
index b4ed7cf3..b6b62283 100644
--- a/tests/test_routes.py
+++ b/tests/test_routes.py
@@ -44,6 +44,24 @@ def test_shorthand_routes_get():
     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 @@ def test_websocket_route_with_subprotocols():
         'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
         'Sec-WebSocket-Version': '13'})
     assert response.status == 101
-    
+
     assert results == ['bar', 'bar', None, None]
 
 
@@ -754,6 +772,7 @@ def test_remove_route_without_clean_cache():
     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