Request Handling¶
Every route handler in Cello receives a Request object as its first argument. The Request is constructed in Rust with zero-copy optimizations and exposes a clean Python API for accessing path parameters, headers, query strings, and request bodies.
The Request Object¶
@app.get("/example")
def handler(request):
print(request.method) # "GET"
print(request.path) # "/example"
print(request.params) # {} (path parameters)
print(request.query_params) # {} (query string parameters)
print(request.headers) # {"host": "localhost:8000", ...}
Properties¶
| Property | Type | Description |
|---|---|---|
method | str | HTTP method (GET, POST, etc.) |
path | str | Request path (e.g., /users/123) |
params | dict[str, str] | Path parameters extracted from the route |
query_params | dict[str, str] | Query string parameters |
query | dict[str, str] | Alias for query_params |
headers | dict[str, str] | Request headers (lowercase keys) |
Path Parameters¶
Path parameters are extracted automatically by the Rust radix-tree router and are available via request.params:
@app.get("/users/{user_id}/posts/{post_id}")
def get_post(request):
user_id = request.params["user_id"]
post_id = request.params["post_id"]
return {"user_id": user_id, "post_id": post_id}
Note
Path parameters are always strings. Cast them explicitly if you need integers or other types.
@app.get("/items/{id}")
def get_item(request):
item_id = int(request.params["id"])
return {"id": item_id}
Query Parameters¶
Access query string values from request.query_params or the request.query alias:
# GET /search?q=python&limit=10&sort=desc
@app.get("/search")
def search(request):
query = request.query_params.get("q", "")
limit = int(request.query_params.get("limit", "10"))
sort = request.query.get("sort", "asc")
return {"query": query, "limit": limit, "sort": sort}
Using get_query_param()¶
For convenience, the get_query_param() method provides default value support:
@app.get("/search")
def search(request):
query = request.get_query_param("q", "")
page = request.get_query_param("page", "1")
return {"query": query, "page": int(page)}
Request Headers¶
Headers are available as a dictionary with lowercase keys:
@app.get("/info")
def info(request):
content_type = request.headers.get("content-type", "")
auth = request.headers.get("authorization", "")
user_agent = request.headers.get("user-agent", "")
return {
"content_type": content_type,
"auth_present": bool(auth),
"user_agent": user_agent
}
Use get_header() for a convenient accessor with default values:
@app.get("/versioned")
def versioned(request):
version = request.get_header("API-Version", "1")
return {"api_version": version}
Request Body¶
Cello uses lazy body parsing -- the body is only parsed when you access it, and the result is cached for subsequent calls within the same request. All parsing is performed in Rust using SIMD-accelerated JSON.
JSON Body¶
@app.post("/users")
def create_user(request):
data = request.json() # Parsed via SIMD JSON in Rust, cached
return {"created": data["name"]}
Performance
request.json() uses SIMD-accelerated JSON parsing in Rust, which is up to 10x faster than Python's built-in json module. Results are cached so repeated calls are free.
Text Body¶
@app.post("/notes")
def create_note(request):
body_text = request.text() # Body as UTF-8 string, cached
return {"length": len(body_text)}
Raw Bytes¶
@app.post("/upload")
def upload(request):
raw = request.body() # Raw bytes
return {"size": len(raw)}
Form Data¶
@app.post("/login")
def login(request):
form = request.form() # URL-encoded form data as dict
username = form.get("username", "")
password = form.get("password", "")
return {"user": username}
Content Type Detection¶
Check the content type of incoming requests:
@app.post("/data")
def handle_data(request):
if request.is_json():
data = request.json()
return {"format": "json", "data": data}
elif request.is_form():
data = request.form()
return {"format": "form", "data": data}
elif request.is_multipart():
return {"format": "multipart"}
else:
return {"format": "unknown", "content_type": request.content_type()}
| Method | Checks for |
|---|---|
is_json() | application/json |
is_form() | application/x-www-form-urlencoded |
is_multipart() | multipart/form-data |
content_type() | Returns the raw Content-Type header value |
Request Context¶
Middleware can attach data to the request context, which is then available in handlers:
# After JWT middleware runs, claims are in context
@app.get("/profile")
def profile(request):
claims = request.context.get("jwt_claims")
user_id = claims["sub"]
return {"user_id": user_id}
# After session middleware runs, session data is available
@app.get("/dashboard")
def dashboard(request):
user = request.session.get("user_id")
return {"user": user}
Lazy Parsing Internals¶
Cello's lazy body parsing ensures minimal overhead:
First call to request.json()
→ Parse body bytes with SIMD JSON (Rust)
→ Cache the result in an RwLock
Second call to request.json()
→ Return cached result (no re-parsing)
| Operation | Overhead | Notes |
|---|---|---|
json() first call | ~1us/KB | SIMD-accelerated |
json() subsequent | ~10ns | Cached result |
text() first call | ~100ns | UTF-8 validation |
form() first call | ~500ns | URL decoding |
body() | ~0ns | Zero-copy bytes |
Warning
Do not parse JSON manually in Python using the json module. Always use request.json() to benefit from Rust's SIMD acceleration and caching.
Next Steps¶
- Response Types - Building HTTP responses
- Routing - Route definition and parameters
- Blueprints - Organizing routes into groups