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

HTTP client for making requests and a decorator-based HTTP server built on axum + tokio.

Client Functions

All client functions return a response object with the following fields:

FieldTypeDescription
statusintHTTP status code
bodystringRaw response body
jsonanyParsed JSON body (if applicable)
headersobjectResponse headers
urlstringFinal URL (after redirects)
timeintResponse time in milliseconds
methodstringHTTP method used

http.get(url, options?) -> object

Sends an HTTP GET request.

let resp = http.get("https://api.example.com/users")
say resp.status  // 200
say resp.json    // [{id: 1, name: "Alice"}, ...]

http.post(url, options?) -> object

Sends an HTTP POST request.

let resp = http.post("https://api.example.com/users", {
    body: { name: "Alice", email: "alice@example.com" }
})
say resp.status  // 201

http.put(url, options?) -> object

Sends an HTTP PUT request.

let resp = http.put("https://api.example.com/users/1", {
    body: { name: "Alice Updated" }
})

http.delete(url, options?) -> object

Sends an HTTP DELETE request.

let resp = http.delete("https://api.example.com/users/1")
say resp.status  // 204

http.patch(url, options?) -> object

Sends an HTTP PATCH request.

let resp = http.patch("https://api.example.com/users/1", {
    body: { active: false }
})

http.head(url, options?) -> object

Sends an HTTP HEAD request (headers only, no body).

let resp = http.head("https://example.com")
say resp.status

Options Object

FieldTypeDescription
bodyanyRequest body (auto-serialized as JSON)
headersobjectCustom request headers
authstringBearer token (sets Authorization: Bearer <token>)
timeoutint or floatTime limit for the whole request, in seconds (default: 30; 0.5 is half a second)
let resp = http.get("https://api.example.com/me", {
    auth: "my-secret-token",
    headers: { "Accept": "application/json" },
    timeout: 10
})

Errors

A response with an error status (404, 500, …) is not an error: it is returned with ok == false. A request that gets no response raises a catchable error whose code says why:

Codee.typeMessage (after http.get error: )
E0036TimeoutErrortimed out after 10 seconds waiting for host:port
E0037ConnectionErrorconnection refused by host:port, cannot resolve host 'name': ..., TLS handshake failed with host:port: ..., could not connect to host:port: ...

Other failures (an invalid URL, a private address, a rejected redirect, a body over max_bytes) are E0016. fetch, http.download (default timeout 300 seconds) and http.crawl report the same codes. E0036 and E0037 messages name the host and port, not the full URL (a query string can carry credentials).

try {
    http.get("https://api.example.com/slow", { timeout: 2 })
} catch e {
    if e.code == "E0036" {
        println("too slow, try again later")
    } else if e.code == "E0037" {
        println("server unreachable: {e.message}")
    }
}

http.download(url, destination?) -> object

Downloads a file from url and saves it to destination. If no destination is provided, the filename is derived from the URL. Returns an object with path, size, and status.

let result = http.download("https://example.com/data.zip", "data.zip")
say result.size  // bytes downloaded

http.crawl(url) -> object

Fetches a web page and extracts structured data. Returns an object with:

FieldTypeDescription
urlstringThe URL crawled
statusintHTTP status code
titlestringPage title
descriptionstringMeta description
linksarrayArray of absolute URLs found in href attributes
textstringVisible text content (first 500 characters)
html_lengthintTotal HTML length in characters
let page = http.crawl("https://example.com")
say page.title
say len(page.links)

http.response(status, body?, headers?) -> object

Builds the response an HTTP handler returns when it needs a status code other than 200, response headers, or a body that is not JSON (see Server Decorators).

  • status — an Int from 200 to 599.
  • body — null (the default) sends no body; a String is sent as is, with Content-Type: text/plain; charset=utf-8 unless headers sets one; any other value is sent as JSON (application/json unless set).
  • headers — an object of header names to values. A value may be a String, a number or a Bool, or an array of them (the header is sent once per element, e.g. Set-Cookie).

Invalid arguments raise an error when the response is built: a status outside 200-599, a body with status 204 or 304, an invalid header name, a header value containing a line break or other control character (so a value can never inject a header), and Content-Length, Transfer-Encoding or Connection, which the server sets.

@post("/items")
fn create(body) {
    return http.response(201, { id: 7 }, { Location: "/items/7" })
}

@get("/health")
fn health() {
    return http.response(200, "ok")          // text/plain
}

@delete("/items/:id")
fn remove(id) {
    return http.response(204)                // no body
}

The result is an object ({ status, headers, body } tagged as an HttpResponse); a handler may adjust its fields before returning it. It is validated again when it is sent; an invalid one answers 500.

http.pretty(response) -> null

Pretty-prints an HTTP response object to stderr with color formatting.

let resp = http.get("https://api.example.com/status")
http.pretty(resp)

Server Decorators

Forge supports declarative HTTP servers using decorators. The @server decorator configures the server, and @get, @post, @put, @patch, @delete decorators define route handlers.

@server(port: 3000)

@get("/")
fn index() {
    return { message: "Welcome to Forge!" }
}

@get("/users/:id")
fn get_user(id) {
    if id == "0" {
        return http.response(404, { error: "no such user" })
    }
    return { id: id, name: "User " + id }
}

@post("/users")
fn create_user(body, headers) {
    if get(headers, "authorization") == null {
        return http.response(401, { error: "unauthorized" })
    }
    return http.response(201, { ok: true, name: body.name }, { Location: "/users/1" })
}

@delete("/users/:id")
fn delete_user(id) {
    return http.response(204)
}

Handler Parameters

Handler functions receive arguments based on parameter names:

  • Path parameters (:id, :name) are passed by matching the parameter name.
  • body or data receives the request body ({} when the request has none).
  • raw_body receives the body’s exact text, before decoding ("" when the request has none) — for example to check a webhook’s signature.
  • query or qs receives query string parameters as an object.
  • headers receives the request headers as an object with lower-case names (repeated headers joined with , ).
  • req or request receives the whole request: { method, path, params, query, headers, body, raw_body } (body is null and raw_body is "" when there is none).
  • Any other name receives the query parameter of that name, or null.

Path, query and header values are Strings. Use get(headers, "x-name") for an optional header: indexing a missing key is an error.

Request Bodies

The body is decoded by its Content-Type:

Content typebody
application/json, */*+jsonthe parsed JSON value
application/x-www-form-urlencodedan object of Strings
anything else, or nonethe body as a String

Invalid JSON is answered 400 and a body that is not UTF-8 text 415 (whatever its content type, so raw_body is always the exact text), with a JSON {"error": ...} body, before the handler runs. Bodies are limited to 2 MiB (413).

@post("/webhook")
fn webhook(raw_body, headers, body) {
    let expected = crypto.sha256(env.get("WEBHOOK_SECRET", "") + raw_body)
    if get(headers, "x-signature") != expected {
        return http.response(401, { error: "bad signature" })
    }
    return { received: body.event }
}

Responses

A returned value is sent as 200 JSON. http.response(status, body?, headers?) sets the status, headers and body. A runtime error in a handler answers 500 {"error": message}.

Databases

A database the program’s top level opens (db.open, pg.connect) is shared by all handlers. Statements of concurrent requests run one at a time on the connection; a transaction (db.begin() … db.commit(), or BEGIN in SQL) belongs to the request that opened it — other requests wait for it — and is rolled back if the request ends without committing. db.last_insert_rowid() returns the request’s own last insert. db.open or db.close inside a handler affects that request only.

Server Features

  • Built on axum and tokio; each request runs in an isolated fork of the program, concurrently with other requests.
  • CORS is same-origin by default; @server(cors: "permissive") allows any origin.
  • WebSocket support via the @ws decorator.