Posting Forms
🔥 Hot Tips
- Append
~RANDOM()to filters for dynamic tokens - Redirect after successful POST (Post/Redirect/Get pattern)
- Use
request.bodyfor form data,request.paramsfor query @noauth()only for trusted public endpoints- Tokens auto-refresh via
FreshTokenheader
If you're used to posting forms in the traditional manner to the web service, pay attention to the following:
- All
POST,PUT,PATCH, andDELETErequests are secured by default - You must pass a
formTokeninput value to be validated (CSRF protection)
Tina4 Python makes this simple and automatic — no manual validation required in your routes.
Secure by Default
Tina4 generates a unique, signed token per session and validates it on every write request.
Invalid tokens return a 403 Forbidden automatically.
1. Basic Form Handling
Route Setup
from tina4_python import get, post
@get("/contact")
async def contact_form(request, response):
return response.render("contact.twig")
@post("/contact")
async def submit_contact(request, response):
# Token already validated → proceed safely!
name = request.body.get("name", "")
email = request.body.get("email", "")
message = request.body.get("message", "")
# Process: save to DB, send email, etc.
# await send_email(email, message)
return response.redirect("/thanks?success=true")Template (Twig/Jinja)
<!-- templates/contact.twig -->
<form method="POST" action="/contact">
{{ form_token() }} <!-- Auto-generates <input name="formToken" value="..."> -->
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name" placeholder="Your name" required>
</div>
<div>
<label for="email">Email</label>
<input type="email" id="email" name="email" placeholder="your@email.com" required>
</div>
<div>
<label for="message">Message</label>
<textarea id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit">Send Message</button>
</form>2. Generating Form Tokens: Three Ways
There are three ways to get a formToken in Tina4 Python (aligned with PHP for consistency):
A. Using the Global Function form_token()
Pass optional context for better security (e.g., page-specific tokens).
<!-- templates/login.twig -->
<form name="login" method="POST" action="/login">
<input type="text" name="username" placeholder="Username" required>
{% set token = form_token({"page": "Login"}) %}
<input type="hidden" name="formToken" value="{{ token }}">
<button type="submit">Login</button>
</form>B. Using the Filter | form_token
Append ~RANDOM() to refresh the token on each render (prevents replay attacks).
<!-- templates/register.twig -->
<form name="register" method="POST" action="/register">
<input type="password" name="password" placeholder="Password" required>
{{ ("Register" ~ RANDOM()) | form_token }}
<!-- Outputs: <input type="hidden" name="formToken" value="fresh_token_here"> -->
<button type="submit">Register</button>
</form>C. From Response Headers (FreshToken)
For AJAX or meta tags — grab from the X-Fresh-Token header.
<!-- In your base layout -->
<meta name="fresh-token" content="{{ request.headers.get('FreshToken', '') }}">// In JS
const token = document.querySelector('meta[name="fresh-token"]').content;
fetch('/api/save', {
method: 'POST',
headers: { 'Authorization': 'Bearer '+ token },
body: JSON.stringify({ data: 'value' })
});3. File Uploads with Forms
Add enctype="multipart/form-data" and handle via request.files.
<form method="POST" action="/upload" enctype="multipart/form-data">
{{ form_token() }}
<input type="file" name="avatar" accept="image/*" multiple>
<button type="submit">Upload Files</button>
</form>@post("/upload")
async def handle_upload(request, response):
files = request.files.getlist("avatar") # List for multiple
for file in files:
await file.save(f"public/uploads/{file.filename}")
return response("Files uploaded!")4. Validation & Error Handling
Return errors and old input on failure.
@post("/register")
async def register_user(request, response):
errors = {}
data = request.body
if not data.get("email"):
errors["email"] = "Email is required"
if len(data.get("password", "")) < 8:
errors["password"] = "Password must be at least 8 characters"
if errors:
return response.render("register.twig", {
"errors": errors,
"old": data # Repopulate form
})
# Success
return response.redirect("/dashboard")In Twig:
<input type="email" name="email" value="{{ old.email|e if old else '' }}" required>
{% if errors.email %}
<span class="error">{{ errors.email }}</span>
{% endif %}5. Disabling Protection (@noauth())
Rarely needed — only for public webhooks.
@post("/webhook/payment")
@noauth() # Skips token validation
async def payment_webhook(request, response):
payload = request.body
# Process without token
return response("Received")Security Warning
Use @noauth() only for non-user endpoints like webhooks.
Never on login/register forms!
Example: Full Login Flow
Route
@get("/login")
async def login_page(request, response):
return response.render("login.twig")
@post("/login")
async def process_login(request, response):
username = request.body["username"]
password = request.body["password"]
if await validate_user(username, password):
request.session["user"] = username
return response.redirect("/dashboard")
return response.render("login.twig", {"error": "Invalid credentials"})Template
<form method="POST" action="/login">
{{ ("Login" ~ RANDOM()) | form_token }}
{% if error %}
<p class="error">{{ error }}</p>
{% endif %}
<input type="text" name="username" value="{{ old.username|e if old else '' }}" required>
<input type="password" name="password" required>
<button>Login</button>
</form>