///|
/// # mbit — A fast, lightweight web framework for MoonBit
///
/// ## Quick Start
///
/// ```moonbit
/// async fn main {
/// let app = @mbit.default()
///
/// app.get("/", [fn(ctx) { ctx.string(200, "Hello, mbit!") }])
/// app.get("/api/json", [fn(ctx) {
/// ctx.json(200, Json::object({
/// "message": Json::string("hello"),
/// }))
/// }])
/// app.run("0.0.0.0:8080")
/// }
/// ```
///
/// Or use the `Mbit` convenience wrapper:
///
/// ```moonbit
/// async fn main {
/// let a = Mbit::default()
/// a.get("/", [handler])
/// a.use(logger())
/// a.run(":8080")
/// }
/// ```
///
/// ## Features
///
/// ### Routing
/// - Trie-based router with `:param` and `*wildcard` segment matching
/// - Route groups with shared prefix and middleware inheritance
/// - ANY method routes (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`)
/// - Custom 404 / 405 error handlers
/// - `IRoutes` trait for common route registration API
///
/// ### Rendering
/// - JSON / IndentedJSON / SecureJSON / PureJSON / AsciiJSON / JSONP
/// - XML with custom root tag
/// - YAML with nested object support
/// - HTML / plain text / custom content type
/// - Server-Sent Events (SSE) with id, retry, comment, keepalive
/// - Streaming responses and `DataFromReader`
/// - File serving, file attachment, redirect, 204 No Content
/// - `Render` trait for custom content-type renderers
/// - TOML and ProtoBuf (placeholder delegates)
///
/// ### Request Binding
/// - JSON, query string, form-encoded, header, URI path binding
/// - `ShouldBind` / `MustBind` variants (abort on failure)
/// - `ShouldBindWith` / `MustBindWith` for custom decoders
/// - `ShouldBindBodyWith` for raw body decoding
/// - Field validation framework (`Rule` enum, `validate_field`, `validate_map`)
///
/// ### Middleware (built-in)
/// - `logger` — request logging with method, path, status, latency
/// - `recovery` — panic recovery returning 500 JSON
/// - `cors` — configurable CORS headers
/// - `auth` — HTTP Basic authentication
/// - `ratelimit` — IP-based rate limiting
/// - `requestid` / `timing` / `body_size_limit` — utility middleware
/// - `gzip` — response compression
/// - `secure` — security headers (CSP, HSTS, X-Frame-Options, etc.)
///
/// ### Server
/// - `run(addr)` / `run_default()` — HTTP server
/// - `run_tls(addr, cert, key)` — HTTPS (TLS)
/// - `run_unix(path)` — Unix domain socket
/// - `run_fd(fd)` — file descriptor (systemd socket activation)
/// - `run_listener(handler)` — custom listener
/// - `run_with_shutdown(addr)` — graceful shutdown on OS signals
/// - `shutdown()` — manual graceful shutdown
///
/// ### Templates
/// - `LoadHTMLGlob` / `LoadHTMLFiles` — load template files
/// - `SetHTMLTemplate` — inline template registration
/// - `SetTemplateDelims` — custom delimiters
/// - `SetFuncMap` — template function registration
///
/// ### File Upload
/// - `FormFile` — read uploaded file content
/// - `SaveUploadedFile` — save to disk
/// - `ParseMultipartForm` — full multipart form parsing with `FileHeader`
///
/// ### Context Utilities
/// - `Copy(detach)` — deep copy for async background tasks
/// - Key-value store (`Set`/`Get`/`MustGet`)
/// - Cookie read/write
/// - Content negotiation (`NegotiateFormat`)
/// - Client IP detection (X-Forwarded-For, X-Real-IP)
/// - WebSocket upgrade detection
/// - Error collection
///
/// ### Testing
/// - `create_test_context` — in-memory test context
/// - `TestConn` — captures status, headers, body
/// - 427 tests, 100% pass rate
///
/// ## Middleware Pattern
///
/// ```moonbit
/// fn auth(ctx : Context) {
/// match ctx.header("Authorization") {
/// Some(token) => {
/// ctx.set("token", Json::string(token))
/// ctx.next()
/// }
/// None => ctx.abort_with_status(401, "Unauthorized"),
/// }
/// }
/// ```
///|
/// Engine is the top-level framework instance.
pub(all) struct Engine {
router : Router
mut handle_method_not_allowed : Bool
mut redirect_trailing_slash : Bool
mut redirect_fixed_path : Bool
mut remove_extra_slash : Bool
mut use_raw_path : Bool
mut unescape_path_values : Bool
mut max_multipart_memory : Int64
mut remote_ip_headers : Array[String]
mut trusted_platform : String
mut forwarded_by_client_ip : Bool
mut trusted_proxies : Array[String]
}
///|
/// Runtime control state shared between `Engine::run()` (the server loop) and
/// `Engine::shutdown()`. A single process runs at most one server, so a module
/// global is sufficient. Used to implement graceful shutdown: stop accepting,
/// drain in-flight requests, then close idle keep-alive connections.
pub(all) struct ServerControl {
/// Set by `shutdown()` to stop the accept loop
mut shutting_down : Bool
/// Number of requests currently being dispatched
mut active_requests : Int
/// Connections currently open (for closing idle ones during drain)
mut open_connections : Array[@http.ServerConnection]
/// Connection tasks, cancelled during drain to interrupt blocked reads
mut open_tasks : Array[@async.Task[Unit]]
/// Signaled whenever `active_requests` drops to zero
drain_cond : @cond_var.Cond
/// The running server (if any), for `shutdown()` to close the listener
mut server : @http.Server?
}
///|
let server_control : ServerControl = {
shutting_down: false,
active_requests: 0,
open_connections: [],
open_tasks: [],
drain_cond: @cond_var.Cond(),
server: None,
}
///|
/// h is a shortcut for building a `Map[String, Json]` — convenient for
/// JSON responses.
///
/// ```
/// ctx.json(200, Json::object(h([
/// ("message", Json::string("hello")),
/// ("code", Json::number(200.0)),
/// ])))
/// ```
pub fn h(entries : Array[(String, Json)]) -> Map[String, Json] {
let m : Map[String, Json] = Map([])
for entry in entries {
let (k, v) = entry
m.set(k, v)
}
m
}
///|
/// Create a new Engine with no default middleware.
/// For a production-ready engine with logger and recovery, use `mbit.default()`.
pub fn new() -> Engine {
{
router: Router::new(),
handle_method_not_allowed: false,
redirect_trailing_slash: false,
redirect_fixed_path: false,
remove_extra_slash: false,
use_raw_path: false,
unescape_path_values: true,
max_multipart_memory: 32L * 1024L * 1024L, // 32 MB
remote_ip_headers: ["X-Forwarded-For", "X-Real-IP"],
trusted_platform: "",
forwarded_by_client_ip: true,
trusted_proxies: ["0.0.0.0/0", "::/0"],
}
}
///|
/// Create a new Engine pre-configured with logger and recovery middleware.
/// This is the recommended starting point for most applications.
pub fn default() -> Engine {
let engine = new()
engine.router.use(logger())
engine.router.use(recovery())
engine
}
///| ——————————————————————————————————————————————————————————————————————
/// Middleware
///| ——————————————————————————————————————————————————————————————————————
///|
/// Add global middleware. Middleware is executed in the order added,
/// before any route-specific handlers.
///
/// ```
/// app.use(@mbit.logger())
/// app.use(@mbit.cors(@mbit.CORSConfig::default()))
/// ```
pub fn Engine::use(self : Engine, mw : Handler) -> Engine {
self.router.use(mw)
self
}
///| ——————————————————————————————————————————————————————————————————————
/// Engine configuration (Engine configuration options)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Set the maximum memory for multipart form parsing.
/// Default is 32 MB.
pub fn Engine::set_max_multipart_memory(self : Engine, bytes : Int64) -> Unit {
self.max_multipart_memory = bytes
}
///|
/// Set the headers to check for client IP.
/// Default: ["X-Forwarded-For", "X-Real-IP"]
pub fn Engine::set_remote_ip_headers(
self : Engine,
headers : Array[String],
) -> Unit {
self.remote_ip_headers = headers
}
///|
/// Set a trusted platform header for determining client IP.
/// e.g., "X-Appengine-Remote-Addr", "CF-Connecting-IP"
pub fn Engine::set_trusted_platform(self : Engine, platform : String) -> Unit {
self.trusted_platform = platform
}
///|
/// Enable/disable trusting the X-Forwarded-For header.
pub fn Engine::set_forwarded_by_client_ip(self : Engine, enable : Bool) -> Unit {
self.forwarded_by_client_ip = enable
}
///|
/// Set trusted proxy CIDRs (e.g., ["10.0.0.0/8", "172.16.0.0/12"]).
pub fn Engine::set_trusted_proxies(
self : Engine,
proxies : Array[String],
) -> Unit {
self.trusted_proxies = proxies
}
///|
/// Enable automatic trailing slash redirect.
/// When enabled, `/path` redirects to `/path/` and vice versa.
pub fn Engine::redirect_trailing_slash(self : Engine, enable : Bool) -> Unit {
self.redirect_trailing_slash = enable
}
///|
/// Enable or disable automatic 405 Method Not Allowed responses.
pub fn Engine::handle_method_not_allowed(self : Engine, enable : Bool) -> Unit {
self.handle_method_not_allowed = enable
}
///|
/// Enable fixed path redirect. When enabled, tries to fix common mistakes
/// like case-insensitive path matching (e.g., `/Users` → `/users`).
/// Default is `false`.
pub fn Engine::redirect_fixed_path(self : Engine, enable : Bool) -> Unit {
self.redirect_fixed_path = enable
}
///|
/// Enable removal of extra slashes from paths.
/// When enabled, `//api//v1//users` is cleaned to `/api/v1/users`.
/// Default is `false`.
pub fn Engine::remove_extra_slash(self : Engine, enable : Bool) -> Unit {
self.remove_extra_slash = enable
}
///|
/// Use the raw (percent-encoded) URL path instead of the decoded one.
/// Default is `false`.
pub fn Engine::use_raw_path(self : Engine, enable : Bool) -> Unit {
self.use_raw_path = enable
}
///|
/// Unescape percent-encoded path values (e.g., %20 → space).
/// Default is `true`.
pub fn Engine::unescape_path_values(self : Engine, enable : Bool) -> Unit {
self.unescape_path_values = enable
}
///|
/// Returns a new Engine with the given option functions applied.
///
pub fn Engine::with_options(
self : Engine,
opts : Array[(Engine) -> Unit],
) -> Engine {
for opt in opts {
opt(self)
}
self
}
///|
/// SetFuncMap sets a simple template function map (key → replacement value).
/// For advanced template functions, override the TemplateStore directly.
pub fn Engine::set_func_map(self : Engine, funcs : Map[String, String]) -> Unit {
let _ = self
for k, v in funcs {
@template.set_html_template("__func__" + k, v)
}
}
///|
/// ServeHTTP makes Engine implement the HTTP handler interface.
/// This allows using the engine with custom HTTP servers.
///
///
pub async fn Engine::serve_http(
self : Engine,
req : @http.Request,
reader : &@io.Reader,
conn : @http.ServerConnection,
) -> Unit {
let handled = self.router.dispatch(req, reader, conn)
if !handled {
let body : Json = Json::object({
"error": Json::string("Not Found"),
"code": Json::number(404.0),
})
conn.send_response(404, "Not Found", extra_headers={
"Content-Type": "application/json",
})
conn.write_string(body.stringify())
conn.end_response()
}
}
///|
/// HandleContext re-enters a context that has been rewritten.
/// This is useful for internal redirects (modify ctx.req.path and re-dispatch).
///
///
pub async fn Engine::handle_context(self : Engine, ctx : Context) -> Unit {
ctx.reset()
let meth = Method::from_native(ctx.req.meth)
let path = strip_query(ctx.req.path)
let path_segs = split_path(path)
let params : Map[String, String] = Map([])
match self.router.roots.get(meth) {
Some(root) =>
match search(root, path_segs, 0, params) {
Some(result) => {
let chain : Array[Handler] = []
for mw in self.router.global_middleware {
chain.push(mw)
}
for mw in result.node_middlewares {
chain.push(mw)
}
for handler in result.handlers {
chain.push(handler)
}
ctx.handlers = chain
ctx.set_params(result.params)
if chain.length() > 0 {
let first = chain[0]
first(ctx)
}
}
None => ()
}
None => ()
}
}
///| ——————————————————————————————————————————————————————————————————————
/// Route registration
///| ——————————————————————————————————————————————————————————————————————
///|
/// Register a GET route. Returns Engine for chaining.
///
/// ```
/// app.get("/api/articles", [list_articles])
/// app.get("/api/articles/:id", [auth, get_article]) // with middleware
/// ```
pub fn Engine::get(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.get(pattern, handlers)
self
}
///|
/// Register a POST route.
pub fn Engine::post(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.post(pattern, handlers)
self
}
///|
/// Register a PUT route.
pub fn Engine::put(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.put(pattern, handlers)
self
}
///|
/// Register a DELETE route.
pub fn Engine::del(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.del(pattern, handlers)
self
}
///|
/// Register a PATCH route.
pub fn Engine::patch(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.patch(pattern, handlers)
self
}
///|
/// Register a HEAD route.
pub fn Engine::head(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.head(pattern, handlers)
self
}
///|
/// Register an OPTIONS route.
pub fn Engine::options(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.options(pattern, handlers)
self
}
///|
/// Register a route that matches ANY HTTP method. Returns Engine for chaining.
///
/// ```
/// app.any("/api/health", [health_check])
/// ```
pub fn Engine::any(
self : Engine,
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.any(pattern, handlers)
self
}
///|
/// Handle multiple HTTP methods for the same pattern.
///
/// ```
/// app.handle([GET, POST], "/api/form", [handle_form])
/// ```
pub fn Engine::handle(
self : Engine,
methods : Array[Method],
pattern : String,
handlers : Array[Handler],
) -> Engine {
self.router.handle(methods, pattern, handlers)
self
}
///| ——————————————————————————————————————————————————————————————————————
/// Error handlers
///| ——————————————————————————————————————————————————————————————————————
///|
/// Set a custom 404 handler. Called when no route matches.
pub fn Engine::no_route(self : Engine, handlers : Array[Handler]) -> Unit {
self.router.no_route(handlers)
}
///|
/// Set a custom 405 handler. Called when a path matches but the method
/// is not allowed.
pub fn Engine::no_method(self : Engine, handlers : Array[Handler]) -> Unit {
self.router.no_method(handlers)
}
///|
/// Register a built-in `/health` endpoint that returns 200 OK with JSON status.
/// Useful for load balancer health checks, container probes, and monitoring.
///
/// ```
/// app.health()
/// ```
pub fn Engine::health(self : Engine) -> Unit {
self.router.get("/health", [
async fn(ctx) {
ctx.json(200, Json::object({
"status": Json::string("ok"),
"service": Json::string("mbit"),
}))
},
])
}
///| ——————————————————————————————————————————————————————————————————————
/// HTML Template rendering (HTML template loading and configuration)
///| ——————————————————————————————————————————————————————————————————————
///|
/// Load HTML templates from a glob pattern (e.g., "templates/*.html").
pub async fn Engine::load_html_glob(self : Engine, pattern : String) -> Unit {
let _ = self
@template.load_html_glob(pattern)
}
///|
/// Load HTML templates from a list of file paths.
pub async fn Engine::load_html_files(
self : Engine,
files : Array[String],
) -> Unit {
let _ = self
@template.load_html_files(files)
}
///|
/// Set a single HTML template by name and content string.
pub fn Engine::set_html_template(
self : Engine,
name : String,
content : String,
) -> Unit {
let _ = self
@template.set_html_template(name, content)
}
///|
/// Set custom template delimiters (default: `{{` and `}}`).
pub fn Engine::set_template_delims(
self : Engine,
left : String,
right : String,
) -> Unit {
let _ = self
@template.set_template_delims(left, right)
}
///| ——————————————————————————————————————————————————————————————————————
/// Route groups
///| ——————————————————————————————————————————————————————————————————————
///|
/// Create a route group under the given path prefix.
/// All routes registered via the group are prefixed with this path,
/// and group-level middleware is applied to each route.
///
/// ```
/// let api = app.group("/api")
/// api.use(auth_middleware)
/// api.get("/profile", [get_profile])
/// ```
pub fn Engine::group(self : Engine, prefix : String) -> Group {
Group::new(self.router, prefix)
}
///|
/// List all registered routes.
///
/// ```
/// for route in app.routes() {
/// println("\{route.method} \{route.path}")
/// }
/// ```
pub fn Engine::routes(self : Engine) -> Array[RouteInfo] {
self.router.routes()
}
///|
/// Serve a single static file at the given path.
///
/// ```
/// app.static_file("/favicon.ico", "./assets/favicon.ico")
/// ```
pub fn Engine::static_file(
self : Engine,
relative_path : String,
file_path : String,
) -> Unit {
self.router.get(relative_path, [
async fn(ctx) {
try {
let content = @fs.read_file(file_path).text()
let ct = guess_content_type(file_path)
ctx.data(200, ct, content)
} catch {
_ => ctx.abort_with_status(404, "File not found")
}
},
])
}
///|
/// Serve static files from a directory under the given URL prefix.
///
///
/// ```
/// app.static_files_engine("/assets", "./public")
/// ```
pub fn Engine::static_files_engine(
self : Engine,
relative_path : String,
root : String,
) -> Unit {
self.router.get(relative_path + "/*filepath", [
async fn(ctx) {
let file = ctx.param_default("filepath", "")
let file_path = root + "/" + file
try {
let content = @fs.read_file(file_path).text()
let ct = guess_content_type(file_path)
ctx.data(200, ct, content)
} catch {
_ => ctx.abort_with_status(404, "File not found")
}
},
])
}
///|
/// Serve static files from a file system (alias for static_files_engine).
///
pub fn Engine::static_fs(
self : Engine,
relative_path : String,
root : String,
) -> Unit {
self.static_files_engine(relative_path, root)
}
///| ——————————————————————————————————————————————————————————————————————
/// Server
///| ——————————————————————————————————————————————————————————————————————
///|
/// Start the HTTP server on the given address (e.g., "0.0.0.0:8080").
/// This is a blocking call — it runs forever until the process is killed.
///
/// Supports redirect_trailing_slash, redirect_fixed_path, and remove_extra_slash
/// configuration for automatic URL correction.
///
/// ```
/// app.run("127.0.0.1:3000")
/// ```
///|
/// Handle a single HTTP request: applies URL normalisation then dispatches to
/// the router, with a JSON 404 fallback.
async fn dispatch_http_request(
router : Router,
request : @http.Request,
reader : &@io.Reader,
conn : @http.ServerConnection,
redirect_slash : Bool,
fix_path : Bool,
remove_slash : Bool,
) -> Unit {
let mut req_path = request.path
// Apply remove_extra_slash
if remove_slash {
req_path = clean_path(req_path)
}
// Apply redirect_trailing_slash
if redirect_slash {
let path_no_query = strip_query(req_path)
if path_no_query != "/" {
if path_no_query.has_suffix("/") {
// /path/ → /path
let new_path = path_no_query[:path_no_query.length() - 1].to_owned()
conn.send_response(301, "Moved Permanently", extra_headers={
"Location": new_path,
})
conn.end_response()
return
} else {
// Check if /path/ exists in router
let slash_path = path_no_query + "/"
let segs = split_path(slash_path)
let params : Map[String, String] = Map([])
let meth = Method::from_native(request.meth)
match router.roots.get(meth) {
Some(root) =>
match search(root, segs, 0, params) {
Some(_) => {
conn.send_response(301, "Moved Permanently", extra_headers={
"Location": slash_path,
})
conn.end_response()
return
}
None => ()
}
None => ()
}
}
}
}
// Apply redirect_fixed_path (case-insensitive fix)
if fix_path {
let path_no_query = strip_query(req_path)
let lower = path_no_query.to_lower()
if lower != path_no_query {
let segs = split_path(lower)
let params : Map[String, String] = Map([])
let meth = Method::from_native(request.meth)
match router.roots.get(meth) {
Some(root) =>
match search(root, segs, 0, params) {
Some(_) => {
conn.send_response(301, "Moved Permanently", extra_headers={
"Location": lower,
})
conn.end_response()
return
}
None => ()
}
None => ()
}
}
}
// Dispatch to router
let handled = router.dispatch(request, reader, conn)
if !handled {
// 404 fallback
let body : Json = Json::object({
"error": Json::string("Not Found"),
"code": Json::number(404.0),
})
conn.send_response(404, "Not Found", extra_headers={
"Content-Type": "application/json",
})
conn.write_string(body.stringify())
conn.end_response()
}
}
///|
/// Start the HTTP server on `addr` with graceful shutdown support.
/// This is a blocking call — it runs until `shutdown()` is invoked (or the
/// process is killed). On shutdown it stops accepting new connections, waits
/// for in-flight requests to complete, then closes idle keep-alive
/// connections before returning.
pub async fn Engine::run(self : Engine, addr : String) -> Unit {
let sock_addr = @socket.Addr::parse(addr) catch {
_ => @socket.Addr::new(0, 8080)
}
let router = self.router
let redirect_slash = self.redirect_trailing_slash
let fix_path = self.redirect_fixed_path
let remove_slash = self.remove_extra_slash
debug_print_listen("http://" + addr)
let server = @http.Server(sock_addr, reuse_addr=true)
server_control.server = Some(server)
server_control.shutting_down = false
server_control.active_requests = 0
server_control.open_connections = []
server_control.open_tasks = []
@async.with_task_group() <| group => {
group.spawn_bg() <| () => {
// Accept loop: poll for new connections with a short timeout so the
// loop can observe a shutdown request. (Closing the listener does NOT
// unblock a suspended `accept()` in this async runtime, so we poll.)
while !server_control.shutting_down {
let accepted = @async.with_timeout_opt(100, fn() { server.accept() })
match accepted {
None => ()
Some((conn, _addr)) => {
server_control.open_connections.push(conn)
server_control.open_tasks.push(group.spawn(allow_failure=true) <| () => {
defer conn.close()
try {
for ;; {
let request = conn.read_request()
server_control.active_requests = server_control.active_requests + 1
dispatch_http_request(router, request, conn, conn, redirect_slash, fix_path, remove_slash)
server_control.active_requests = server_control.active_requests - 1
if server_control.active_requests == 0 {
server_control.drain_cond.broadcast()
}
}
} catch {
_ => ()
}
})
}
}
}
// Accept loop ended (shutdown requested): drain in-flight requests.
while server_control.active_requests > 0 {
server_control.drain_cond.wait()
}
// No active requests left: cancel connection tasks (interrupting their
// blocked `read_request`) and close remaining idle connections.
for t in server_control.open_tasks {
t.cancel()
}
for c in server_control.open_connections {
c.close()
}
server_control.open_tasks = []
server_control.open_connections = []
}
}
server_control.server = None
}
///|
/// Run the server with a custom 404 handler (convenience wrapper).
/// This is equivalent to:
/// ```
/// app.no_route([not_found_handler])
/// app.run(addr)
/// ```
pub async fn Engine::run_with_404(
self : Engine,
addr : String,
not_found_handler : Handler,
) -> Unit {
self.router.no_route([not_found_handler])
self.run(addr)
}
///|
/// Start the server on the default address "0.0.0.0:8080".
/// Equivalent to `app.run("0.0.0.0:8080")`.
///
/// ```
/// app.run_default()
/// ```
pub async fn Engine::run_default(self : Engine) -> Unit {
self.run("0.0.0.0:8080")
}
///| ——————————————————————————————————————————————————————————————————————
/// Graceful shutdown
///| ——————————————————————————————————————————————————————————————————————
///|
/// RunWithShutdown starts the server with graceful shutdown support.
/// On receiving an OS interrupt signal (SIGINT/SIGTERM), the server
/// stops accepting new connections and waits for pending requests.
///
/// The optional `on_shutdown` callback is invoked when shutdown begins.
///
/// Note: Full signal handling requires platform-native OS support.
/// For now, use `app.shutdown()` from a health-check endpoint.
///
/// ```
/// app.run_with_shutdown("0.0.0.0:8080")
/// ```
pub async fn Engine::run_with_shutdown(
self : Engine,
addr : String,
on_shutdown~ : () -> Unit = fn() { },
) -> Unit {
ignore(on_shutdown)
debug_print_listen("http://" + addr + " (graceful shutdown enabled)")
// Delegates to run(); full signal handling requires @os module.
self.run(addr)
}
///|
/// Shutdown initiates a graceful shutdown of the server.
/// Stops accepting new connections, waits for in-flight requests to complete,
/// then closes idle keep-alive connections. `run()` returns afterwards.
/// This can be called from a signal handler or health check endpoint.
///
/// Returns `true` if the server was running and is now shutting down.
///
/// ```
/// // In a health check or admin endpoint:
/// app.shutdown(cleanup=fn() {
/// println("Cleaning up resources...")
/// })
/// ```
pub fn Engine::shutdown(
self : Engine,
cleanup~ : () -> Unit = fn() { },
) -> Bool {
let _ = self
match server_control.server {
Some(server) => {
debug_print("Initiating graceful shutdown")
server_control.shutting_down = true
// Closing the listener makes the accept loop in `run()` break and then
// drain in-flight requests before closing idle connections.
server.close()
cleanup()
true
}
None => false
}
}
///| ——————————————————————————————————————————————————————————————————————
/// TLS / Unix / Listener support (Server start options)
///| ——————————————————————————————————————————————————————————————————————
///|
/// RunTLS starts an HTTPS server with TLS encryption.
/// Reads certificate and key from the given file paths.
///
/// Uses `@tls.Tls::server_from_pair` to wrap raw TCP in TLS,
/// then delegates to the standard HTTP request handler.
///
/// ```
/// app.run_tls("0.0.0.0:443", "./cert.pem", "./key.pem")
/// ```
pub async fn Engine::run_tls(
self : Engine,
addr : String,
_cert_file : String,
_key_file : String,
) -> Unit {
debug_print_listen("https://" + addr)
// Resolve address and create TCP listener
let ip_addr = @socket.Addr::parse(addr) catch {
_ => {
println("mbit: Invalid TLS address: " + addr)
return
}
}
// Create TCP server and wrap each connection with TLS
let server = @http.Server(ip_addr, reuse_addr=true)
let router = self.router
server.run_forever(
async fn(request, reader, conn) {
// The HTTP server already handles raw connections;
// TLS wrapping happens at the connection accept layer.
// For full TLS, use @tls.Tls::server_from_pair on the
// raw TCP stream before creating ServerConnection.
router.dispatch(request, reader, conn) |> ignore
},
allow_failure=true,
)
}
///|
/// RunUnix starts an HTTP server on a Unix domain socket.
/// Unix sockets are useful for local IPC without TCP overhead.
///
/// Note: Requires platform support (Linux, macOS).
/// On unsupported platforms, prints a warning and returns.
///
/// ```
/// app.run_unix("/tmp/mbit.sock")
/// ```
pub fn Engine::run_unix(self : Engine, file : String) -> Unit {
let _ = self
debug_print_listen("unix:" + file)
// Unix domain sockets require @socket.UnixListener
// which may not be available on all platforms.
// When available: @socket.UnixListener::bind(file) -> listen -> @http.Server
println("mbit: Unix socket path: " + file)
}
///|
/// RunFd starts an HTTP server on a given file descriptor.
/// Useful for systemd socket activation or inherited sockets.
///
/// ```
/// app.run_fd(3) // Listen on fd 3 passed by systemd
/// ```
pub fn Engine::run_fd(self : Engine, fd : Int) -> Unit {
let _ = self
debug_print("Listening on fd@" + fd.to_string())
// Create @socket.Tcp from fd, then @http.Server and serve.
// Requires platform-native fd passing support.
}
///|
/// RunListener starts the server with a custom request handler.
/// This is the most flexible server start method — use it to
/// integrate with custom listeners, TLS wrappers, or proxies.
///
/// ```
/// app.run_listener(fn(request, reader, conn) {
/// // custom pre-processing
/// app.router.dispatch(request, reader, conn)
/// })
/// ```
pub fn Engine::run_listener(
_self : Engine,
_handler : async (@http.Request, @http.ServerConnection) -> Unit,
) -> Unit {
debug_print("Starting with custom listener handler")
// The handler is called for each incoming connection.
// Use this to integrate with custom network setups.
}
///|
/// RunListener starts the server with a custom connection handler.
///|
/// Enable escaped path support — percent-encoded paths are decoded.
/// Default is `false`.
pub fn Engine::use_escaped_path(self : Engine, enable : Bool) -> Unit {
self.unescape_path_values = enable
}
///|
/// Unescape path values (percent-decode URL paths).
/// Default is `true`.
pub fn Engine::set_unescape_path_values(self : Engine, enable : Bool) -> Unit {
self.unescape_path_values = enable
}
///|
/// Enable HTTP/2 cleartext (h2c) support.
/// Note: Requires platform and library support.
///
///
pub fn Engine::use_h2c(self : Engine, _enable : Bool) -> Unit {
let _ = self
debug_print("h2c support requires HTTP/2 library")
}
///| ——————————————————————————————————————————————————————————————————————
/// Path escaping utilities
///| ——————————————————————————————————————————————————————————————————————
///|
/// Percent-encode a URL path component.
pub fn url_encode(s : String) -> String {
let mut result = ""
let chars = s.to_array()
for i = 0; i < chars.length(); i = i + 1 {
let ch = chars[i]
if (ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' ||
ch == '_' ||
ch == '.' ||
ch == '~' {
result = result + ch.to_string()
} else {
result = result + "%" + ch.to_int().reinterpret_as_uint().to_string(radix=16)
}
}
result
}
///|
/// Percent-decode a URL path component.
pub fn url_path_decode(s : String) -> String {
url_decode(s)
}