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

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:// or https://). Required.
  • opts — an optional table:
    • method — HTTP method (default "GET"); upper-cased for you.
    • query — query parameters appended to url (encoded), so you never build ?a=1&b=2 by hand. A { [name] = value } map or a { {name, value}, ... } pair-list (repeated/ordered keys); a LIST value repeats the key. See btv.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 a Content-Type: application/json header is added unless the caller set one.
    • form — a table sent as an application/x-www-form-urlencoded body (same shape as query); sets the content-type header unless the caller did. Mutually exclusive with body.
    • 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 in fetch.
    • max_redirects — cap on redirects to follow when redirect == "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.