HTTP
Request and response handling for Plain applications.
Overview
You interact with Request and Response objects in your views and middleware.
from plain.views import View
from plain.http import Response
class ExampleView(View):
def get(self):
# Access a request header
user_agent = self.request.headers.get("User-Agent")
# Access a query parameter
page = self.request.query_params.get("page", "1")
# Create and return a response
response = Response("Hello, world!", status_code=200)
response.headers["X-Custom-Header"] = "Custom Value"
return response
Request
The Request object provides access to all incoming HTTP request data.
Headers
Access request headers through the headers property. Header names are case-insensitive.
content_type = self.request.headers.get("Content-Type")
auth = self.request.headers.get("authorization") # Case-insensitive
Query parameters
Query string parameters are available as a QueryDict through query_params.
# URL: /search?q=plain&page=2
query = self.request.query_params.get("q") # "plain"
page = self.request.query_params.get("page", "1") # "2"
# For parameters with multiple values (?tags=python&tags=web)
tags = self.request.query_params.getlist("tags") # ["python", "web"]
Body data
Access request body data based on the content type.
JSON data:
# Returns dict, raises BadRequestError400 for invalid JSON
data = self.request.json_data
name = data.get("name")
Form data:
# For application/x-www-form-urlencoded or multipart/form-data
form = self.request.form_data
email = form.get("email")
File uploads:
# For multipart/form-data requests
uploaded_file = self.request.files.get("document")
if uploaded_file:
content = uploaded_file.read()
Raw body:
raw_bytes = self.request.body
Content negotiation
Check what content types the client accepts.
# Check if client accepts JSON
if self.request.accepts("application/json"):
return JsonResponse({"message": "Hello"})
# Get preferred type from options
preferred = self.request.get_preferred_type("text/html", "application/json")
Cookies
Read cookies from the request.
session_id = self.request.cookies.get("session_id")
# Read a signed cookie (returns None if signature is invalid)
user_id = self.request.get_signed_cookie("user_id", default=None)
Response
The Response class creates HTTP responses with string or bytes content.
from plain.http import Response
# Basic response
response = Response("Hello, world!")
# With status code and headers
response = Response(
content="Created!",
status_code=201,
headers={"X-Custom": "value"},
)
# Set content type
response = Response("<h1>Hello</h1>", content_type="text/html")
Response types
Plain provides specialized response classes for common use cases.
JSON responses:
from plain.http import JsonResponse
return JsonResponse({"name": "Plain", "version": "1.0"})
Redirects:
from plain.http import RedirectResponse
return RedirectResponse("/new-location")
File downloads:
from plain.http import FileResponse
# Serve a file
return FileResponse(open("report.pdf", "rb"))
# Force download with custom filename
return FileResponse(
open("report.pdf", "rb"),
as_attachment=True,
filename="monthly-report.pdf",
)
Streaming responses:
from plain.http import StreamingResponse
def generate_data():
for i in range(1000):
yield f"Line {i}\n"
return StreamingResponse(generate_data(), content_type="text/plain")
Other response types include NotModifiedResponse (304) and NotAllowedResponse (405).
Setting cookies
Set cookies on the response.
response = Response("Welcome!")
response.set_cookie("session_id", "abc123", httponly=True, secure=True)
# With expiration
response.set_cookie("remember_me", "yes", max_age=86400 * 30) # 30 days
# Signed cookie (tamper-proof)
response.set_signed_cookie("user_id", "42", httponly=True)
# Delete a cookie
response.delete_cookie("old_cookie")
Default response headers
Plain applies default headers from DEFAULT_RESPONSE_HEADERS in settings to all responses. You can customize these per-view.
Override a default header:
response = Response("content")
response.headers["X-Frame-Options"] = "SAMEORIGIN"
Remove a default header:
response = Response("content")
response.headers["X-Frame-Options"] = None # Removes the header
Extend a default header:
from plain.runtime import settings
if csp := settings.DEFAULT_RESPONSE_HEADERS.get("Content-Security-Policy"):
csp = csp.format(request=self.request)
response.headers["Content-Security-Policy"] = f"{csp}; script-src https://cdn.example.com"
Content Security Policy (CSP)
Plain includes built-in support for Content Security Policy through nonces. Each request generates a unique cryptographically secure nonce available via request.csp_nonce.
Configure CSP in settings:
# app/settings.py
DEFAULT_RESPONSE_HEADERS = {
"Content-Security-Policy": (
"default-src 'self'; "
"script-src 'self' 'nonce-{request.csp_nonce}'; "
"style-src 'self' 'nonce-{request.csp_nonce}'; "
"img-src 'self' data:; "
"font-src 'self'; "
"connect-src 'self'; "
"frame-ancestors 'self'; "
"base-uri 'self'; "
"form-action 'self'"
),
"X-Frame-Options": "DENY",
}
The {request.csp_nonce} placeholder is replaced with a unique nonce for each request.
Use nonces in templates:
<script nonce="{{ request.csp_nonce }}">
console.log("This script is allowed by CSP");
</script>
<style nonce="{{ request.csp_nonce }}">
.example { color: red; }
</style>
External scripts and stylesheets loaded from 'self' don't need nonces:
<script src="/assets/app.js"></script>
<link rel="stylesheet" href="/assets/app.css">
Use Google's CSP Evaluator to analyze your CSP policy.
Middleware
Create custom middleware by subclassing HttpMiddleware.
from plain.http import HttpMiddleware, Request, Response
class TimingMiddleware(HttpMiddleware):
def process_request(self, request: Request) -> Response:
import time
start = time.time()
response = self.get_response(request)
duration = time.time() - start
response.headers["X-Request-Duration"] = f"{duration:.3f}s"
return response
Exceptions
Raise exceptions to return specific HTTP error responses.
from plain.http import NotFoundError404, ForbiddenError403, BadRequestError400
# Return 404
raise NotFoundError404("Page not found")
# Return 403
raise ForbiddenError403("Access denied")
# Return 400
raise BadRequestError400("Invalid input")
Additional exceptions include SuspiciousOperationError400, TooManyFieldsSentError400, TooManyFilesSentError400, and RequestDataTooBigError400.
FAQs
How do I access the client's IP address?
Use request.client_ip. If you're behind a proxy, enable HTTP_X_FORWARDED_FOR in settings.
ip = self.request.client_ip
How do I build an absolute URL?
Use request.build_absolute_uri().
# Current page
url = self.request.build_absolute_uri()
# Specific path
url = self.request.build_absolute_uri("/api/users")
How do I check if the request is HTTPS?
Use request.is_https() or check request.scheme.
if self.request.is_https():
# Secure connection
pass
What's the difference between QueryDict and a regular dict?
QueryDict handles multiple values for the same key (common in query strings and form data). Use get() for a single value or getlist() for all values.
How do I handle large file uploads?
Configure DATA_UPLOAD_MAX_MEMORY_SIZE in settings. For very large files, consider streaming the upload instead of loading it into memory.
Installation
The plain.http module is included with Plain by default. No additional installation is required.
from plain.http import Request, Response, JsonResponse