patx/relay-lang

Fix axum Method import for web handler extractor

Commit 2fe9bfe · Harrison Erd · 2026-02-09T19:39:27-05:00

Changeset
2fe9bfebae83efb4e15547cf5fabc42a5eb3d0b3
Parents
f5d4236313924cd4a27922b8f88a415f3467c304

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index 5ca6c13..35c6c75 100644
--- a/README.md
+++ b/README.md
@@ -266,34 +266,67 @@ MongoDB helpers return `Deferred` values, so they run asynchronously until the r
 
 ```relay
 app = WebApp()
-route = app.route()
 server = WebServer()
 
[email protected]("/")
[email protected]("/")
 fn index()
-    return "Hello world"
+    return read_file("docs/index.html")
 
[email protected]("/hello/<name>")
-fn hello(name)
-    return "Hello {{ name }}"
[email protected]("/")
+fn create_paste(paste)
+    id = "abc123"
+    return app.redirect("/" + id)
 
 server.run(app)
 ```
 
 Notes:
-- Define `app`/`route` before the decorated handlers.
-- Decorators can use `@app.get("/path")` directly instead of `@route.get`.
-- Returning a dict/list/json produces a JSON response.
+- Flask/FastAPI-like decorators: `@app.get`, `@app.post`, `@app.put`, `@app.patch`, `@app.delete`.
+- Path params use `<name>` syntax, e.g. `@app.get("/p/<pid>")`.
+- Handler params are auto-bound from query/body/path (path wins over body, body wins over query).
+- `app.redirect(url)` returns an HTTP 302 response with a `Location` header.
+- `request`, `cookies`, and `session` are injected into each handler scope.
+- Returning dict/list/json produces JSON; strings become text/html/plain automatically.
+- `session` persists via an HttpOnly `relay_sid` cookie.
 
 ### Custom responses
 
 ```relay
[email protected]("/status")
[email protected]("/status")
 fn status()
     return Response({"ok": True}, status=201, content_type="application/json")
 ```
 
-Relay will infer `content_type` when possible; use `Response(...)` to force status codes or headers.
+Relay will infer `content_type` when possible; use `Response(...)` to force status codes/content types.
+
+### Pastebin-style example (Mongo + redirect)
+
+```relay
+app = WebApp()
+server = WebServer()
+mongo = Mongo("mongodb://localhost:27017")
+db = mongo.db("relay_demo")
+pastes = db.collection("pastes")
+
[email protected]("/")
+fn index()
+    return read_file("docs/index.html")
+
[email protected]("/<pid>")
+fn view_paste(pid)
+    paste = pastes.find_one({"_id": pid})
+    if (paste == None)
+        return Response("not found", status=404)
+    return str(paste["paste_content"])
+
[email protected]("/")
+fn new_paste(paste)
+    inserted = pastes.insert_one({"paste_content": paste})
+    pid = str(inserted["inserted_id"])
+    return app.redirect("/" + pid)
+
+server.run(app)
+```
 
 ---
 
diff --git a/src/main.rs b/src/main.rs
index 27402ae..66f8968 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -19,7 +19,7 @@ use std::{
 
 use axum::{
     extract::{Path, Query},
-    http::{HeaderMap, StatusCode},
+    http::{HeaderMap, Method, StatusCode},
     response::IntoResponse,
     routing::{get, post},
     Json as AxumJson, Router,
@@ -31,21 +31,22 @@ use html_escape::encode_safe;
 use indexmap::IndexMap;
 use mongodb::{
     bson::{self, Bson, Document},
-    Client as MongoClient,
-    Collection as MongoCollection,
-    Database as MongoDatabase,
+    Client as MongoClient, Collection as MongoCollection, Database as MongoDatabase,
 };
 use serde_json::Value as J;
-use thiserror::Error;
 use std::sync::OnceLock;
-
+use thiserror::Error;
 
 // ========================= Errors =========================
 
 #[derive(Debug, Error)]
 pub enum RelayError {
     #[error("Syntax error: {msg} at line {line}, col {col}")]
-    Syntax { msg: String, line: usize, col: usize },
+    Syntax {
+        msg: String,
+        line: usize,
+        col: usize,
+    },
 
     #[error("Runtime error: {0}")]
     Runtime(String),
@@ -68,13 +69,31 @@ struct Program {
 #[derive(Debug, Clone)]
 enum Stmt {
     Expr(Expr),
-    Assign { name: String, expr: Expr },
-    AugAssign { name: String, op: AugOp, expr: Expr },
+    Assign {
+        name: String,
+        expr: Expr,
+    },
+    AugAssign {
+        name: String,
+        op: AugOp,
+        expr: Expr,
+    },
     Return(Option<Expr>),
 
-    If { cond: Expr, then_block: Vec<Stmt>, else_block: Vec<Stmt> },
-    While { cond: Expr, body: Vec<Stmt> },
-    For { var: String, iter: Expr, body: Vec<Stmt> },
+    If {
+        cond: Expr,
+        then_block: Vec<Stmt>,
+        else_block: Vec<Stmt>,
+    },
+    While {
+        cond: Expr,
+        body: Vec<Stmt>,
+    },
+    For {
+        var: String,
+        iter: Expr,
+        body: Vec<Stmt>,
+    },
 
     FuncDef {
         name: String,
@@ -119,16 +138,29 @@ enum Expr {
     List(Vec<Expr>),
     Dict(Vec<(Expr, Expr)>),
 
-    Member { object: Box<Expr>, name: String }, // obj.name
+    Member {
+        object: Box<Expr>,
+        name: String,
+    }, // obj.name
     Call {
         callee: Box<Expr>,
         args: Vec<Expr>,
         kwargs: Vec<(String, Expr)>,
     },
-    Index { target: Box<Expr>, index: Box<Expr> },
+    Index {
+        target: Box<Expr>,
+        index: Box<Expr>,
+    },
 
-    BinOp { left: Box<Expr>, op: BinOp, right: Box<Expr> },
-    Unary { op: UnaryOp, expr: Box<Expr> },
+    BinOp {
+        left: Box<Expr>,
+        op: BinOp,
+        right: Box<Expr>,
+    },
+    Unary {
+        op: UnaryOp,
+        expr: Box<Expr>,
+    },
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -378,8 +410,8 @@ impl<'a> Lexer<'a> {
                     if is_ident_start(c) {
                         let id = self.read_ident();
                         let k = match id.as_str() {
-                            "fn" | "if" | "for" | "while" | "return" | "True" | "False" | "None" | "str"
-                            | "int" | "float" => Tok::Keyword(id),
+                            "fn" | "if" | "for" | "while" | "return" | "True" | "False"
+                            | "None" | "str" | "int" | "float" => Tok::Keyword(id),
                             _ => Tok::Ident(id),
                         };
                         out.push(self.wrap(k));
@@ -401,13 +433,21 @@ impl<'a> Lexer<'a> {
     }
 
     fn tok(&self, kind: Tok) -> Token {
-        Token { kind, line: self.line, col: self.col }
+        Token {
+            kind,
+            line: self.line,
+            col: self.col,
+        }
     }
     fn wrap(&self, kind: Tok) -> Token {
         self.tok(kind)
     }
     fn err(&self, msg: &str) -> RelayError {
-        RelayError::Syntax { msg: msg.into(), line: self.line, col: self.col }
+        RelayError::Syntax {
+            msg: msg.into(),
+            line: self.line,
+            col: self.col,
+        }
     }
 
     fn peek_char(&self) -> Option<char> {
@@ -501,7 +541,9 @@ impl<'a> Lexer<'a> {
             }
             if c == '\\' {
                 self.advance_char();
-                let esc = self.peek_char().ok_or_else(|| self.err("Bad string escape"))?;
+                let esc = self
+                    .peek_char()
+                    .ok_or_else(|| self.err("Bad string escape"))?;
                 self.advance_char();
                 out.push(match esc {
                     'n' => '\n',
@@ -615,14 +657,25 @@ impl Parser {
 
         // assignment
         if let Some(name) = self.peek_ident_string() {
-            if self.peek_n_is(1, &Tok::Assign) || self.peek_n_is(1, &Tok::AugAdd) || self.peek_n_is(1, &Tok::AugSub) {
+            if self.peek_n_is(1, &Tok::Assign)
+                || self.peek_n_is(1, &Tok::AugAdd)
+                || self.peek_n_is(1, &Tok::AugSub)
+            {
                 self.bump(); // ident
                 let op = self.bump().kind.clone();
                 let expr = self.parse_expr()?;
                 return Ok(match op {
                     Tok::Assign => Stmt::Assign { name, expr },
-                    Tok::AugAdd => Stmt::AugAssign { name, op: AugOp::Add, expr },
-                    Tok::AugSub => Stmt::AugAssign { name, op: AugOp::Sub, expr },
+                    Tok::AugAdd => Stmt::AugAssign {
+                        name,
+                        op: AugOp::Add,
+                        expr,
+                    },
+                    Tok::AugSub => Stmt::AugAssign {
+                        name,
+                        op: AugOp::Sub,
+                        expr,
+                    },
                     _ => unreachable!(),
                 });
             }
@@ -665,7 +718,11 @@ impl Parser {
                     default = Some(self.parse_expr()?);
                 }
 
-                params.push(Param { name: pname, ty, default });
+                params.push(Param {
+                    name: pname,
+                    ty,
+                    default,
+                });
 
                 if self.peek_is(&Tok::Comma) {
                     self.bump();
@@ -677,7 +734,12 @@ impl Parser {
         self.expect(&Tok::RParen)?;
         self.expect_newline()?;
         let body = self.parse_block()?;
-        Ok(Stmt::FuncDef { name, params, body, decorators })
+        Ok(Stmt::FuncDef {
+            name,
+            params,
+            body,
+            decorators,
+        })
     }
 
     fn parse_if(&mut self) -> RResult<Stmt> {
@@ -694,7 +756,11 @@ impl Parser {
             self.expect_newline()?;
             else_block = self.parse_block()?;
         }
-        Ok(Stmt::If { cond, then_block, else_block })
+        Ok(Stmt::If {
+            cond,
+            then_block,
+            else_block,
+        })
     }
 
     fn parse_while(&mut self) -> RResult<Stmt> {
@@ -760,7 +826,11 @@ impl Parser {
             }
             self.bump(); // op
             let right = self.parse_binop(prec + 1)?;
-            left = Expr::BinOp { left: Box::new(left), op, right: Box::new(right) };
+            left = Expr::BinOp {
+                left: Box::new(left),
+                op,
+                right: Box::new(right),
+            };
         }
         Ok(left)
     }
@@ -769,12 +839,18 @@ impl Parser {
         if self.peek_op("-") {
             self.bump();
             let e = self.parse_unary()?;
-            return Ok(Expr::Unary { op: UnaryOp::Neg, expr: Box::new(e) });
+            return Ok(Expr::Unary {
+                op: UnaryOp::Neg,
+                expr: Box::new(e),
+            });
         }
         if self.peek_ident_string().as_deref() == Some("not") {
             self.bump();
             let e = self.parse_unary()?;
-            return Ok(Expr::Unary { op: UnaryOp::Not, expr: Box::new(e) });
+            return Ok(Expr::Unary {
+                op: UnaryOp::Not,
+                expr: Box::new(e),
+            });
         }
         self.parse_postfix()
     }
@@ -786,7 +862,10 @@ impl Parser {
             if self.peek_is(&Tok::Dot) {
                 self.bump();
                 let name = self.expect_ident()?;
-                e = Expr::Member { object: Box::new(e), name };
+                e = Expr::Member {
+                    object: Box::new(e),
+                    name,
+                };
                 continue;
             }
 
@@ -817,7 +896,11 @@ impl Parser {
                     }
                 }
                 self.expect(&Tok::RParen)?;
-                e = Expr::Call { callee: Box::new(e), args, kwargs };
+                e = Expr::Call {
+                    callee: Box::new(e),
+                    args,
+                    kwargs,
+                };
                 continue;
             }
 
@@ -825,7 +908,10 @@ impl Parser {
                 self.bump();
                 let idx = self.parse_expr()?;
                 self.expect(&Tok::RBracket)?;
-                e = Expr::Index { target: Box::new(e), index: Box::new(idx) };
+                e = Expr::Index {
+                    target: Box::new(e),
+                    index: Box::new(idx),
+                };
                 continue;
             }
 
@@ -892,7 +978,10 @@ impl Parser {
     }
 
     fn peek_binop(&self) -> Option<(BinOp, u8)> {
-        if let Some(Token { kind: Tok::Op(op), .. }) = self.t.get(self.i) {
+        if let Some(Token {
+            kind: Tok::Op(op), ..
+        }) = self.t.get(self.i)
+        {
             let (b, p) = match op.as_str() {
                 "==" => (BinOp::Eq, 2),
                 "!=" => (BinOp::Ne, 2),
@@ -929,7 +1018,10 @@ impl Parser {
         self.peek().map(|t| &t.kind == k).unwrap_or(false)
     }
     fn peek_n_is(&self, n: usize, k: &Tok) -> bool {
-        self.t.get(self.i + n).map(|t| &t.kind == k).unwrap_or(false)
+        self.t
+            .get(self.i + n)
+            .map(|t| &t.kind == k)
+            .unwrap_or(false)
     }
     fn peek_op(&self, s: &str) -> bool {
         matches!(self.peek(), Some(Token { kind: Tok::Op(op), .. }) if op == s)
@@ -970,8 +1062,14 @@ impl Parser {
     }
     fn peek_ident_string(&self) -> Option<String> {
         match self.peek() {
-            Some(Token { kind: Tok::Ident(s), .. }) => Some(s.clone()),
-            Some(Token { kind: Tok::Keyword(s), .. }) => Some(s.clone()),
+            Some(Token {
+                kind: Tok::Ident(s),
+                ..
+            }) => Some(s.clone()),
+            Some(Token {
+                kind: Tok::Keyword(s),
+                ..
+            }) => Some(s.clone()),
             _ => None,
         }
     }
@@ -982,10 +1080,18 @@ impl Parser {
     }
     fn err_here(&self, msg: &str) -> RelayError {
         let (line, col) = self.peek().map(|t| (t.line, t.col)).unwrap_or((0, 0));
-        RelayError::Syntax { msg: msg.into(), line, col }
+        RelayError::Syntax {
+            msg: msg.into(),
+            line,
+            col,
+        }
     }
     fn err_at(&self, line: usize, col: usize, msg: &str) -> RelayError {
-        RelayError::Syntax { msg: msg.into(), line, col }
+        RelayError::Syntax {
+            msg: msg.into(),
+            line,
+            col,
+        }
     }
 }
 
@@ -1024,6 +1130,8 @@ struct Response {
     status: u16,
     content_type: String,
     body: Vec<u8>,
+    headers: HashMap<String, String>,
+    set_cookies: Vec<String>,
 }
 
 #[derive(Clone)]
@@ -1054,14 +1162,19 @@ struct Deferred {
 }
 impl Deferred {
     fn new(f: BoxFut) -> Self {
-        Self { fut: tokio::sync::Mutex::new(Some(f)), cached: tokio::sync::Mutex::new(None) }
+        Self {
+            fut: tokio::sync::Mutex::new(Some(f)),
+            cached: tokio::sync::Mutex::new(None),
+        }
     }
     async fn resolve(&self) -> RResult<Value> {
         if let Some(v) = self.cached.lock().await.clone() {
             return Ok(v);
         }
         let mut g = self.fut.lock().await;
-        let f = g.take().ok_or_else(|| RelayError::Runtime("Deferred already taken".into()))?;
+        let f = g
+            .take()
+            .ok_or_else(|| RelayError::Runtime("Deferred already taken".into()))?;
         drop(g);
         let v = f.await?;
         *self.cached.lock().await = Some(v.clone());
@@ -1074,17 +1187,21 @@ struct TaskHandle {
 }
 impl TaskHandle {
     fn new(j: tokio::task::JoinHandle<RResult<Value>>) -> Self {
-        Self { join: tokio::sync::Mutex::new(Some(j)) }
+        Self {
+            join: tokio::sync::Mutex::new(Some(j)),
+        }
     }
     async fn join(&self) -> RResult<Value> {
         let mut g = self.join.lock().await;
-        let h = g.take().ok_or_else(|| RelayError::Runtime("Task already joined".into()))?;
+        let h = g
+            .take()
+            .ok_or_else(|| RelayError::Runtime("Task already joined".into()))?;
         drop(g);
-        h.await.map_err(|e| RelayError::Runtime(format!("Task join error: {e}")))?
+        h.await
+            .map_err(|e| RelayError::Runtime(format!("Task join error: {e}")))?
     }
 }
 
-
 // A Thunk is an unevaluated expression captured for delayed execution (used by schedulers like sleep/timeout).
 struct Thunk {
     expr: Expr,
@@ -1095,7 +1212,9 @@ impl Thunk {
         Self { expr, env }
     }
     async fn run(&self) -> RResult<Value> {
-        let ev = Evaluator { env: self.env.clone() };
+        let ev = Evaluator {
+            env: self.env.clone(),
+        };
         ev.eval_expr(&self.expr).await
     }
 }
@@ -1120,12 +1239,25 @@ impl Value {
         match self {
             Value::Int(n) => n.to_string(),
             Value::Float(f) => f.to_string(),
-            Value::Bool(b) => if *b { "True".into() } else { "False".into() },
+            Value::Bool(b) => {
+                if *b {
+                    "True".into()
+                } else {
+                    "False".into()
+                }
+            }
             Value::Str(s) => s.clone(),
             Value::None => "None".into(),
-            Value::List(v) => format!("[{}]", v.iter().map(|x| x.repr()).collect::<Vec<_>>().join(", ")),
+            Value::List(v) => format!(
+                "[{}]",
+                v.iter().map(|x| x.repr()).collect::<Vec<_>>().join(", ")
+            ),
             Value::Dict(m) => {
-                let pairs = m.iter().map(|(k, v)| format!("{k}: {}", v.repr())).collect::<Vec<_>>().join(", ");
+                let pairs = m
+                    .iter()
+                    .map(|(k, v)| format!("{k}: {}", v.repr()))
+                    .collect::<Vec<_>>()
+                    .join(", ");
                 format!("{{{pairs}}}")
             }
             Value::Json(j) => j.to_string(),
@@ -1149,7 +1281,9 @@ struct Env {
 }
 impl Env {
     fn new_global() -> Self {
-        Self { scopes: vec![HashMap::new()] }
+        Self {
+            scopes: vec![HashMap::new()],
+        }
     }
     fn push(&mut self) {
         self.scopes.push(HashMap::new());
@@ -1278,27 +1412,30 @@ impl Evaluator {
     #[async_recursion]
     async fn eval_stmt(&self, s: &Stmt) -> RResult<Flow> {
         match s {
-            
-Stmt::Expr(e) => {
-    let v = self.eval_expr(e).await?;
-
-    // Non-blocking: expression statements never wait.
-    // If the result is Deferred/Task, run it in the background and keep the process alive.
-    match v {
-        Value::Deferred(d) => {
-            let h = tokio::spawn(async move { let _ = d.resolve().await; });
-            track_bg(h).await;
-        }
-        Value::Task(t) => {
-            let h = tokio::spawn(async move { let _ = t.join().await; });
-            track_bg(h).await;
-        }
-        _ => {}
-    }
+            Stmt::Expr(e) => {
+                let v = self.eval_expr(e).await?;
+
+                // Non-blocking: expression statements never wait.
+                // If the result is Deferred/Task, run it in the background and keep the process alive.
+                match v {
+                    Value::Deferred(d) => {
+                        let h = tokio::spawn(async move {
+                            let _ = d.resolve().await;
+                        });
+                        track_bg(h).await;
+                    }
+                    Value::Task(t) => {
+                        let h = tokio::spawn(async move {
+                            let _ = t.join().await;
+                        });
+                        track_bg(h).await;
+                    }
+                    _ => {}
+                }
 
-    Ok(Flow::None)
-}
-                        Stmt::Assign { name, expr } => {
+                Ok(Flow::None)
+            }
+            Stmt::Assign { name, expr } => {
                 let v = self.eval_expr(expr).await?;
                 let mut env = self.env.lock().await;
                 if !env.assign_existing(name, v.clone()) {
@@ -1307,7 +1444,9 @@ Stmt::Expr(e) => {
                 Ok(Flow::None)
             }
             Stmt::AugAssign { name, op, expr } => {
-                let rhs = self.resolve_deferred_only(self.eval_expr(expr).await?).await?;
+                let rhs = self
+                    .resolve_deferred_only(self.eval_expr(expr).await?)
+                    .await?;
                 let env = self.env.lock().await;
                 let cur = env.get(name)?;
                 drop(env);
@@ -1330,12 +1469,20 @@ Stmt::Expr(e) => {
                 };
                 Ok(Flow::Return(v))
             }
-            Stmt::If { cond, then_block, else_block } => {
+            Stmt::If {
+                cond,
+                then_block,
+                else_block,
+            } => {
                 let c = self.resolve_if_needed(self.eval_expr(cond).await?).await?;
                 let mut env = self.env.lock().await;
                 env.push();
                 drop(env);
-                let r = if c.truthy() { self.eval_block(then_block).await? } else { self.eval_block(else_block).await? };
+                let r = if c.truthy() {
+                    self.eval_block(then_block).await?
+                } else {
+                    self.eval_block(else_block).await?
+                };
                 let mut env = self.env.lock().await;
                 env.pop();
                 Ok(r)
@@ -1365,7 +1512,11 @@ Stmt::Expr(e) => {
                     Value::List(v) => v,
                     Value::Str(s) => s.chars().map(|c| Value::Str(c.to_string())).collect(),
                     Value::Dict(m) => m.keys().map(|k| Value::Str(k.clone())).collect(),
-                    _ => return Err(RelayError::Type("for(each in iterable) expects list/string/dict".into())),
+                    _ => {
+                        return Err(RelayError::Type(
+                            "for(each in iterable) expects list/string/dict".into(),
+                        ))
+                    }
                 };
 
                 for item in items {
@@ -1383,7 +1534,12 @@ Stmt::Expr(e) => {
                 }
                 Ok(Flow::None)
             }
-            Stmt::FuncDef { name, params, body, decorators } => {
+            Stmt::FuncDef {
+                name,
+                params,
+                body,
+                decorators,
+            } => {
                 let f = UserFunction {
                     name: name.clone(),
                     params: params.clone(),
@@ -1448,54 +1604,80 @@ Stmt::Expr(e) => {
             }
 
             Expr::Index { target, index } => {
-                let t = self.resolve_if_needed(self.eval_expr(target).await?).await?;
+                let t = self
+                    .resolve_if_needed(self.eval_expr(target).await?)
+                    .await?;
                 let idx = self.resolve_if_needed(self.eval_expr(index).await?).await?;
                 match (t, idx) {
-                    (Value::List(v), Value::Int(i)) => v.get(i as usize).cloned().ok_or_else(|| RelayError::Runtime("Index out of range".into())),
-                    (Value::Str(s), Value::Int(i)) => s.chars().nth(i as usize).map(|c| Value::Str(c.to_string())).ok_or_else(|| RelayError::Runtime("Index out of range".into())),
-                    (Value::Dict(m), k) => m.get(&k.repr()).cloned().ok_or_else(|| RelayError::Runtime("Key not found".into())),
+                    (Value::List(v), Value::Int(i)) => v
+                        .get(i as usize)
+                        .cloned()
+                        .ok_or_else(|| RelayError::Runtime("Index out of range".into())),
+                    (Value::Str(s), Value::Int(i)) => s
+                        .chars()
+                        .nth(i as usize)
+                        .map(|c| Value::Str(c.to_string()))
+                        .ok_or_else(|| RelayError::Runtime("Index out of range".into())),
+                    (Value::Dict(m), k) => m
+                        .get(&k.repr())
+                        .cloned()
+                        .ok_or_else(|| RelayError::Runtime("Key not found".into())),
                     (Value::Json(j), Value::Str(k)) => Ok(match j.get(&k) {
                         Some(v) => Value::Json(v.clone()),
                         None => return Err(RelayError::Runtime("Key not found".into())),
                     }),
-                    _ => Err(RelayError::Type("Indexing expects list/string/dict/json".into())),
+                    _ => Err(RelayError::Type(
+                        "Indexing expects list/string/dict/json".into(),
+                    )),
                 }
             }
 
-            Expr::Call { callee, args, kwargs } => {
+            Expr::Call {
+                callee,
+                args,
+                kwargs,
+            } => {
                 // callee can be Member(...) which may return a bound method
                 // IMPORTANT: spawn(expr) must receive raw (unresolved) values so it can run them concurrently
                 let is_spawn_ident = matches!(&**callee, Expr::Ident(name) if name == "spawn");
                 let is_sleep_ident = matches!(&**callee, Expr::Ident(name) if name == "sleep");
                 let is_print_ident = matches!(&**callee, Expr::Ident(name) if name == "print");
 
-                let c = self.resolve_deferred_only(self.eval_expr(callee).await?).await?;
-
-                
-// args
-let mut a = Vec::new();
-for (idx, x) in args.iter().enumerate() {
-    if (is_sleep_ident && idx == 1) || is_print_ident {
-        // Pass unevaluated expression as a thunk (evaluated after delay or in print)
-        let env = self.snapshot_env().await;
-        a.push(Value::Thunk(Arc::new(Thunk::new(x.clone(), env))));
-        continue;
-    }
-    let v = self.eval_expr(x).await?;
-    let v = if is_spawn_ident { v } else { self.resolve_deferred_only(v).await? };
-    a.push(v);
-}
+                let c = self
+                    .resolve_deferred_only(self.eval_expr(callee).await?)
+                    .await?;
+
+                // args
+                let mut a = Vec::new();
+                for (idx, x) in args.iter().enumerate() {
+                    if (is_sleep_ident && idx == 1) || is_print_ident {
+                        // Pass unevaluated expression as a thunk (evaluated after delay or in print)
+                        let env = self.snapshot_env().await;
+                        a.push(Value::Thunk(Arc::new(Thunk::new(x.clone(), env))));
+                        continue;
+                    }
+                    let v = self.eval_expr(x).await?;
+                    let v = if is_spawn_ident {
+                        v
+                    } else {
+                        self.resolve_deferred_only(v).await?
+                    };
+                    a.push(v);
+                }
 
                 // kwargs
                 let mut kw = Vec::new();
                 for (k, x) in kwargs {
                     let v = self.eval_expr(x).await?;
-                    let v = if is_spawn_ident { v } else { self.resolve_deferred_only(v).await? };
+                    let v = if is_spawn_ident {
+                        v
+                    } else {
+                        self.resolve_deferred_only(v).await?
+                    };
                     kw.push((k.clone(), v));
                 }
 
                 self.call_value(c, a, kw).await
-
             }
 
             Expr::BinOp { left, op, right } => {
@@ -1533,7 +1715,12 @@ for (idx, x) in args.iter().enumerate() {
         }
     }
 
-    async fn call_value(&self, callee: Value, args: Vec<Value>, kwargs: Vec<(String, Value)>) -> RResult<Value> {
+    async fn call_value(
+        &self,
+        callee: Value,
+        args: Vec<Value>,
+        kwargs: Vec<(String, Value)>,
+    ) -> RResult<Value> {
         match callee {
             Value::Builtin(f) => (f)(args, kwargs).await,
             Value::Function(f) => self.call_user_fn(&f, args, kwargs).await,
@@ -1543,7 +1730,12 @@ for (idx, x) in args.iter().enumerate() {
         }
     }
 
-    async fn call_user_fn(&self, f: &UserFunction, args: Vec<Value>, kwargs: Vec<(String, Value)>) -> RResult<Value> {
+    async fn call_user_fn(
+        &self,
+        f: &UserFunction,
+        args: Vec<Value>,
+        kwargs: Vec<(String, Value)>,
+    ) -> RResult<Value> {
         // bind positional + kwargs + defaults
         let mut bound: HashMap<String, Value> = HashMap::new();
 
@@ -1558,7 +1750,10 @@ for (idx, x) in args.iter().enumerate() {
         for p in &f.params {
             if !bound.contains_key(&p.name) {
                 if let Some(def) = &p.default {
-                    bound.insert(p.name.clone(), self.resolve_if_needed(self.eval_expr(def).await?).await?);
+                    bound.insert(
+                        p.name.clone(),
+                        self.resolve_if_needed(self.eval_expr(def).await?).await?,
+                    );
                 } else {
                     bound.insert(p.name.clone(), Value::None);
                 }
@@ -1618,33 +1813,37 @@ for (idx, x) in args.iter().enumerate() {
                 }
             }
 
-            Value::Deferred(d) => {
-                match name {
-                    "join" | "resolve" => {
-                        let d0 = d.clone();
-                        Ok(Value::Builtin(Arc::new(move |_args, _kwargs| {
-                            let d0 = d0.clone();
-                            Box::pin(async move {
-                                let d2 = Deferred::new(Box::pin(async move {
-                                    let v = d0.resolve().await?;
-                                    Ok(v)
-                                }));
-                                Ok(Value::Deferred(Arc::new(d2)))
-                            })
-                        })))
-                    }
-                    _ => Ok(Value::None),
+            Value::Deferred(d) => match name {
+                "join" | "resolve" => {
+                    let d0 = d.clone();
+                    Ok(Value::Builtin(Arc::new(move |_args, _kwargs| {
+                        let d0 = d0.clone();
+                        Box::pin(async move {
+                            let d2 = Deferred::new(Box::pin(async move {
+                                let v = d0.resolve().await?;
+                                Ok(v)
+                            }));
+                            Ok(Value::Deferred(Arc::new(d2)))
+                        })
+                    })))
                 }
-            }
+                _ => Ok(Value::None),
+            },
 
             Value::Dict(m) => Ok(m.get(name).cloned().unwrap_or(Value::None)),
             Value::Json(j) => Ok(j.get(name).cloned().map(Value::Json).unwrap_or(Value::None)),
 
-            _ => Err(RelayError::Type("Member access expects object/dict/json/task/deferred".into())),
+            _ => Err(RelayError::Type(
+                "Member access expects object/dict/json/task/deferred".into(),
+            )),
         }
     }
 
-    async fn register_decorators_for_fn(&self, fn_name: &str, decorators: &[Decorator]) -> RResult<()> {
+    async fn register_decorators_for_fn(
+        &self,
+        fn_name: &str,
+        decorators: &[Decorator],
+    ) -> RResult<()> {
         for d in decorators {
             match d {
                 Decorator::Route { base, method, path } => {
@@ -1661,8 +1860,8 @@ for (idx, x) in args.iter().enumerate() {
                         }
                         _ => {
                             return Err(RelayError::Type(format!(
-                                "@{base}.* expects `{base}` to be a WebApp (preferred) or Route handle"
-                            )))
+                            "@{base}.* expects `{base}` to be a WebApp (preferred) or Route handle"
+                        )))
                         }
                     }
                 }
@@ -1678,13 +1877,21 @@ fn coerce_param_type(ty: &str, v: Value) -> RResult<Value> {
         "int" => match v {
             Value::Int(n) => Ok(Value::Int(n)),
             Value::Float(f) => Ok(Value::Int(f as i64)),
-            Value::Str(s) => s.trim().parse::<i64>().map(Value::Int).map_err(|_| RelayError::Type("Bad int".into())),
+            Value::Str(s) => s
+                .trim()
+                .parse::<i64>()
+                .map(Value::Int)
+                .map_err(|_| RelayError::Type("Bad int".into())),
             _ => Err(RelayError::Type("Bad int".into())),
         },
         "float" => match v {
             Value::Float(f) => Ok(Value::Float(f)),
             Value::Int(n) => Ok(Value::Float(n as f64)),
-            Value::Str(s) => s.trim().parse::<f64>().map(Value::Float).map_err(|_| RelayError::Type("Bad float".into())),
+            Value::Str(s) => s
+                .trim()
+                .parse::<f64>()
+                .map(Value::Float)
+                .map_err(|_| RelayError::Type("Bad float".into())),
             _ => Err(RelayError::Type("Bad float".into())),
         },
         "Json" | "json" => match v {
@@ -1696,7 +1903,9 @@ fn coerce_param_type(ty: &str, v: Value) -> RResult<Value> {
                 }
                 Ok(Value::Json(J::Object(obj)))
             }
-            Value::Str(s) => serde_json::from_str::<J>(&s).map(Value::Json).map_err(|_| RelayError::Type("Bad Json".into())),
+            Value::Str(s) => serde_json::from_str::<J>(&s)
+                .map(Value::Json)
+                .map_err(|_| RelayError::Type("Bad Json".into())),
             _ => Err(RelayError::Type("Bad Json".into())),
         },
         _ => Ok(v),
@@ -1773,7 +1982,9 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
         "print",
         Value::Builtin(Arc::new(|args, _| {
             Box::pin(async move {
-                let needs_async = args.iter().any(|v| matches!(v, Value::Thunk(_) | Value::Deferred(_) | Value::Task(_)));
+                let needs_async = args
+                    .iter()
+                    .any(|v| matches!(v, Value::Thunk(_) | Value::Deferred(_) | Value::Task(_)));
                 if needs_async {
                     let d = Deferred::new(Box::pin(async move {
                         let mut out = Vec::new();
@@ -1791,12 +2002,18 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
                             };
                             out.push(resolved);
                         }
-                        println!("{}", out.iter().map(|v| v.repr()).collect::<Vec<_>>().join(" "));
+                        println!(
+                            "{}",
+                            out.iter().map(|v| v.repr()).collect::<Vec<_>>().join(" ")
+                        );
                         Ok(Value::None)
                     }));
                     Ok(Value::Deferred(Arc::new(d)))
                 } else {
-                    println!("{}", args.iter().map(|v| v.repr()).collect::<Vec<_>>().join(" "));
+                    println!(
+                        "{}",
+                        args.iter().map(|v| v.repr()).collect::<Vec<_>>().join(" ")
+                    );
                     Ok(Value::None)
                 }
             })
@@ -1806,7 +2023,13 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
     // converters: str/int/float
     env.set(
         "str",
-        Value::Builtin(Arc::new(|args, _| Box::pin(async move { Ok(Value::Str(args.get(0).cloned().unwrap_or(Value::None).repr())) }))),
+        Value::Builtin(Arc::new(|args, _| {
+            Box::pin(async move {
+                Ok(Value::Str(
+                    args.get(0).cloned().unwrap_or(Value::None).repr(),
+                ))
+            })
+        })),
     );
     env.set(
         "int",
@@ -1827,53 +2050,52 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
         })),
     );
 
-    
-// sleep(ms, value=None) -> Deferred
-// Non-blocking scheduler primitive:
-// - returns immediately with a Deferred
-// - after ms, evaluates/resolves the 2nd argument (Thunk/Deferred/Task/callable/value)
-// - prints non-None result and resolves to it
-env.set(
-    "sleep",
-    Value::Builtin(Arc::new(|args, _| {
-        Box::pin(async move {
-            let ms = expect_int(&args, 0, "sleep(ms, value?)")? as u64;
-            let value = args.get(1).cloned().unwrap_or(Value::None);
-
-            let d = Deferred::new(Box::pin(async move {
-                tokio::time::sleep(Duration::from_millis(ms)).await;
-
-                // Evaluate/resolve after delay
-                let mut out = match value {
-                    Value::Thunk(t) => t.run().await?,
-                    Value::Deferred(d) => d.resolve().await?,
-                    Value::Task(t) => t.join().await?,
-                    Value::Builtin(f) => (f)(vec![], vec![]).await?, // sleep(ms, fn)
-                    Value::Function(f) => {
-                        // call user fn with no args after delay
-                        // we cannot access evaluator here, so return the function itself;
-                        // if you want sleep(ms, user_fn) to call it, wrap as sleep(ms, spawn(user_fn()))
-                        Value::Function(f)
-                    }
-                    v => v,
-                };
+    // sleep(ms, value=None) -> Deferred
+    // Non-blocking scheduler primitive:
+    // - returns immediately with a Deferred
+    // - after ms, evaluates/resolves the 2nd argument (Thunk/Deferred/Task/callable/value)
+    // - prints non-None result and resolves to it
+    env.set(
+        "sleep",
+        Value::Builtin(Arc::new(|args, _| {
+            Box::pin(async move {
+                let ms = expect_int(&args, 0, "sleep(ms, value?)")? as u64;
+                let value = args.get(1).cloned().unwrap_or(Value::None);
 
-                // If evaluation yielded Deferred/Task, resolve now
-                out = match out {
-                    Value::Deferred(d) => d.resolve().await?,
-                    Value::Task(t) => t.join().await?,
-                    v => v,
-                };
+                let d = Deferred::new(Box::pin(async move {
+                    tokio::time::sleep(Duration::from_millis(ms)).await;
+
+                    // Evaluate/resolve after delay
+                    let mut out = match value {
+                        Value::Thunk(t) => t.run().await?,
+                        Value::Deferred(d) => d.resolve().await?,
+                        Value::Task(t) => t.join().await?,
+                        Value::Builtin(f) => (f)(vec![], vec![]).await?, // sleep(ms, fn)
+                        Value::Function(f) => {
+                            // call user fn with no args after delay
+                            // we cannot access evaluator here, so return the function itself;
+                            // if you want sleep(ms, user_fn) to call it, wrap as sleep(ms, spawn(user_fn()))
+                            Value::Function(f)
+                        }
+                        v => v,
+                    };
 
-                if !out.is_none() {
-                    println!("{}", out.repr());
-                }
-                Ok(out)
-            }));
-            Ok(Value::Deferred(Arc::new(d)))
-        })
-    })),
-);
+                    // If evaluation yielded Deferred/Task, resolve now
+                    out = match out {
+                        Value::Deferred(d) => d.resolve().await?,
+                        Value::Task(t) => t.join().await?,
+                        v => v,
+                    };
+
+                    if !out.is_none() {
+                        println!("{}", out.repr());
+                    }
+                    Ok(out)
+                }));
+                Ok(Value::Deferred(Arc::new(d)))
+            })
+        })),
+    );
     // timeout(expr, ms)
     env.set(
         "timeout",
@@ -1925,7 +2147,9 @@ env.set(
                 let text = expect_str(&args, 0, "save_file(text, path)")?;
                 let path = expect_str(&args, 1, "save_file(text, path)")?;
                 let d = Deferred::new(Box::pin(async move {
-                    tokio::fs::write(path, text).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                    tokio::fs::write(path, text)
+                        .await
+                        .map_err(|e| RelayError::Runtime(e.to_string()))?;
                     Ok(Value::None)
                 }));
                 Ok(Value::Deferred(Arc::new(d)))
@@ -1943,7 +2167,8 @@ env.set(
                     let s = tokio::fs::read_to_string(path)
                         .await
                         .map_err(|e| RelayError::Runtime(e.to_string()))?;
-                    let j: J = serde_json::from_str(&s).map_err(|e| RelayError::Runtime(e.to_string()))?;
+                    let j: J =
+                        serde_json::from_str(&s).map_err(|e| RelayError::Runtime(e.to_string()))?;
                     Ok(Value::Json(j))
                 }));
                 Ok(Value::Deferred(Arc::new(d)))
@@ -1954,7 +2179,10 @@ env.set(
         "save_json",
         Value::Builtin(Arc::new(|args, _| {
             Box::pin(async move {
-                let val = args.get(0).cloned().ok_or_else(|| RelayError::Type("save_json(data, path) missing data".into()))?;
+                let val = args
+                    .get(0)
+                    .cloned()
+                    .ok_or_else(|| RelayError::Type("save_json(data, path) missing data".into()))?;
                 let path = expect_str(&args, 1, "save_json(data, path)")?;
                 let d = Deferred::new(Box::pin(async move {
                     let j = match val {
@@ -1969,8 +2197,11 @@ env.set(
                         Value::Str(s) => serde_json::from_str::<J>(&s).unwrap_or(J::String(s)),
                         other => J::String(other.repr()),
                     };
-                    let s = serde_json::to_string_pretty(&j).map_err(|e| RelayError::Runtime(e.to_string()))?;
-                    tokio::fs::write(path, s).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                    let s = serde_json::to_string_pretty(&j)
+                        .map_err(|e| RelayError::Runtime(e.to_string()))?;
+                    tokio::fs::write(path, s)
+                        .await
+                        .map_err(|e| RelayError::Runtime(e.to_string()))?;
                     Ok(Value::None)
                 }));
                 Ok(Value::Deferred(Arc::new(d)))
@@ -2062,7 +2293,9 @@ env.set(
                     }
                     match join_set.join_next().await {
                         Some(r) => r.map_err(|e| RelayError::Runtime(e.to_string()))?,
-                        None => Err(RelayError::Runtime("race([..]) expects at least one task".into())),
+                        None => Err(RelayError::Runtime(
+                            "race([..]) expects at least one task".into(),
+                        )),
                     }
                 }));
                 Ok(Value::Deferred(Arc::new(d)))
@@ -2075,7 +2308,11 @@ env.set(
         "Response",
         Value::Builtin(Arc::new(|args, kwargs| {
             Box::pin(async move {
-                let status = kwargs.iter().find(|(k, _)| k == "status").map(|(_, v)| v.clone()).unwrap_or(Value::Int(200));
+                let status = kwargs
+                    .iter()
+                    .find(|(k, _)| k == "status")
+                    .map(|(_, v)| v.clone())
+                    .unwrap_or(Value::Int(200));
                 let ct = kwargs
                     .iter()
                     .find(|(k, _)| k == "content_type")
@@ -2096,26 +2333,37 @@ env.set(
                     Value::Json(j) => j.to_string().into_bytes(),
                     v => v.repr().into_bytes(),
                 };
-                Ok(Value::Response(Response { status, content_type: ct, body: bytes }))
+                Ok(Value::Response(Response {
+                    status,
+                    content_type: ct,
+                    body: bytes,
+                    headers: HashMap::new(),
+                    set_cookies: vec![],
+                }))
             })
         })),
     );
 
     // Web framework
-    // WebServer is a singleton "type" object in v0.1: use WebServer.run(app)
     env.set(
         "WebServer",
-        Value::Obj(Object::WebServer(WebServerHandle::new())),
+        Value::Builtin(Arc::new(|_args, _| {
+            Box::pin(async move { Ok(Value::Obj(Object::WebServer(WebServerHandle::new()))) })
+        })),
     );
     env.set(
         "WebApp",
-        Value::Builtin(Arc::new(|_args, _| Box::pin(async move { Ok(Value::Obj(Object::WebApp(WebAppHandle::new()))) }))),
+        Value::Builtin(Arc::new(|_args, _| {
+            Box::pin(async move { Ok(Value::Obj(Object::WebApp(WebAppHandle::new()))) })
+        })),
     );
 
     // HTTP client
     env.set(
         "Http",
-        Value::Builtin(Arc::new(|_args, _| Box::pin(async move { Ok(Value::Obj(Object::Http(HttpHandle::new()))) }))),
+        Value::Builtin(Arc::new(|_args, _| {
+            Box::pin(async move { Ok(Value::Obj(Object::Http(HttpHandle::new()))) })
+        })),
     );
 
     // MongoDB client
@@ -2125,8 +2373,12 @@ env.set(
             Box::pin(async move {
                 let uri = expect_str(&args, 0, "Mongo(uri)")?;
                 let d = Deferred::new(Box::pin(async move {
-                    let client = MongoClient::with_uri_str(uri).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
-                    Ok(Value::Obj(Object::MongoClient(MongoClientHandle::new(client))))
+                    let client = MongoClient::with_uri_str(uri)
+                        .await
+                        .map_err(|e| RelayError::Runtime(e.to_string()))?;
+                    Ok(Value::Obj(Object::MongoClient(MongoClientHandle::new(
+                        client,
+                    ))))
                 }));
                 Ok(Value::Deferred(Arc::new(d)))
             })
@@ -2135,9 +2387,9 @@ env.set(
 
     // Hook global callback for web requests -> interpreter calls
     // We call handler by name and inject params (Path > Body > Query)
-    set_global_callback(Arc::new(move |fn_name, req| {
+    set_global_callback(Arc::new(move |app, fn_name, req| {
         let ev = evaluator.clone();
-        Box::pin(async move { ev.call_web_handler(&fn_name, req).await })
+        Box::pin(async move { ev.call_web_handler(app, &fn_name, req).await })
     }));
 
     Ok(())
@@ -2154,7 +2406,10 @@ fn expect_int(args: &[Value], i: usize, sig: &str) -> RResult<i64> {
     match args.get(i) {
         Some(Value::Int(n)) => Ok(*n),
         Some(Value::Float(f)) => Ok(*f as i64),
-        Some(Value::Str(s)) => s.trim().parse().map_err(|_| RelayError::Type(format!("{sig} expects int"))),
+        Some(Value::Str(s)) => s
+            .trim()
+            .parse()
+            .map_err(|_| RelayError::Type(format!("{sig} expects int"))),
         _ => Err(RelayError::Type(format!("{sig} expects int"))),
     }
 }
@@ -2172,7 +2427,9 @@ fn value_to_json(v: &Value) -> J {
         Value::List(vs) => J::Array(vs.iter().map(value_to_json).collect()),
         Value::Str(s) => J::String(s.clone()),
         Value::Int(n) => J::Number((*n).into()),
-        Value::Float(f) => serde_json::Number::from_f64(*f).map(J::Number).unwrap_or(J::Null),
+        Value::Float(f) => serde_json::Number::from_f64(*f)
+            .map(J::Number)
+            .unwrap_or(J::Null),
         Value::Bool(b) => J::Bool(*b),
         Value::None => J::Null,
         Value::Bytes(b) => J::String(format!("<bytes {}>", b.len())),
@@ -2183,8 +2440,12 @@ fn value_to_json(v: &Value) -> J {
 fn value_to_document(v: Value, sig: &str) -> RResult<Document> {
     let json = value_to_json(&v);
     match json {
-        J::Object(_) => bson::to_document(&json).map_err(|e| RelayError::Runtime(format!("{sig}: {e}"))),
-        _ => Err(RelayError::Type(format!("{sig} expects a dict/json object"))),
+        J::Object(_) => {
+            bson::to_document(&json).map_err(|e| RelayError::Runtime(format!("{sig}: {e}")))
+        }
+        _ => Err(RelayError::Type(format!(
+            "{sig} expects a dict/json object"
+        ))),
     }
 }
 
@@ -2208,6 +2469,7 @@ struct WebServerHandle;
 #[derive(Default)]
 struct AppState {
     routes: Vec<RouteSpec>,
+    sessions: HashMap<String, IndexMap<String, Value>>,
 }
 #[derive(Clone)]
 struct RouteSpec {
@@ -2223,7 +2485,9 @@ impl WebServerHandle {
 }
 impl WebAppHandle {
     fn new() -> Self {
-        Self { inner: Arc::new(Mutex::new(AppState::default())) }
+        Self {
+            inner: Arc::new(Mutex::new(AppState::default())),
+        }
     }
 
     // Back-compat: route handle style (route = app.route(); @route.get("/"))
@@ -2234,26 +2498,48 @@ impl WebAppHandle {
     // New spec: decorator attaches directly to the app: @app.get("/"), @app.post("/")
     fn register(&self, method: String, path: String, fn_name: String) {
         let mut st = self.inner.lock().unwrap();
-        st.routes.push(RouteSpec { method, path, fn_name });
+        st.routes.push(RouteSpec {
+            method,
+            path,
+            fn_name,
+        });
+    }
+
+    fn get_session(&self, sid: &str) -> IndexMap<String, Value> {
+        let st = self.inner.lock().unwrap();
+        st.sessions.get(sid).cloned().unwrap_or_default()
+    }
+
+    fn put_session(&self, sid: String, data: IndexMap<String, Value>) {
+        let mut st = self.inner.lock().unwrap();
+        st.sessions.insert(sid, data);
     }
 }
 impl RouteHandle {
     fn register(&self, method: String, path: String, fn_name: String) {
         let mut st = self.app.inner.lock().unwrap();
-        st.routes.push(RouteSpec { method, path, fn_name });
+        st.routes.push(RouteSpec {
+            method,
+            path,
+            fn_name,
+        });
     }
 }
 
 // Callback from Axum -> interpreter
 #[derive(Clone)]
 struct RequestParts {
+    method: String,
+    path: String,
     path_params: HashMap<String, String>,
     query: HashMap<String, String>,
     json: Option<J>,
     headers: HashMap<String, String>,
+    cookies: HashMap<String, String>,
+    session_id: Option<String>,
 }
 
-type HandlerCb = Arc<dyn Fn(String, RequestParts) -> BoxFut + Send + Sync>;
+type HandlerCb = Arc<dyn Fn(WebAppHandle, String, RequestParts) -> BoxFut + Send + Sync>;
 static GLOBAL_CB: OnceLock<HandlerCb> = OnceLock::new();
 
 static BG_TASKS: OnceLock<tokio::sync::Mutex<Vec<tokio::task::JoinHandle<()>>>> = OnceLock::new();
@@ -2278,7 +2564,6 @@ async fn drain_bg() {
     }
 }
 
-
 fn set_global_callback(cb: HandlerCb) {
     let _ = GLOBAL_CB.set(cb);
 }
@@ -2295,15 +2580,18 @@ async fn run_app(app: WebAppHandle) -> RResult<()> {
         let method = r.method.to_lowercase();
         let path = relay_path_to_axum(&r.path);
         let fn_name = r.fn_name.clone();
+        let route_path = r.path.clone();
+        let app_handle = app.clone();
 
         // define handler in THIS scope so `get(handler)` / `post(handler)` can see it
-        let handler = move |
-            Path(ax_path): Path<HashMap<String, String>>,
-            Query(q): Query<HashMap<String, String>>,
-            headers: HeaderMap,
-            body: Option<AxumJson<J>>
-        | {
+        let handler = move |method: Method,
+                            Path(ax_path): Path<HashMap<String, String>>,
+                            Query(q): Query<HashMap<String, String>>,
+                            headers: HeaderMap,
+                            body: Option<AxumJson<J>>| {
             let fn_name = fn_name.clone();
+            let route_path = route_path.clone();
+            let app_handle = app_handle.clone();
             async move {
                 let mut h = HashMap::new();
                 for (k, v) in headers.iter() {
@@ -2311,18 +2599,28 @@ async fn run_app(app: WebAppHandle) -> RResult<()> {
                         h.insert(k.to_string(), s.to_string());
                     }
                 }
+                let cookies = parse_cookie_header(h.get("cookie").cloned());
+                let session_id = cookies.get("relay_sid").cloned();
 
                 let req = RequestParts {
+                    method: method.to_string(),
+                    path: route_path,
                     path_params: ax_path,
                     query: q,
                     json: body.map(|b| b.0),
                     headers: h,
+                    cookies,
+                    session_id,
                 };
 
-                let cb = get_global_callback()
-                    .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No relay callback".to_string()))?;
+                let cb = get_global_callback().ok_or_else(|| {
+                    (
+                        StatusCode::INTERNAL_SERVER_ERROR,
+                        "No relay callback".to_string(),
+                    )
+                })?;
 
-                let v = cb(fn_name, req)
+                let v = cb(app_handle, fn_name, req)
                     .await
                     .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
 
@@ -2333,7 +2631,14 @@ async fn run_app(app: WebAppHandle) -> RResult<()> {
         router = match method.as_str() {
             "get" => router.route(&path, get(handler)),
             "post" => router.route(&path, post(handler)),
-            _ => return Err(RelayError::Runtime("Only GET/POST supported in v0.1".into())),
+            "put" => router.route(&path, axum::routing::put(handler)),
+            "patch" => router.route(&path, axum::routing::patch(handler)),
+            "delete" => router.route(&path, axum::routing::delete(handler)),
+            _ => {
+                return Err(RelayError::Runtime(format!(
+                    "Unsupported HTTP method: {method}"
+                )))
+            }
         };
     }
 
@@ -2348,6 +2653,31 @@ async fn run_app(app: WebAppHandle) -> RResult<()> {
     Ok(())
 }
 
+fn parse_cookie_header(raw: Option<String>) -> HashMap<String, String> {
+    let mut out = HashMap::new();
+    if let Some(v) = raw {
+        for seg in v.split(';') {
+            let part = seg.trim();
+            if part.is_empty() {
+                continue;
+            }
+            if let Some((k, vv)) = part.split_once('=') {
+                out.insert(k.trim().to_string(), vv.trim().to_string());
+            }
+        }
+    }
+    out
+}
+
+fn new_session_id() -> String {
+    use std::time::{SystemTime, UNIX_EPOCH};
+    let now = SystemTime::now()
+        .duration_since(UNIX_EPOCH)
+        .unwrap_or_default()
+        .as_nanos();
+    format!("rsid_{now:x}")
+}
+
 fn relay_path_to_axum(p: &str) -> String {
     // "/api/<user_id>/x" -> "/api/:user_id/x"
     let mut out = String::new();
@@ -2372,29 +2702,94 @@ fn relay_path_to_axum(p: &str) -> String {
 async fn value_to_axum_response(v: Value) -> axum::response::Response {
     let r = match v {
         Value::Response(r) => r,
-        Value::Json(j) => Response { status: 200, content_type: "application/json".into(), body: j.to_string().into_bytes() },
+        Value::Json(j) => Response {
+            status: 200,
+            content_type: "application/json".into(),
+            body: j.to_string().into_bytes(),
+            headers: HashMap::new(),
+            set_cookies: vec![],
+        },
         Value::Dict(m) => {
             // dict/list => JSON
             let mut obj = serde_json::Map::new();
             for (k, vv) in m {
                 obj.insert(k, J::String(vv.repr()));
             }
-            Response { status: 200, content_type: "application/json".into(), body: J::Object(obj).to_string().into_bytes() }
+            Response {
+                status: 200,
+                content_type: "application/json".into(),
+                body: J::Object(obj).to_string().into_bytes(),
+                headers: HashMap::new(),
+                set_cookies: vec![],
+            }
         }
         Value::List(vs) => {
-            let arr = vs.into_iter().map(|x| J::String(x.repr())).collect::<Vec<_>>();
-            Response { status: 200, content_type: "application/json".into(), body: J::Array(arr).to_string().into_bytes() }
-        }
-        Value::Bytes(b) => Response { status: 200, content_type: "application/octet-stream".into(), body: b },
+            let arr = vs
+                .into_iter()
+                .map(|x| J::String(x.repr()))
+                .collect::<Vec<_>>();
+            Response {
+                status: 200,
+                content_type: "application/json".into(),
+                body: J::Array(arr).to_string().into_bytes(),
+                headers: HashMap::new(),
+                set_cookies: vec![],
+            }
+        }
+        Value::Bytes(b) => Response {
+            status: 200,
+            content_type: "application/octet-stream".into(),
+            body: b,
+            headers: HashMap::new(),
+            set_cookies: vec![],
+        },
         Value::Str(s) => {
             // template rendering happens in handler; here pick content-type
-            let ct = if s.contains("<") || s.contains("{{") { "text/html; charset=utf-8" } else { "text/plain; charset=utf-8" };
-            Response { status: 200, content_type: ct.into(), body: s.into_bytes() }
-        }
-        other => Response { status: 200, content_type: "text/plain; charset=utf-8".into(), body: other.repr().into_bytes() },
+            let ct = if s.contains("<") || s.contains("{{") {
+                "text/html; charset=utf-8"
+            } else {
+                "text/plain; charset=utf-8"
+            };
+            Response {
+                status: 200,
+                content_type: ct.into(),
+                body: s.into_bytes(),
+                headers: HashMap::new(),
+                set_cookies: vec![],
+            }
+        }
+        other => Response {
+            status: 200,
+            content_type: "text/plain; charset=utf-8".into(),
+            body: other.repr().into_bytes(),
+            headers: HashMap::new(),
+            set_cookies: vec![],
+        },
     };
 
-    (StatusCode::from_u16(r.status).unwrap_or(StatusCode::OK), [(axum::http::header::CONTENT_TYPE, r.content_type)], r.body).into_response()
+    {
+        let mut resp = (
+            StatusCode::from_u16(r.status).unwrap_or(StatusCode::OK),
+            [(axum::http::header::CONTENT_TYPE, r.content_type)],
+            r.body,
+        )
+            .into_response();
+        for (k, v) in r.headers {
+            if let (Ok(hn), Ok(hv)) = (
+                axum::http::header::HeaderName::from_bytes(k.as_bytes()),
+                axum::http::header::HeaderValue::from_str(&v),
+            ) {
+                resp.headers_mut().insert(hn, hv);
+            }
+        }
+        for cookie in r.set_cookies {
+            if let Ok(hv) = axum::http::header::HeaderValue::from_str(&cookie) {
+                resp.headers_mut()
+                    .append(axum::http::header::SET_COOKIE, hv);
+            }
+        }
+        resp
+    }
 }
 
 // ========================= HTTP Client =========================
@@ -2405,7 +2800,9 @@ struct HttpHandle {
 }
 impl HttpHandle {
     fn new() -> Self {
-        Self { client: reqwest::Client::new() }
+        Self {
+            client: reqwest::Client::new(),
+        }
     }
     fn get_member(&self, name: &str) -> Value {
         // bound method: (args, kwargs) -> Deferred<HttpResponse>
@@ -2416,15 +2813,27 @@ impl HttpHandle {
                 Box::pin(async move {
                     let url = expect_str(&args, 0, "http.get(url)")?;
                     let d = Deferred::new(Box::pin(async move {
-                        let resp = client.get(url).send().await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                        let resp = client
+                            .get(url)
+                            .send()
+                            .await
+                            .map_err(|e| RelayError::Runtime(e.to_string()))?;
                         let status = resp.status().as_u16();
                         let headers = resp
                             .headers()
                             .iter()
                             .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
                             .collect::<HashMap<_, _>>();
-                        let bytes = resp.bytes().await.map_err(|e| RelayError::Runtime(e.to_string()))?.to_vec();
-                        Ok(Value::Obj(Object::HttpResponse(HttpResponseHandle { status, headers, body: bytes })))
+                        let bytes = resp
+                            .bytes()
+                            .await
+                            .map_err(|e| RelayError::Runtime(e.to_string()))?
+                            .to_vec();
+                        Ok(Value::Obj(Object::HttpResponse(HttpResponseHandle {
+                            status,
+                            headers,
+                            body: bytes,
+                        })))
                     }));
                     Ok(Value::Deferred(Arc::new(d)))
                 })
@@ -2449,15 +2858,26 @@ impl HttpHandle {
                             Value::Bytes(b) => req.body(b),
                             other => req.body(other.repr()),
                         };
-                        let resp = req.send().await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                        let resp = req
+                            .send()
+                            .await
+                            .map_err(|e| RelayError::Runtime(e.to_string()))?;
                         let status = resp.status().as_u16();
                         let headers = resp
                             .headers()
                             .iter()
                             .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
                             .collect::<HashMap<_, _>>();
-                        let bytes = resp.bytes().await.map_err(|e| RelayError::Runtime(e.to_string()))?.to_vec();
-                        Ok(Value::Obj(Object::HttpResponse(HttpResponseHandle { status, headers, body: bytes })))
+                        let bytes = resp
+                            .bytes()
+                            .await
+                            .map_err(|e| RelayError::Runtime(e.to_string()))?
+                            .to_vec();
+                        Ok(Value::Obj(Object::HttpResponse(HttpResponseHandle {
+                            status,
+                            headers,
+                            body: bytes,
+                        })))
                     }));
                     Ok(Value::Deferred(Arc::new(d)))
                 })
@@ -2487,7 +2907,8 @@ impl HttpResponseHandle {
                     let body = body.clone();
                     Box::pin(async move {
                         let s = String::from_utf8_lossy(&body).to_string();
-                        let j: J = serde_json::from_str(&s).map_err(|e| RelayError::Runtime(e.to_string()))?;
+                        let j: J = serde_json::from_str(&s)
+                            .map_err(|e| RelayError::Runtime(e.to_string()))?;
                         Ok(Value::Json(j))
                     })
                 }))
@@ -2524,7 +2945,9 @@ impl MongoClientHandle {
                     let client = client.clone();
                     Box::pin(async move {
                         let name = expect_str(&args, 0, "mongo.db(name)")?;
-                        Ok(Value::Obj(Object::MongoDatabase(MongoDatabaseHandle { db: client.database(&name) })))
+                        Ok(Value::Obj(Object::MongoDatabase(MongoDatabaseHandle {
+                            db: client.database(&name),
+                        })))
                     })
                 }))
             }
@@ -2542,7 +2965,9 @@ impl MongoDatabaseHandle {
                     let db = db.clone();
                     Box::pin(async move {
                         let name = expect_str(&args, 0, "db.collection(name)")?;
-                        Ok(Value::Obj(Object::MongoCollection(MongoCollectionHandle { collection: db.collection::<Document>(&name) })))
+                        Ok(Value::Obj(Object::MongoCollection(MongoCollectionHandle {
+                            collection: db.collection::<Document>(&name),
+                        })))
                     })
                 }))
             }
@@ -2559,9 +2984,15 @@ impl MongoCollectionHandle {
                 Value::Builtin(Arc::new(move |args, _| {
                     let collection = collection.clone();
                     Box::pin(async move {
-                        let doc = value_to_document(args.get(0).cloned().unwrap_or(Value::None), "collection.insert_one(doc)")?;
+                        let doc = value_to_document(
+                            args.get(0).cloned().unwrap_or(Value::None),
+                            "collection.insert_one(doc)",
+                        )?;
                         let d = Deferred::new(Box::pin(async move {
-                            let result = collection.insert_one(doc, None).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                            let result = collection
+                                .insert_one(doc, None)
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
                             let mut obj = serde_json::Map::new();
                             obj.insert("inserted_id".into(), bson_to_json(result.inserted_id)?);
                             Ok(Value::Json(J::Object(obj)))
@@ -2577,14 +3008,21 @@ impl MongoCollectionHandle {
                     Box::pin(async move {
                         let list = match args.get(0) {
                             Some(Value::List(v)) => v.clone(),
-                            _ => return Err(RelayError::Type("collection.insert_many(docs) expects a list".into())),
+                            _ => {
+                                return Err(RelayError::Type(
+                                    "collection.insert_many(docs) expects a list".into(),
+                                ))
+                            }
                         };
                         let mut docs = Vec::with_capacity(list.len());
                         for v in list {
                             docs.push(value_to_document(v, "collection.insert_many(docs)")?);
                         }
                         let d = Deferred::new(Box::pin(async move {
-                            let result = collection.insert_many(docs, None).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                            let result = collection
+                                .insert_many(docs, None)
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
                             let mut obj = serde_json::Map::new();
                             let mut ids = serde_json::Map::new();
                             for (k, v) in result.inserted_ids {
@@ -2602,9 +3040,15 @@ impl MongoCollectionHandle {
                 Value::Builtin(Arc::new(move |args, _| {
                     let collection = collection.clone();
                     Box::pin(async move {
-                        let filter = value_to_document(args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())), "collection.find_one(filter)")?;
+                        let filter = value_to_document(
+                            args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())),
+                            "collection.find_one(filter)",
+                        )?;
                         let d = Deferred::new(Box::pin(async move {
-                            let result = collection.find_one(filter, None).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                            let result = collection
+                                .find_one(filter, None)
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
                             match result {
                                 Some(doc) => Ok(Value::Json(bson_to_json(Bson::Document(doc))?)),
                                 None => Ok(Value::None),
@@ -2619,11 +3063,21 @@ impl MongoCollectionHandle {
                 Value::Builtin(Arc::new(move |args, _| {
                     let collection = collection.clone();
                     Box::pin(async move {
-                        let filter = value_to_document(args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())), "collection.find(filter)")?;
+                        let filter = value_to_document(
+                            args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())),
+                            "collection.find(filter)",
+                        )?;
                         let d = Deferred::new(Box::pin(async move {
-                            let mut cursor = collection.find(filter, None).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                            let mut cursor = collection
+                                .find(filter, None)
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
                             let mut out = Vec::new();
-                            while let Some(doc) = cursor.try_next().await.map_err(|e| RelayError::Runtime(e.to_string()))? {
+                            while let Some(doc) = cursor
+                                .try_next()
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?
+                            {
                                 out.push(Value::Json(bson_to_json(Bson::Document(doc))?));
                             }
                             Ok(Value::List(out))
@@ -2638,15 +3092,32 @@ impl MongoCollectionHandle {
                     let collection = collection.clone();
                     Box::pin(async move {
                         if args.len() < 2 {
-                            return Err(RelayError::Type("collection.update_one(filter, update)".into()));
+                            return Err(RelayError::Type(
+                                "collection.update_one(filter, update)".into(),
+                            ));
                         }
-                        let filter = value_to_document(args[0].clone(), "collection.update_one(filter, update)")?;
-                        let update = value_to_document(args[1].clone(), "collection.update_one(filter, update)")?;
+                        let filter = value_to_document(
+                            args[0].clone(),
+                            "collection.update_one(filter, update)",
+                        )?;
+                        let update = value_to_document(
+                            args[1].clone(),
+                            "collection.update_one(filter, update)",
+                        )?;
                         let d = Deferred::new(Box::pin(async move {
-                            let result = collection.update_one(filter, update, None).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                            let result = collection
+                                .update_one(filter, update, None)
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
                             let mut obj = serde_json::Map::new();
-                            obj.insert("matched_count".into(), J::Number(result.matched_count.into()));
-                            obj.insert("modified_count".into(), J::Number(result.modified_count.into()));
+                            obj.insert(
+                                "matched_count".into(),
+                                J::Number(result.matched_count.into()),
+                            );
+                            obj.insert(
+                                "modified_count".into(),
+                                J::Number(result.modified_count.into()),
+                            );
                             if let Some(id) = result.upserted_id {
                                 obj.insert("upserted_id".into(), bson_to_json(id)?);
                             }
@@ -2661,9 +3132,15 @@ impl MongoCollectionHandle {
                 Value::Builtin(Arc::new(move |args, _| {
                     let collection = collection.clone();
                     Box::pin(async move {
-                        let filter = value_to_document(args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())), "collection.delete_one(filter)")?;
+                        let filter = value_to_document(
+                            args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())),
+                            "collection.delete_one(filter)",
+                        )?;
                         let d = Deferred::new(Box::pin(async move {
-                            let result = collection.delete_one(filter, None).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                            let result = collection
+                                .delete_one(filter, None)
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
                             Ok(Value::Int(result.deleted_count as i64))
                         }));
                         Ok(Value::Deferred(Arc::new(d)))
@@ -2675,9 +3152,15 @@ impl MongoCollectionHandle {
                 Value::Builtin(Arc::new(move |args, _| {
                     let collection = collection.clone();
                     Box::pin(async move {
-                        let filter = value_to_document(args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())), "collection.delete_many(filter)")?;
+                        let filter = value_to_document(
+                            args.get(0).cloned().unwrap_or(Value::Dict(IndexMap::new())),
+                            "collection.delete_many(filter)",
+                        )?;
                         let d = Deferred::new(Box::pin(async move {
-                            let result = collection.delete_many(filter, None).await.map_err(|e| RelayError::Runtime(e.to_string()))?;
+                            let result = collection
+                                .delete_many(filter, None)
+                                .await
+                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
                             Ok(Value::Int(result.deleted_count as i64))
                         }));
                         Ok(Value::Deferred(Arc::new(d)))
@@ -2703,6 +3186,32 @@ impl Object {
                         Box::pin(async move { Ok(Value::Obj(Object::Route(a.route()))) })
                     }))
                 }
+                "redirect" => Value::Builtin(Arc::new(move |args, _| {
+                    Box::pin(async move {
+                        let location = expect_str(&args, 0, "app.redirect(location)")?;
+                        let mut headers = HashMap::new();
+                        headers.insert("location".into(), location);
+                        Ok(Value::Response(Response {
+                            status: 302,
+                            content_type: "text/plain; charset=utf-8".into(),
+                            body: Vec::new(),
+                            headers,
+                            set_cookies: vec![],
+                        }))
+                    })
+                })),
+                "json" => Value::Builtin(Arc::new(move |args, _| {
+                    Box::pin(async move {
+                        let body = args.get(0).cloned().unwrap_or(Value::None);
+                        Ok(Value::Response(Response {
+                            status: 200,
+                            content_type: "application/json".into(),
+                            body: value_to_json(&body).to_string().into_bytes(),
+                            headers: HashMap::new(),
+                            set_cookies: vec![],
+                        }))
+                    })
+                })),
                 _ => Value::None,
             },
             Object::Route(_r) => Value::None, // decorators handle route registration
@@ -2714,7 +3223,11 @@ impl Object {
                         Box::pin(async move {
                             let app = match args.get(0) {
                                 Some(Value::Obj(Object::WebApp(a))) => a.clone(),
-                                _ => return Err(RelayError::Type("server.run(app) expects a WebApp".into())),
+                                _ => {
+                                    return Err(RelayError::Type(
+                                        "server.run(app) expects a WebApp".into(),
+                                    ))
+                                }
                             };
                             s.run(app).await
                         })
@@ -2748,25 +3261,48 @@ impl WebServerHandle {
 // ========================= Web handler injection + smart returns =========================
 
 impl Evaluator {
-    async fn call_web_handler(&self, fn_name: &str, req: RequestParts) -> RResult<Value> {
+    async fn call_web_handler(
+        &self,
+        app: WebAppHandle,
+        fn_name: &str,
+        req: RequestParts,
+    ) -> RResult<Value> {
         // Path > Body > Query precedence, and "data: Json" style typing
         let f = { self.env.lock().await.get(fn_name)? };
         let f = self.resolve_if_needed(f).await?;
 
         let func = match f {
             Value::Function(f) => f,
-            _ => return Err(RelayError::Type(format!("Handler {fn_name} is not a function"))),
+            _ => {
+                return Err(RelayError::Type(format!(
+                    "Handler {fn_name} is not a function"
+                )))
+            }
         };
 
+        let RequestParts {
+            method,
+            path,
+            path_params,
+            query,
+            json,
+            headers,
+            cookies,
+            session_id,
+        } = req;
+
+        let query_map = query.clone();
+        let json_body = json.clone();
+
         // build arg map by param list
         let mut bound: HashMap<String, Value> = HashMap::new();
 
         // start with Query
-        for (k, v) in req.query {
+        for (k, v) in query {
             bound.insert(k, Value::Str(v));
         }
         // then Body
-        if let Some(j) = req.json {
+        if let Some(j) = json {
             // if handler has a Json param name, bind it (first param typed Json)
             let mut json_param_name = None;
             for p in &func.params {
@@ -2776,14 +3312,14 @@ impl Evaluator {
                 }
             }
             if let Some(n) = json_param_name {
-                bound.insert(n, Value::Json(j));
+                bound.insert(n, Value::Json(j.clone()));
             } else {
                 // default: "data"
-                bound.insert("data".into(), Value::Json(j));
+                bound.insert("data".into(), Value::Json(j.clone()));
             }
         }
         // then Path overrides
-        for (k, v) in req.path_params {
+        for (k, v) in path_params {
             bound.insert(k, Value::Str(v));
         }
 
@@ -2791,7 +3327,10 @@ impl Evaluator {
         for p in &func.params {
             if !bound.contains_key(&p.name) {
                 if let Some(def) = &p.default {
-                    bound.insert(p.name.clone(), self.resolve_if_needed(self.eval_expr(def).await?).await?);
+                    bound.insert(
+                        p.name.clone(),
+                        self.resolve_if_needed(self.eval_expr(def).await?).await?,
+                    );
                 } else {
                     bound.insert(p.name.clone(), Value::None);
                 }
@@ -2806,6 +3345,45 @@ impl Evaluator {
             }
         }
 
+        let mut req_dict = IndexMap::new();
+        req_dict.insert("method".into(), Value::Str(method));
+        req_dict.insert("path".into(), Value::Str(path));
+        req_dict.insert(
+            "query".into(),
+            Value::Dict(
+                query_map
+                    .iter()
+                    .map(|(k, v)| (k.clone(), Value::Str(v.clone())))
+                    .collect(),
+            ),
+        );
+        req_dict.insert(
+            "headers".into(),
+            Value::Dict(
+                headers
+                    .iter()
+                    .map(|(k, v)| (k.clone(), Value::Str(v.clone())))
+                    .collect(),
+            ),
+        );
+        req_dict.insert(
+            "cookies".into(),
+            Value::Dict(
+                cookies
+                    .iter()
+                    .map(|(k, v)| (k.clone(), Value::Str(v.clone())))
+                    .collect(),
+            ),
+        );
+        if let Some(j) = json_body {
+            req_dict.insert("json".into(), Value::Json(j));
+        }
+
+        let existing_session = session_id
+            .as_ref()
+            .map(|sid| app.get_session(sid))
+            .unwrap_or_default();
+
         // run handler in new scope
         {
             let mut env = self.env.lock().await;
@@ -2813,6 +3391,17 @@ impl Evaluator {
             for (k, v) in &bound {
                 env.set(k, v.clone());
             }
+            env.set("request", Value::Dict(req_dict));
+            env.set(
+                "cookies",
+                Value::Dict(
+                    cookies
+                        .iter()
+                        .map(|(k, v)| (k.clone(), Value::Str(v.clone())))
+                        .collect(),
+                ),
+            );
+            env.set("session", Value::Dict(existing_session.clone()));
         }
 
         let out = match self.eval_block(&func.body).await? {
@@ -2821,13 +3410,20 @@ impl Evaluator {
         };
 
         // template strings: render with locals if string contains {{ }}
-        let rendered = {
+        let (rendered, final_session) = {
             let env = self.env.lock().await;
             let locals = env.snapshot_top();
-            match out {
-                Value::Str(s) if s.contains("{{") && s.contains("}}") => Value::Str(render_template(&s, &locals)),
+            let session = match locals.get("session") {
+                Some(Value::Dict(m)) => m.clone(),
+                _ => existing_session,
+            };
+            let rendered = match out {
+                Value::Str(s) if s.contains("{{") && s.contains("}}") => {
+                    Value::Str(render_template(&s, &locals))
+                }
                 other => other,
-            }
+            };
+            (rendered, session)
         };
 
         {
@@ -2835,9 +3431,80 @@ impl Evaluator {
             env.pop();
         }
 
-        // smart returns:
-        // dict/list/json => JSON response; string => text/plain or html; bytes => binary; Response => manual
-        Ok(rendered)
+        let mut response = match rendered {
+            Value::Response(r) => r,
+            Value::Json(j) => Response {
+                status: 200,
+                content_type: "application/json".into(),
+                body: j.to_string().into_bytes(),
+                headers: HashMap::new(),
+                set_cookies: vec![],
+            },
+            Value::Dict(m) => {
+                let mut obj = serde_json::Map::new();
+                for (k, vv) in m {
+                    obj.insert(k, J::String(vv.repr()));
+                }
+                Response {
+                    status: 200,
+                    content_type: "application/json".into(),
+                    body: J::Object(obj).to_string().into_bytes(),
+                    headers: HashMap::new(),
+                    set_cookies: vec![],
+                }
+            }
+            Value::List(vs) => {
+                let arr = vs
+                    .into_iter()
+                    .map(|x| J::String(x.repr()))
+                    .collect::<Vec<_>>();
+                Response {
+                    status: 200,
+                    content_type: "application/json".into(),
+                    body: J::Array(arr).to_string().into_bytes(),
+                    headers: HashMap::new(),
+                    set_cookies: vec![],
+                }
+            }
+            Value::Bytes(b) => Response {
+                status: 200,
+                content_type: "application/octet-stream".into(),
+                body: b,
+                headers: HashMap::new(),
+                set_cookies: vec![],
+            },
+            Value::Str(s) => {
+                let ct = if s.contains("<") || s.contains("{{") {
+                    "text/html; charset=utf-8"
+                } else {
+                    "text/plain; charset=utf-8"
+                };
+                Response {
+                    status: 200,
+                    content_type: ct.into(),
+                    body: s.into_bytes(),
+                    headers: HashMap::new(),
+                    set_cookies: vec![],
+                }
+            }
+            other => Response {
+                status: 200,
+                content_type: "text/plain; charset=utf-8".into(),
+                body: other.repr().into_bytes(),
+                headers: HashMap::new(),
+                set_cookies: vec![],
+            },
+        };
+
+        if !final_session.is_empty() {
+            let sid = session_id.unwrap_or_else(new_session_id);
+            app.put_session(sid.clone(), final_session);
+            response
+                .set_cookies
+                .push(format!("relay_sid={sid}; Path=/; HttpOnly; SameSite=Lax"));
+        }
+
+        Ok(Value::Response(response))
     }
 }
 
@@ -2871,7 +3538,10 @@ async fn main() -> anyhow::Result<()> {
     }
 
     // run program (this defines functions, registers decorators if route exists before defs)
-    let result = evaluator.eval_program(&program).await.map_err(anyhow::Error::msg)?;
+    let result = evaluator
+        .eval_program(&program)
+        .await
+        .map_err(anyhow::Error::msg)?;
 
     // Wait for any background Deferred/Task spawned by expression statements
     drain_bg().await;