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:
| Attribute | Description |
|---|---|
self.request.method | HTTP method (GET, POST, etc.) |
self.request.uri | Full request path including query string |
self.request.path | Path without the query string |
self.request.query | Query string portion (a=1&b=2) |
self.request.headers | Request headers dictionary |
self.request.body | Request body as bytes |
self.request.remote_ip | Client IP address |
self.request.files | Uploaded files dictionary (multipart/form-data) |
self.request.arguments | Raw 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 absentRoute 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 serializationfinish() 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 headerTo 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 redirectCookie 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 stringcookie_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.jsUse 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.