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 idCurrently I have two handlers GetUser
& PostUser
and two routes.
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:
/app/user
, it will throw a TypeError: post() missing 1 required positional argument: 'user_id'
./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