Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP Server

Forge includes a built-in HTTP server powered by axum and tokio. Servers are defined declaratively using decorators and launched automatically when the interpreter detects a @server directive.

Architecture

Forge Source
    |
    v
Parser extracts decorators (@server, @get, @post, ...)
    |
    v
runtime/server.rs builds axum Router
    |
    v
axum + tokio serve requests
    |
    v
Each request forks the program's state (VM template or interpreter),
decodes the body, calls the handler on the blocking pool,
and builds the response from its return value

The server implementation lives in src/runtime/server.rs, with response values and body decoding in src/runtime/http_response.rs.

Server Configuration

The @server decorator configures the server:

@server(port: 3000, host: "0.0.0.0")
ParameterTypeDefaultDescription
portint8080Listen port
hoststring“127.0.0.1”Bind address

Route Decorators

@get(path?)

Registers a function as a GET handler.

@get("/users")
fn list_users() {
    return [{ name: "Alice" }, { name: "Bob" }]
}

@post(path?)

Registers a function as a POST handler.

@post("/users")
fn create_user(body) {
    say "Creating: " + body.name
    return { ok: true }
}

@put(path?)

Registers a function as a PUT handler.

@patch(path?)

Registers a function as a PATCH handler.

@delete(path?)

Registers a function as a DELETE handler.

@ws(path?)

Registers a function as a WebSocket handler. The function receives each incoming message as a string and returns a response string.

@ws("/chat")
fn handle_message(msg) {
    return "Echo: " + msg
}

Path Parameters

Path parameters use colon syntax (:param). They are automatically mapped to function parameters by name.

@get("/users/:id")
fn get_user(id) {
    return { id: id, name: "User " + id }
}
// GET /users/42 -> { id: "42", name: "User 42" }

The Forge path syntax (:id) is internally converted to axum’s brace syntax ({id}).

Handler Parameters

Handler functions receive arguments based on their parameter names (runtime::server::handler_args, shared by both engines):

Parameter NameSource
Name matching a path paramURL path parameter
body or dataRequest body, decoded by content type ({} when absent)
query or qsQuery string as an object
headersRequest headers, lower-case names
raw_bodyThe body’s exact text ("" when there is none)
req or request{ method, path, params, query, headers, body, raw_body }
Name matching a query paramIndividual query parameter
Othernull
@post("/search")
fn search(body, query, req) {
    say "Search body: " + str(body)
    say "Query params: " + str(query)
    say "Method: " + req.method
    return { results: [] }
}

Request bodies are decoded before the handler runs (runtime::http_response::read_body): a body that is not UTF-8 is refused (415), JSON content types are parsed (400 if invalid), application/x-www-form-urlencoded becomes an object of Strings, and anything else is a String. The undecoded text is kept as raw_body. These errors, like every error the server answers itself, have a JSON {"error": ...} body.

Responses

A handler’s return value is encoded as JSON on the engine that ran it, and the server builds the response from that JSON, so both engines answer byte-identically. A value built by http.response(status, body?, headers?) is an object tagged __type__: "HttpResponse"; the server validates it (ResponseSpec::from_json) and sends its status, headers and body (null: no body; String: as is, text/plain by default; anything else: JSON). Every other value is a 200 JSON body.

JSON Serialization

Return values from handlers are automatically serialized to JSON:

Forge TypeJSON
intnumber
floatnumber
boolboolean
stringstring
nullnull
arrayarray
objectobject
ResultOk(v){"Ok": v}
ResultErr(v){"Err": v}
Other"<TypeName>"

Error Handling

If a handler function throws a runtime error, the server returns HTTP 500 with:

{ "error": "error message here" }

An invalid response value (an out-of-range status, a header value with a line break, …) also answers 500, with invalid http.response: ....

CORS

CORS is same-origin by default. @server(cors: "permissive") allows any origin, method and header.

Concurrency Model

Each request runs in its own fork of the program’s state after the top level ran (a frozen VM template, or the interpreter’s fork_for_serving), on tokio’s blocking pool. Handlers run in parallel; mutations a handler makes to top-level variables are private to its request.

Database connections opened by the top level are shared by the forks (stdlib::db_share): a mutex serializes statements, and a transaction belongs to the request that opened it (other requests wait for it, at most 30 seconds) and is rolled back when that request ends.