Request & Response
Access incoming request data and control outgoing responses through thread-safe proxy objects.
The Request Object
The request object is a thread-local proxy to the current BaseRequest. Import it from lcore:
from lcore import request
@app.route('/info')
def info():
return {
'method': request.method, # 'GET', 'POST', etc.
'path': request.path, # '/info'
'url': request.url, # 'http://localhost:8080/info?q=test'
'fullpath': request.fullpath, # '/info' (with script_name)
'ip': request.remote_addr, # '127.0.0.1'
'content_type': request.content_type,
'content_length': request.content_length,
'is_ajax': request.is_xhr, # True if X-Requested-With
'chunked': request.chunked, # True if chunked encoding
}
Request Properties
| Property | Type | Description |
|---|---|---|
method | str | HTTP method (GET, POST, etc.) |
path | str | Request path without query string |
url | str | Full request URL including scheme, host, path, query |
fullpath | str | Path with script_name prefix |
query_string | str | Raw query string |
script_name | str | WSGI SCRIPT_NAME |
remote_addr | str | Client IP address |
remote_route | list | List of IPs from X-Forwarded-For |
content_type | str | Content-Type header value |
content_length | int | Content-Length (-1 if invalid) |
is_xhr | bool | True if XMLHttpRequest |
is_ajax | bool | Alias for is_xhr |
auth | tuple | Parsed Basic auth (username, password) or None |
chunked | bool | True if chunked transfer encoding |
Query Parameters
# GET /search?q=python&page=2&tag=web&tag=api
@app.route('/search')
def search():
q = request.query.get('q', '') # 'python'
page = request.query.get('page', '1') # '2' (always strings)
tags = request.query.getall('tag') # ['web', 'api']
return {'q': q, 'page': int(page), 'tags': tags}
request.query and request.GET are FormsDict instances. Use request.params to access combined query + form data.
Form Data
# POST with application/x-www-form-urlencoded
@app.post('/login')
def login():
username = request.forms.get('username')
password = request.forms.get('password')
return {'user': username}
request.forms contains form fields from POST bodies. request.POST includes both form fields and file uploads from multipart requests.
JSON Body
# POST with application/json body
@app.post('/api/users')
def create_user():
data = request.json # Auto-parsed dict
if data is None:
abort(400, 'Invalid JSON')
return {'created': data.get('name')}
request.json returns None if the Content-Type is not application/json or the body cannot be parsed. Always check for None.
File Uploads
@app.post('/upload')
def upload():
upload = request.files.get('document')
if not upload:
abort(400, 'No file uploaded')
# FileUpload properties
name = upload.filename # Sanitized filename
content_type = upload.content_type
size = upload.content_length
# Save to disk
upload.save('/tmp/uploads/', overwrite=True)
return {
'filename': name,
'content_type': content_type,
'size': size
}
FileUpload Object
| Property/Method | Description |
|---|---|
.filename | Sanitized filename (path components stripped) |
.file | File-like object for reading content |
.content_type | MIME type from Content-Type header |
.content_length | File size in bytes |
.get_header(name) | Get a specific multipart header |
.save(dest, overwrite=False) | Save file to directory or path |
Cookies
Reading Cookies
@app.route('/read')
def read_cookies():
# Plain cookies
session_id = request.cookies.get('session_id', 'none')
# Signed cookies (HMAC-SHA256 + JSON, tamper-proof)
user = request.get_cookie('user', secret='my-secret-key')
return {'session': session_id, 'user': user}
Setting Cookies
from datetime import timedelta
@app.route('/set')
def set_cookies():
# Plain cookie
response.set_cookie('session_id', 'abc123',
path='/',
httponly=True,
secure=True,
samesite='Lax'
)
# Signed cookie (tamper-proof)
response.set_cookie('user', 'alice',
secret='my-secret-key',
max_age=timedelta(days=7)
)
# Delete a cookie
response.delete_cookie('old_cookie')
return 'Cookies set!'
Cookie Options
| Option | Type | Description |
|---|---|---|
max_age | int / timedelta | Cookie lifetime in seconds (timedelta auto-converted) |
expires | datetime | Absolute expiry time |
path | str | Cookie path scope (default: current path) |
domain | str | Cookie domain scope |
secure | bool | Only send over HTTPS |
httponly | bool | Prevent JavaScript access |
samesite | str | Lax, Strict, or None |
Signed cookies use HMAC-SHA256 with JSON serialization (not pickle). The signature prevents tampering, and JSON ensures safe deserialization. Always use a strong secret key.
Headers
Request Headers
@app.route('/headers')
def show_headers():
# Case-insensitive access
auth = request.headers.get('Authorization')
ct = request.get_header('Content-Type', 'none')
custom = request.headers.get('X-Custom-Header')
return {'auth': auth, 'content_type': ct, 'custom': custom}
Response Headers
@app.route('/api/data')
def data():
response.set_header('X-Request-ID', 'abc-123')
response.set_header('Cache-Control', 'max-age=3600')
response.add_header('Set-Cookie', 'a=1') # add_header allows duplicates
response.content_type = 'application/json'
return {'data': 'value'}
| Method | Description |
|---|---|
response.set_header(name, value) | Set header (replaces existing) |
response.add_header(name, value) | Add header (allows multiple values) |
response.get_header(name) | Read a response header |
The Response Object
from lcore import response
@app.route('/custom')
def custom_response():
response.status = 201
response.content_type = 'application/json'
response.set_header('X-Custom', 'value')
return {'status': 'created'}
Response Properties
| Property | Type | Description |
|---|---|---|
status | int / str | Set with int (200) or string ('200 OK') |
status_code | int | Numeric status code (read-only) |
status_line | str | Full status line (read-only) |
content_type | str | Content-Type header (default: text/html; charset=UTF-8) |
content_length | int | Content-Length header |
charset | str | Character set from Content-Type |
expires | datetime | Expires header |
headers | HeaderDict | Mutable header dictionary |
body | str/bytes | Response body |
HTTPResponse for Immediate Return
from lcore import HTTPResponse
@app.route('/immediate')
def immediate():
# Raise HTTPResponse to skip normal processing
raise HTTPResponse(
body='{"error": "custom"}',
status=418,
headers={'Content-Type': 'application/json'}
)
Static Files
from lcore import static_file
@app.route('/static/<filepath:path>')
def serve_static(filepath):
return static_file(filepath, root='./public')
# With download mode
@app.route('/download/<filename>')
def download(filename):
return static_file(filename, root='./files', download=True)
# Custom headers and mimetype
@app.route('/assets/<path:path>')
def assets(path):
return static_file(path, root='./assets',
headers={'Cache-Control': 'max-age=86400'}
)
static_file Parameters
| Parameter | Type | Description |
|---|---|---|
filename | str | Relative file path |
root | str | Root directory to serve from |
mimetype | str/bool | Override MIME type (True = auto-detect) |
download | bool/str | True or filename for Content-Disposition |
charset | str | Character set for text files (default: UTF-8) |
etag | str | Custom ETag (default: SHA256 auto-generated) |
headers | dict | Additional response headers |
static_file uses os.path.realpath() to resolve symlinks and prevent path traversal attacks. Download filenames are sanitized with os.path.basename(). ETags use SHA256.
Redirects
from lcore import redirect
@app.route('/old-page')
def old_page():
redirect('/new-page') # 303 See Other (default)
@app.route('/moved')
def moved():
redirect('/new-location', 301) # Permanent redirect
@app.route('/login-required')
def login_required():
redirect('/login', 307) # Temporary, preserves method
Redirect Codes
| Code | Name | Use Case |
|---|---|---|
301 | Moved Permanently | URL has permanently changed (SEO) |
302 | Found | Temporary redirect (legacy) |
303 | See Other | Redirect after POST (default) |
307 | Temporary Redirect | Preserves HTTP method |
308 | Permanent Redirect | Permanent, preserves method |
Lcore