Cannoli Web Framework
Build web applications with Strada's built-in web framework.
📦 Download Cannoli on GitHub
Overview
Cannoli is Strada's built-in web framework for building HTTP servers. It provides:
Routing
URL path matching and HTTP method handling
Request Parsing
Query params, form data, JSON bodies, headers, cookies
Response Building
JSON, HTML, redirects, custom headers, cookies
File Uploads
Multipart form-data parsing for file uploads
SSL/TLS
HTTPS support with certificate configuration
Static Files
Built-in static file serving with directory listing
Quick Start
func handle(scalar $c) hash {
my str $path = $c->path();
if ($path eq "/") {
return $c->json({
"message" => "Hello, Cannoli!"
});
}
if ($path eq "/greet") {
my str $name = $c->param("name");
if ($name eq "") {
$name = "World";
}
return $c->text("Hello, " . $name . "!");
}
return $c->not_found();
}
func main() int {
Cannoli::run(8080, \&handle);
return 0;
}
Run with:
./strada -r myapp.strada
Visit http://localhost:8080/ to see your app.
The Cannoli Context Object
Every request handler receives a Cannoli context object ($c) with methods for accessing request data and building responses.
Request Methods
| Method | Description |
|---|---|
$c->path() | Get request path (e.g., "/users") |
$c->method() | Get HTTP method (GET, POST, etc.) |
$c->param($name) | Get query/form parameter |
$c->params() | Get all parameters as hash |
$c->body() | Get raw request body |
$c->json_body() | Parse body as JSON |
$c->header($name) | Get request header |
$c->headers() | Get all headers as hash |
$c->cookie($name) | Get cookie value |
$c->cookies() | Get all cookies as hash |
$c->user_agent() | Get User-Agent header |
$c->host() | Get Host header |
$c->remote_addr() | Get client IP address |
$c->is_ajax() | Check for XMLHttpRequest |
$c->accepts_json() | Check Accept header for JSON |
Response Methods
| Method | Description |
|---|---|
$c->status($code) | Set HTTP status code |
$c->content_type($type) | Set Content-Type header |
$c->set_header($name, $val) | Set response header |
$c->set_cookie($name, $val, $opts) | Set cookie |
$c->write_body($data) | Write to response body |
$c->build_response() | Build final response hash |
Response Shortcuts
| Method | Description |
|---|---|
$c->json($data) | Return JSON response (200) |
$c->text($str) | Return plain text response |
$c->html($str) | Return HTML response |
$c->redirect($url) | Redirect (302) |
$c->redirect_permanent($url) | Redirect (301) |
$c->not_found() | Return 404 response |
$c->error($code, $msg) | Return error response |
Routing Examples
func handle(scalar $c) hash {
my str $path = $c->path();
my str $method = $c->method();
# Static routes
if ($path eq "/") {
return $c->html("<h1>Welcome</h1>");
}
# Method-based routing
if ($path eq "/users") {
if ($method eq "GET") {
return list_users($c);
}
if ($method eq "POST") {
return create_user($c);
}
}
# Path matching with regex
if ($path =~ /^\/users\/([0-9]+)$/) {
my str $id = $1;
return get_user($c, $id);
}
# Path prefix matching
if ($path =~ /^\/api\//) {
return handle_api($c);
}
return $c->not_found();
}
Request Data
Query Parameters
# GET /search?q=hello&limit=10
func handle_search(scalar $c) hash {
my str $query = $c->param("q");
my str $limit = $c->param("limit");
if ($limit eq "") {
$limit = "20"; # Default value
}
return $c->json({
"query" => $query,
"limit" => int($limit)
});
}
JSON Body
# POST /api/users with JSON body
func create_user(scalar $c) hash {
my hash %data = $c->json_body();
my str $name = $data{"name"};
my str $email = $data{"email"};
if ($name eq "" || $email eq "") {
return $c->error(400, "Name and email required");
}
# Save user...
$c->status(201);
return $c->json({
"id" => 123,
"name" => $name,
"email" => $email
});
}
Form Data
# POST /login with form data
func handle_login(scalar $c) hash {
my str $username = $c->param("username");
my str $password = $c->param("password");
if (authenticate($username, $password)) {
$c->set_cookie("session", "abc123", "Path=/; HttpOnly");
return $c->redirect("/dashboard");
}
return $c->error(401, "Invalid credentials");
}
File Uploads
func handle_upload(scalar $c) hash {
if ($c->has_file("avatar") == 0) {
return $c->error(400, "No file uploaded");
}
my str $filename = $c->file_name("avatar");
my int $size = $c->file_size("avatar");
my str $content_type = $c->file_type("avatar");
my str $content = $c->file_content("avatar");
# Validate file type
if ($content_type !~ /^image\//) {
return $c->error(400, "Only images allowed");
}
# Save file
spew("/uploads/" . $filename, $content);
return $c->json({
"filename" => $filename,
"size" => $size,
"type" => $content_type
});
}
Test with curl:
curl -F "avatar=@photo.jpg" http://localhost:8080/upload
Headers and Cookies
Request Headers
func handle(scalar $c) hash {
my str $auth = $c->header("Authorization");
my str $ua = $c->user_agent();
if ($auth eq "") {
return $c->error(401, "Authorization required");
}
# Validate token...
return $c->json({"ok" => 1});
}
Response Headers
func handle(scalar $c) hash {
# Set custom headers
$c->set_header("X-Custom-Header", "value");
# CORS headers
$c->set_header("Access-Control-Allow-Origin", "*");
# Cache control
$c->set_header("Cache-Control", "max-age=3600");
return $c->json({"data" => "here"});
}
Cookies
# Reading cookies
my str $session = $c->cookie("session");
# Setting cookies
$c->set_cookie("session", "abc123", "Path=/; HttpOnly; Secure");
# Delete cookie (set expiry in past)
$c->set_cookie("session", "", "Path=/; Max-Age=0");
Chunked Responses (Streaming)
func handle_stream(scalar $c) hash {
$c->status(200);
$c->content_type("text/plain");
$c->start_chunked(); # Sends headers
# Stream data in chunks
for (my int $i = 0; $i < 5; $i++) {
$c->write_chunk("Chunk " . $i . "\n");
sys::sleep(1);
}
$c->end_chunked(); # Sends final 0\r\n\r\n
return $c->build_response();
}
HTTPS/SSL
Cannoli supports HTTPS with certificate configuration:
# Run with SSL
./cannoli --ssl --cert cert.pem --key key.pem --port 8443
Or programmatically:
func main() int {
my hash %opts = {
"port" => 8443,
"ssl" => 1,
"cert" => "/path/to/cert.pem",
"key" => "/path/to/key.pem"
};
Cannoli::run_with_opts(%opts, \&handle);
return 0;
}
Static File Server
Serve static files with directory listing:
# Command line
./cannoli --static /var/www/html --listing --port 8080
# Shorthand (positional argument)
./cannoli /var/www/html --listing
Complete Example
# A simple JSON API
my array @todos = [];
my int $next_id = 1;
func handle(scalar $c) hash {
my str $path = $c->path();
my str $method = $c->method();
# CORS for all requests
$c->set_header("Access-Control-Allow-Origin", "*");
# List all todos
if ($path eq "/todos" && $method eq "GET") {
return $c->json({ "todos" => @todos });
}
# Create todo
if ($path eq "/todos" && $method eq "POST") {
my hash %body = $c->json_body();
my hash %todo = {
"id" => $next_id,
"title" => $body{"title"},
"done" => 0
};
$next_id++;
push(@todos, %todo);
$c->status(201);
return $c->json(%todo);
}
return $c->not_found();
}
func main() int {
say("Starting server on http://localhost:8080");
Cannoli::run(8080, \&handle);
return 0;
}