Search code examples
pythontornado

Can one handler handle both post and get with different URLs in tornado


Can one handler handle both post and get requests (with slightly different URLs) in tornado?

For example I'd like to have:

  • /app/user (POST) to create a new user using the data from request body
  • /app/user/<user_id> (GET) to return the user with the given id

Currently I have two handlers GetUser & PostUser and two routes.


Solution

  • Yes, you can do this, however, all quick solutions that I've seen so far always had some minor inconvenience in the handling of positional arguments. Take a look at this example:

    import tornado.ioloop
    import tornado.web
    
    class UserHandler(tornado.web.RequestHandler):
        def get(self, __):
            self.write("Get request")
    
        def post(self, user_id):
            self.write(f"Post request for user id {user_id}")
    
    app = tornado.web.Application([
        (r"/app/user/?(\d+)?", UserHandler),
    ])
    
    app.listen(8000)
    tornado.ioloop.IOLoop.current().start()
    

    This will build a routing rule that matches both url patterns and assigns the UserHandler. The capturing group in the url is optional, so None might be passed as user_id when a POST request is sent to /app/user. With this approach, tornado will always call the get method with the argument for the second group in the regular expression. Depending on your project, you might want to do more extensive error checking (check the given argument, and if it is not None, send a 405 Method Not Allowed, or whatever is appropriate in your case)


    It's also possible to do it by adding two rules with the same handler:

    import tornado.ioloop
    import tornado.web
    
    class UserHandler(tornado.web.RequestHandler):
        def get(self):
            self.write("Get request")
    
        def post(self, user_id):
            self.write(f"Post request for user id {user_id}")
    
    app = tornado.web.Application([
        (r"/app/user", UserHandler),
        (r"/app/user/(\d+)", UserHandler),
    ])
    app.listen(8000)
    tornado.ioloop.IOLoop.current().start()
    

    Keep in mind that both routes will be matched for POST and GET requests by tornado, leading to this inconvenience:

    • If someone sends a POST request to /app/user, it will throw a TypeError: post() missing 1 required positional argument: 'user_id'.
    • If someone sends a GET request to /app/user/123, it will throw a TypeError: get() takes 1 positional argument but 2 were given

    You can work around these, but I think the first solution solves this in a cleaner way.


    Edit: Of course, you can also implement your own Matcher class that allows not only to differentiate between urls, but also request methods, by subclassing tornado.routing.PathMatches. This is a little more code though. The main disadvantage here is that for valid routes, the server will return 404 responses where it should be 405s since the route/"resource" itself is valid, it's just the wrong method.

    import tornado.ioloop
    import tornado.web
    import tornado.routing
    
    class MethodAndPathMatch(tornado.routing.PathMatches):
        def __init__(self, method, path_pattern):
            super().__init__(path_pattern)
            self.method = method
    
        def match(self, request):
            if request.method != self.method:
                return None
    
            return super().match(request)
    
    class UserHandler(tornado.web.RequestHandler):
        def get(self):
            self.write("Get request")
    
        def post(self, user_id):
            self.write(f"Post request for user id {user_id}")
    
    app = tornado.web.Application([
        (MethodAndPathMatch("GET", r"/app/user"), UserHandler),
        (MethodAndPathMatch("POST", r"/app/user/(\d+)"), UserHandler),
    ])
    
    app.listen(8000)
    tornado.ioloop.IOLoop.current().start()
    

    cURL calls for testing:

    $ curl -w '\n' -X POST localhost:8000/app/user
    $ curl -w '\n' -X POST localhost:8000/app/user/123
    $ curl -w '\n' -X GET localhost:8000/app/user
    $ curl -w '\n' -X GET localhost:8000/app/user/123