web — lilu API

←index

Overview

HTTP client utilities for synchronous requests with TLS/mTLS and redirect following. Supports Server-Sent Events streaming, URL encoding, HTML entity escaping, and multipart/URL-encoded form body building and parsing.

Submodules

ModuleDescription
web.serverPre-forking HTTP/1.1 server with keep-alive, chunked transfer encoding, response compression (gzip/deflate), and optional TLS with SNI support.

Functions

NameSignature
url_escapeurl_escape(str) -> encoded
html_unescapehtml_unescape(str) -> decoded
html_escapehtml_escape(str) -> escaped
make_form_datamake_form_data(items) -> content_type, body
requestrequest(uri, options, timeout) -> response, err
sse_clientsse_client(uri, options, callbacks) -> client
parse_argsparse_args(body) -> args
parse_form_dataparse_form_data(boundary, body) -> args

url_escape(str) -> encoded

URL-encode a string for safe use in query parameters

Encodes str using application/x-www-form-urlencoded rules:

html_unescape(str) -> decoded

Decode HTML entities back to their original characters

Handles three entity forms:

Codepoints above 255 are decoded to UTF-8 via std.utf.char.

html_escape(str) -> escaped

Escape angle brackets in a string for safe HTML embedding

Only < and > are replaced (&lt; and &gt;). This is intentionally limited to neutralising tags when embedding user-supplied values inside HTML content or attributes. It is not a general-purpose HTML sanitizer — &, ", and ' are left untouched.

make_form_data(items) -> content_type, body

Build a multipart/form-data body from a list of items

Each item in the list is a table with the following fields:

local web = require("web")
local ct, body = web.make_form_data({
    { name = "username", content = "alice" },
    { name = "avatar",   path = "/tmp/photo.png" },
})
-- use ct as the Content-Type header value, body as the request body
local res = web.request("https://example.com/upload", {
    method  = "POST",
    body    = body,
    headers = { ["content-type"] = ct },
})

request(uri, options, timeout) -> response, err

Perform an HTTP request and return the response

Sends an HTTP request to the given URI. The options table may contain:

Returns a table with body, status, and headers fields on success, or nil and an error message on failure. Default timeout is 1 second. When follow_redirects is enabled and a redirect occurred, the response also contains a final_url field with the URL of the last hop.

local web = require("web")

-- Simple GET
local res, err = web.request("https://api.example.com/data")
if res then print(res.body) end

-- POST with JSON body
local res, err = web.request("https://api.example.com/items", {
    method  = "POST",
    body    = '{"name":"foo"}',
    headers = { ["content-type"] = "application/json" },
})

-- Follow redirects (up to 5 hops)
local res, err = web.request("https://example.com/old-path", {
    follow_redirects = true,
})
if res then print(res.final_url or "no redirect") end

sse_client(uri, options, callbacks) -> client

Create a Server-Sent Events (SSE) streaming client

Returns an SSE client object with the following methods:

The options table may contain:

The callbacks table may contain handler functions keyed by event name:

Named event callbacks (e.g., callbacks.delta) are dispatched by SSE event type. JSON data payloads are automatically decoded.

local web = require("web")
local client = web.sse_client(
    "https://api.openai.com/v1/chat/completions",
    {
        method  = "POST",
        body    = '{"model":"gpt-4o","stream":true,"messages":[...]}',
        headers = {
            ["Authorization"] = "Bearer " .. api_key,
            ["content-type"]  = "application/json",
        },
    },
    {
        -- dispatched for SSE events named "delta"
        delta = function(data)
            io.write((data.choices[1] or {}).delta and
                     data.choices[1].delta.content or "")
        end,
        done  = function() print("\n[stream complete]") end,
        error = function(msg, status)
            print("SSE error " .. tostring(status) .. ": " .. msg)
        end,
    }
)
client:connect()
while client:is_connected() do
    client:update()
end

parse_args(body) -> args

Parse a URL-encoded form body into a key-value table

Parses an application/x-www-form-urlencoded body. + signs in values are restored to spaces before URL-decoding; values are then HTML-escaped via html_escape. Returns an empty table for non-string or empty input. Key-value pairs missing the = separator are silently skipped.

parse_form_data(boundary, body) -> args

Parse a multipart/form-data body into a key-value table

Parses a multipart/form-data body using the given boundary string. Text fields are URL-decoded and HTML-escaped. File uploads are returned as tables with filename, content, and content_type fields.