45

I've been playing around with Tornado, and I've written some code that doesn't seem very nice.

I'm writing an app to store recipes as an example. These are my handlers:

handlers = [
    (r"/recipes/", RecipeHandler),
    (r"/recipes", RecipeSearchHandler), #so query params can be used to search
]

This lead me to writing this:

class RecipeHandler(RequestHandler):      
    def get(self):
        self.render('recipes/index.html')

class RecipeSearchHandler(RequestHandler):    
    def get(self):
        try:
            name = self.get_argument('name', True)
            self.write(name)
        # will do some searching
        except AssertionError:
            self.write("no params")
            # will probably redirect to /recipes/

Is there a better way to approach these URLs without a try/except? I'd like /recipes and /recipes/ to show the same thing, whereas /recipes?name=something would do a search, and ideally be a different handler.

colinjwebb
  • 4,362
  • 7
  • 31
  • 35

4 Answers4

54

There is a better way for GET requests. There is a demo in the tornado source on github here

# url handler
handlers = [(r"/entry/([^/]+)", EntryHandler),]

class EntryHandler(BaseHandler):
    def get(self, slug):
        entry = self.db.get("SELECT * FROM entries WHERE slug = %s", slug)
        if not entry: raise tornado.web.HTTPError(404)
        self.render("entry.html", entry=entry)

Any "text" that matches the regular expression will be passed to the EntryHandler's get method as slug argument. If the url doesn't match any handler, the user will receive a 404 error.

If you wanted to provide another fallback, you could make the parameter optional

(r"/entry/([^/]*)", EntryHandler),

class EntryHandler(BaseHandler):
    def get(self, slug=None):
        pass

Update:

+1 for the link. However does this URL pattern extend to include more parameters if I wanted to search like this... /recipes?ingredient=chicken&style=indian – colinjameswebb

Yes it does.

handlers = [
     (r'/(\d{4})/(\d{2})/(\d{2})/([a-zA-Z\-0-9\.:,_]+)/?', DetailHandler)
]

class DetailHandler(BaseHandler):
    def get(self, year, month, day, slug):
        pass
fedorqui
  • 275,237
  • 103
  • 548
  • 598
mfussenegger
  • 3,931
  • 23
  • 18
  • 3
    +1 for the link. However does this URL pattern extend to include more parameters if I wanted to search like this... /recipes?ingredient=chicken&style=indian – colinjwebb May 23 '12 at 20:01
45

get_argument allows you to provide a default value:

details=self.get_argument("details", None, True)

If it is provided, then no exception will occur if the argument isn't provided

Casebash
  • 114,675
  • 90
  • 247
  • 350
14

Tornado also has a get_arguments function. It returns a list of arguments with the given name. If not present, it returns an empty list ( [] ). I found it cleaner this way to sanitize your web service inputs instead of try..catch blocks.

Sample:
Assume I have a following URL handler:

(r"/recipe",GetRecipe)

And the request handler:

class GetRecipe(RequestHandler):
    def get(self):
        recipe_id = self.get_arguments("rid")
        if recipe_id == []:
            # Handle me
            self.set_status(400)
            return self.finish("Invalid recipe id")
        self.write({"recipe_id":self.get_argument("rid")})


recipe_id list will also hold the value but I found self.get_argument usage convenient this way.

Now for the results:

curl "http://localhost:8890/recipe" -v

*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8890 (#0)
> GET /recipe HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8890
> Accept: */*
> 
< HTTP/1.1 400 Bad Request
< Content-Length: 17
< Content-Type: text/html; charset=UTF-8
* Server TornadoServer/1.1.1 is not blacklisted
< Server: TornadoServer/1.1.1
< 
* Connection #0 to host localhost left intact
Invalid recipe id

curl "http://localhost:8890/recipe?rid=230" -v
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8890 (#0)
> GET /recipe?rid=230 HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:8890
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Length: 20
< Etag: "d69ecb9086a20160178ade6b13eb0b3959aa13c6"
< Content-Type: text/javascript; charset=UTF-8
* Server TornadoServer/1.1.1 is not blacklisted
< Server: TornadoServer/1.1.1
< 
* Connection #0 to host localhost left intact
{"recipe_id": "230"}

Aravindh
  • 535
  • 3
  • 9
6

If you want to use a more dynamic approach for filtering (instead of a hard coded URL) you can get all the passed URL parameters/arguments using self.request.arguments in the request handler.

class ApiHandler(RequestHandler):
    def get(self, path):
        filters = self.request.arguments
        for k,v in filters.items():
            # Do filtering etc...

See http://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPServerRequest.arguments

frmdstryr
  • 20,142
  • 3
  • 38
  • 32
  • Good hint. Just one remark regarding Unicode Strings. The documentation says: Names are of type str, while arguments are byte strings. Note that this is different from RequestHandler.get_argument, which returns argument values as unicode strings. – klaas Mar 19 '17 at 22:11