btv.http
response:text()
res:text() -> the body as a Lua string (already the raw bytes; this is the
identity read, present for fetch-parity and readability). Never rejects.
Defined in http.lua.
response:json()
res:json() -> the body decoded from JSON into a Lua value. Uses btv.json.decode
(the shared codec). Raises on malformed JSON — inside btv.async that surfaces as a
promise rejection, exactly like the browser response.json() rejecting.
Defined in http.lua.
btv.http.encode_uri_component(s)
btv.http.encode_uri_component(s) -> s percent-encoded for use INSIDE a URL component
(one query key/value, a path segment), matching JavaScript’s encodeURIComponent:
every byte outside the RFC 3986 unreserved set (A-Z a-z 0-9 - _ . ~) becomes %XX.
UTF-8 is encoded byte-by-byte, so a space is %20 and é is %C3%A9. Public. The
encoding is the percent-encoding (rust-url) crate’s, not a bespoke implementation
(btv._url_encode_component).
Defined in http.lua.
btv.http.encode_query(params)
btv.http.encode_query(params) -> an encoded query string with NO leading ?, built
from params: either a MAP ({ q = "hi there", page = 2 } -> q=hi+there&page=2) or
an ordered PAIR-LIST ({ { "q", "a" }, { "q", "b" } } -> q=a&q=b, for repeated /
fixed-order keys). A LIST value in a map repeats the key
({ tag = { "x", "y" } } -> tag=x&tag=y). Keys and values are tostring’d. A map
has no guaranteed key order — use a pair-list when order matters. Public.
The Lua side only flattens the map/list shapes into a pair-list; the actual
application/x-www-form-urlencoded serialization is the form_urlencoded
(rust-url) crate’s (btv._url_encode_query), so a space is + and a +/&/= in a
value is escaped correctly — no bespoke encoder.
Defined in http.lua.
btv.http.build_url(url, query)
btv.http.build_url(url, query) -> url with query (a table, see encode_query)
appended as a query string — joined with ?, or & if url already has a ?. A nil
/ empty query returns url unchanged. Public — the exact join opts.query does.
Defined in http.lua.
btv.http.fetch(url, opts)
btv.http.fetch(url[, opts]) -> promise of a Response. The one entry point of the
btv.http client — modeled on the browser’s fetch, fully async, non-blocking.
Args:
url— the absolute request URL (http://orhttps://). Required.opts— an optional table:method— HTTP method (default"GET"); upper-cased for you.query— query parameters appended tourl(encoded), so you never build?a=1&b=2by hand. A{ [name] = value }map or a{ {name, value}, ... }pair-list (repeated/ordered keys); a LIST value repeats the key. Seebtv.http.encode_query.headers— request headers, a{ [name] = value }map or a{ {name, value}, ... }pair-list (for repeated / ordered headers).body— the request body. A string is sent verbatim; any other value is encoded to JSON (btv.json.encode) AND aContent-Type: application/jsonheader is added unless the caller set one.form— a table sent as anapplication/x-www-form-urlencodedbody (same shape asquery); sets the content-type header unless the caller did. Mutually exclusive withbody.timeout— an overall timeout in milliseconds (default: the backend’s).redirect—"follow"(default),"manual"(don’t follow; resolve with the 3xx response), or"error"(reject on a redirect) — as infetch.max_redirects— cap on redirects to follow whenredirect == "follow".
The Response it resolves with is { status, ok, statusText, headers, body } — ok is
true for a 2xx status, headers a { [lowercased-name] = value } map — plus :text()
(the raw body) and :json() (the body decoded from JSON).
RESOLVES with the Response for ANY HTTP status (a 404 / 500 still resolves — check
res.ok / res.status); REJECTS with a { message } table only on a network /
transport failure (DNS, connect, TLS, timeout, a bad URL) — the exact split the browser
fetch makes. Await it inside btv.async, or chain :next / :catch:
btv.async(function()
local res = btv.await(btv.http.fetch("https://api.example.com/search", {
query = { q = "hello world", page = 2 }, -- ?q=hello+world&page=2 (encoded for you)
}))
if res.ok then btv.print(res:json().name) else btv.print("HTTP " .. res.status) end
end)()
Defined in http.lua.
btv.http.fetch_local(url, opts)
btv.http.fetch_local(url[, opts]) — identical to btv.http.fetch, but the request runs
on THIS machine’s network (the native ureq, or the browser’s own fetch()) even when
the session’s btv.http routes to a daemon. The HTTP analogue of the plugin manager’s
local-only btv.fs (btv._local_fs_op): reach for it when a request must originate from
the client, not the daemon (local is a Lua keyword, hence the _local name rather than
an option). In a bare/local session it is exactly btv.http.fetch.
Defined in http.lua.
mount:url()
mount:url() — the mount’s base URL, trailing-slashed
(http://127.0.0.1:53124/<token>/plugin/example/). Hand this to a browser, an
<iframe>, or btv.ui.open. Absorbs the difference between worlds: natively it is the
editor’s bound port plus the session token, on the web build the page’s own origin.
It is the only way to address a mount — build every URL from it.
Defined in httpmount.lua.
mount:path()
mount:path() — the mount’s path on the origin (/<token>/plugin/example natively,
/plugin/example on the web build), no trailing slash.
Defined in httpmount.lua.
mount:origin()
mount:origin() — the origin serving this mount (http://127.0.0.1:53124).
Read through the central btv._http_origin rather than captured per-handle, so a
:set httpport=9000 rebind moves EVERY live mount’s URL at once. A handle that cached
its origin at mount time would keep handing out a dead one.
Defined in httpmount.lua.
mount:close()
mount:close() — retire the route. Idempotent. In-flight requests are dropped (their
clients get a 503). The listener itself stays bound for the session: an idle listener
costs nothing, and a stable origin survives a plugin reload.
Defined in httpmount.lua.
mount:is_open()
mount:is_open() — false once :close() ran.
Defined in httpmount.lua.
btv.http.mount(opts)
btv.http.mount(opts) -> a promise of a Mount.
Publishes opts.on_request at /plugin/<opts.name>/* on the editor’s one origin
(natively under the session-token segment — address it only through mount:url()),
binding the listener if this is the first mount. The promise RESOLVES with the handle
once the route is live, and REJECTS (a { message } table) when the listener cannot
bind ('httpport' is taken) or the name is already mounted — never a silent fallback to
some other port or a suffixed name.
opts = {
name = "example", -- required; [%w_-]+. Mounts at /plugin/example
on_request = function(req, respond) end, -- required
timeout = 30000, -- ms before an unanswered request gets a 504
same_origin = false, -- DANGEROUS when true: serve WITHOUT the sandbox CSP
}
Every response is sandboxed into an opaque origin by default (a content-security-policy
of sandbox allow-scripts … is added beside any CSP the plugin sets). same_origin = true
turns that off so the page can fetch() its own mount and use storage — on the web build
it then runs with the EDITOR’s privileges (its OPFS, the wasm, the daemon link), so only
use it for a page made entirely of the plugin’s own code, never for untrusted HTML.
The handler is called on_request(req, respond):
req = {
method = "GET", -- upper-cased
path = "/style.css", -- MOUNT-RELATIVE; a bare /plugin/example is "/"
raw_path = "/plugin/example/style.css", -- below the session token, in every world
query = { v = "2" }, -- decoded
headers = { ["accept"] = "*/*" }, -- lowercased names
body = "", -- raw bytes, binary-safe
name = "example", -- the mount it routed to
}
req.path is mount-relative so the same handler works under any prefix or origin — the
native port and the browser Service Worker’s origin included.
respond(res) answers it, exactly once:
respond({ status = 200, headers = { ["content-type"] = "text/html" }, body = html })
Every res field is optional ({} is a bare 200). respond may be called LATER — that
is why it is a callback rather than a return value: a handler is free to await a file
read or an upstream fetch first.
on_request = function(req, respond)
btv.fs.read("/tmp/page.html"):next(function(html)
respond({ body = html })
end)
end
Defined in httpmount.lua.
btv.http.origin()
btv.http.origin() -> the origin serving plugin mounts (http://127.0.0.1:53124), or
nil when nothing has mounted yet. Only the origin: natively a mount also needs the
session-token path segment, so address mounts with mount:url(), not this. Nil rather than a guess: until the first
btv.http.mount there is no listener, so there is no origin to report and inventing one
from 'httpport' would describe a port nothing is listening on.
Defined in httpmount.lua.