Skip to content

Request and Response

Tornado’s RequestHandler encapsulates the complete HTTP request/response lifecycle. This article details how to read request parameters (query strings, request bodies, route parameters), construct various responses (plain text, JSON, redirects, errors), manipulate cookies, and serve static files.

The Request Object

Each Handler instance accesses the current request via self.request, which is of type tornado.httputil.HTTPServerRequest:

AttributeDescription
self.request.methodHTTP method (GET, POST, etc.)
self.request.uriFull request path including query string
self.request.pathPath without the query string
self.request.queryQuery string portion (a=1&b=2)
self.request.headersRequest headers dictionary
self.request.bodyRequest body as bytes
self.request.remote_ipClient IP address
self.request.filesUploaded files dictionary (multipart/form-data)
self.request.argumentsRaw dictionary of all request parameters

Reading Request Parameters

Query String Parameters

get_argument and get_arguments handle both query string parameters (?key=value) and form body parameters (application/x-www-form-urlencoded):

class SearchHandler(tornado.web.RequestHandler):
    def get(self):
        # Get a single parameter; returns default if parameter is missing
        keyword = self.get_argument("q", default="")
        page = self.get_argument("page", default="1")

        # Get multiple values for the same parameter name (e.g. ?tag=python&tag=web); returns a list
        tags = self.get_arguments("tag")   # Returns empty list if not present

        self.write(f"Search: {keyword}, Page: {page}, Tags: {tags}")

If a parameter is required and no default is provided, a missing parameter raises 400 Bad Request:

user_id = self.get_argument("id")  # Raises MissingArgumentError if absent

Route Parameters

Capture groups in route regular expressions are passed as positional arguments to the HTTP method:

# Positional parameters: passed in capture order
app = Application([
    (r"/article/(\d+)/(\w+)", ArticleHandler),
])

class ArticleHandler(tornado.web.RequestHandler):
    def get(self, article_id, slug):
        self.write(f"Article ID: {article_id}, Slug: {slug}")

Named capture groups ((?P<name>pattern)) are also passed positionally, but improve readability:

app = Application([
    (r"/user/(?P<user_id>\d+)", UserHandler),
])

class UserHandler(tornado.web.RequestHandler):
    def get(self, user_id):   # Parameter name must match capture group name
        self.write(f"User ID: {user_id}")

Request Body Parameters

POST form body (application/x-www-form-urlencoded):

class LoginHandler(tornado.web.RequestHandler):
    def post(self):
        username = self.get_body_argument("username", default="")
        password = self.get_body_argument("password", default="")

JSON request body (application/json) must be parsed manually:

import json

class ApiHandler(tornado.web.RequestHandler):
    def post(self):
        data = json.loads(self.request.body)
        name = data.get("name")
        self.write({"received": name})

In practice, JSON is often parsed uniformly in prepare(). See Advanced Views and Routing for details.

Constructing Responses

write and finish

write(chunk) writes content to the output buffer, accepting strings or byte strings. When passed a dictionary, it automatically serializes to JSON and sets Content-Type: application/json:

self.write("Plain text response")
self.write(b"<html>...</html>")
self.write({"code": 0, "message": "ok"})  # Auto JSON serialization

finish() explicitly ends the request and flushes the buffer. If not called manually, Tornado calls it automatically after the Handler method returns.

Setting Response Headers

self.set_header("Content-Type", "application/json; charset=UTF-8")
self.add_header("X-Custom", "value")   # Add a header (does not overwrite existing)
self.clear_header("X-Powered-By")      # Remove a specific header

To set default response headers in bulk, override set_default_headers():

class BaseHandler(tornado.web.RequestHandler):
    def set_default_headers(self):
        self.set_header("Access-Control-Allow-Origin", "*")
        self.set_header("Content-Type", "application/json; charset=UTF-8")

Status Codes and Error Responses

# Set status code
self.set_status(201)

# Send an error (calls write_error and terminates the request)
self.send_error(404, reason="Not Found")

# Custom error page (override write_error)
def write_error(self, status_code, **kwargs):
    self.set_header("Content-Type", "application/json")
    self.write({"error": status_code, "message": self._reason})

Redirects

self.redirect("/new-path")              # 302 temporary redirect
self.redirect("/new-path", permanent=True)  # 301 permanent redirect

Cookie Operations

Plain Cookies

# Set a cookie
self.set_cookie("username", "alice")
self.set_cookie(
    "session",
    "abc123",
    expires_days=7,          # Expiry in days
    httponly=True,           # Prevent JavaScript access
    secure=True,             # HTTPS only
    samesite="Lax",          # SameSite policy
)

# Read a cookie (returns None or default if not present)
username = self.get_cookie("username", default="")

# Delete a single cookie
self.clear_cookie("username")

# Clear all cookies
self.clear_all_cookies()

Signed Cookies

Signed cookies use HMAC signatures to prevent client-side tampering. You must configure cookie_secret in the Application settings:

# Configure the secret key in settings
settings = {"cookie_secret": "a long random string; read from env vars in production"}

# Write a signed cookie
self.set_secure_cookie("user_id", "42")

# Read a signed cookie (returns None if signature verification fails)
user_id = self.get_secure_cookie("user_id")
if user_id:
    user_id = user_id.decode()   # Returns bytes; decode to string
cookie_secret is a security-sensitive configuration value — do not hard-code it in source code. Use os.environ.get("COOKIE_SECRET") to read it from environment variables and inject it at deployment time.

Static Files

Configure static_path in settings, and Tornado will automatically serve URLs beginning with /static/:

settings = {
    "static_path": "static",          # Root directory for static files
    "static_url_prefix": "/static/",  # URL prefix (defaults to /static/)
}

Example directory structure:

project/
├── app.py
└── static/
    ├── css/
    │   └── main.css
    └── js/
        └── app.js

Use static_url() in templates to generate versioned URLs (for cache busting):

<link rel="stylesheet" href="{{ static_url('css/main.css') }}">
<script src="{{ static_url('js/app.js') }}"></script>

Template Rendering

self.render() loads a template file and renders it as an HTML response:

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render(
            "index.html",
            title="Home",
            items=["Python", "Tornado", "Async"],
        )

Templates must be located in the settings["template_path"] directory. For template syntax, see Template Engine.

Last updated on