Log inGet started

httpServer

Updated October 16, 2026

Serve HTTP from this engine. A world registers a handler for a method and a path, and the engine's own HTTP server routes matching requests to it — so a browser tab, a curl call or another process on this machine reaches the running world over plain HTTP, with the world deciding what every address answers. The engine's own HTTP server listens on the loopback interface, so a world that asks for nothing is reachable from this machine, and through a forward another program opens to it (adb forward, an SSH tunnel, a reverse proxy). httpServer.listen(target) holds a second address of this world's own choosing: the host in the target is the interface bound and the whole of what decides who can reach it, so "0.0.0.0:8080" answers a phone on the same wifi and "127.0.0.1:8080" answers this machine. httpServer.address(path) is the URL to call, and httpServer.status() reports the host, the port and the reach those routes answer under. Content routes are served under the /app mount: httpServer.route("GET", "/status", h) answers http://<host>:<port>/app/status. The engine's own /engine/* routes are matched first, and every registration lands under /app, so the two trees stay disjoint. A route belongs to the chunk that registered it, and so does an address that chunk opened. When that chunk runs again — a module hot-reload, a cleared require cache — both are released and the new run makes its own, so an edited handler is the one that answers. httpServer.routes() names the owner of every address. A handler runs on the script thread, inside the frame, like any other world code — it may read and write the world, and it holds the tick for as long as it runs. The request waits at most timeoutMs (5 s by default) for its answer; past that the caller is told 504 and the connection closes, while the handler itself carries on to completion.

require("@builtin::modules.api.engine.httpServer") -- httpServer (also available as global 'httpServer')

The httpServer namespace: 21 functions.

globals/httpServer/address

httpServer.address(path: string) -> (string?, string?)

The URL a path answers on — scheme, host, port and the /app mount, ready to be fetched or printed for someone to open. Takes the same path spelling route does, and reads the interface and port from the socket routes answer on: the address listen opened while one is open, and the engine's own server otherwise.

Parameters

  • path string — Path under the /app mount, e.g. "/status".

Returns (string?, string?) — The URL, or nil plus the reason there is none — this engine holds no address, or the path is not one a route can be registered at.

print(httpServer.address("/status")) --> http://127.0.0.1:7607/app/status

globals/httpServer/listen

httpServer.listen(target: string) -> (HttpListener?, string?)

Hold an interface and port of this world's own, and answer content routes on it.

The host in target is the interface bound, and the whole of what decides who can reach those routes: "127.0.0.1:8080" answers programs on this machine, "0.0.0.0:8080" answers any host that routes to this machine on that port — a phone on the same wifi, and whatever else the network lets through. Bind loopback unless you want that. A port of 0 asks the operating system for a free one, which the returned record reports, and http:// may be spelled out in front.

This address serves the routes registered under the /app mount. The engine's own /engine/* tree answers on the loopback server it booted with, whose interface stays what the boot bound.

The address belongs to the chunk that opened it and is released when that chunk runs again, so an edited module holds the address its current source names. Asking for the address already held is the same address back.

Parameters

  • target string — Interface and port to hold, e.g. "0.0.0.0:8080".

Returns (HttpListener?, string?) — The listener record, or nil plus the reason the target or the bind was refused.

local l = assert(httpServer.listen("0.0.0.0:8080"))

globals/httpServer/route

httpServer.route(method: string, path: string, handler: HttpHandler, options: HttpRouteOptions?) -> (number?, string?)

Serve one method and path from this engine, answering each matching request with handler.

The path is relative to the /app mount, and a trailing /* segment matches the rest of the path — "/files/*" answers /app/files/a/b, with "a/b" in request.wildcard. An exact path answers ahead of a wildcard, and among wildcards the longest one wins.

One method and path is served by one handler. Registering an address another chunk serves returns nil and a reason naming the handle and the chunk holding it; httpServer.routes() finds that handle and httpServer.unroute frees the address. Registering an address this same chunk already serves takes it back and releases the handler it replaces, so a chunk that runs twice serves the handler it just built.

The handler runs on the script thread; a route registered from a worker job keeps its handler in that worker's VM, which runs it once per request as the job that registered it while the request waits for its value and the engine thread waits for nothing. The route lasts until it is unregistered or the world changes or the scripting state resets, whatever became of the job that made it. Raising inside the handler answers 500 and writes the error to the engine log; returning something that is not a response table or a string answers 500 saying what arrived.

Parameters

  • method string — HTTP verb, e.g. "GET" or "POST".
  • path string — Path under the /app mount, e.g. "/status" or "/files/*".
  • handler HttpHandler — Called with the request table; returns a response table or a body string.
  • options HttpRouteOptions (optional) — { timeoutMs? } — how long a request waits for this handler.

Returns (number?, string?) — The route handle, or nil plus the reason it was not registered.

local h = httpServer.route("GET", "/status", function(req)

globals/httpServer/routes

httpServer.routes() -> { HttpRoute }

Every route this engine currently serves, in registration order — handle, method, registered path, the address it answers on, its full URL, the chunk that registered it, and how long a request for it waits.

Returns { HttpRoute } — An array of route records.

for _, r in ipairs(httpServer.routes()) do print(r.method, r.url, r.owner) end

globals/httpServer/status

httpServer.status() -> HttpServerStatus

Whether this engine serves content routes, on which interface, port and mount, who can reach them, and how many routes and waiting requests it holds. host, port, url and reach are read from the socket routes answer on — the one listen opened while one is open, and the engine's own server otherwise — and listeners carries every address, each with its own reach. When supported is false, reason says why: a browser tab answers HTTP requests and holds no address of its own.

Returns HttpServerStatus — { supported, reason?, host?, port?, url?, reach?, prefix, routeCount, pending, listeners }.

local s = httpServer.status(); print(s.url, s.reach)

globals/httpServer/unlisten

httpServer.unlisten() -> boolean

Release the address listen opened. Returns once the socket is free, so the same port binds again straight after.

Returns boolean — True when an address was held.

httpServer.unlisten()

globals/httpServer/unroute

httpServer.unroute(handle: number) -> boolean

Stop serving a route and release its handler. The address is free for another registration once this returns true.

Parameters

  • handle number — The handle httpServer.route returned.

Returns boolean — True when a route with this handle was registered.

httpServer.unroute(h)

modules/httpServer/address

address(path: string): (string?, string?)

The URL a path answers on — scheme, host, port and the /app mount, ready to be fetched or printed for someone to open. Takes the same path spelling route does, and reads the interface and port from the socket routes answer on: the address listen opened while one is open, and the engine's own server otherwise.

Parameters

  • path string — Path under the /app mount, e.g. "/status".
print(httpServer.address("/status")) --> http://127.0.0.1:7607/app/status

modules/httpServer/listen

listen(target: string): (HttpListener?, string?)

Hold an interface and port of this world's own, and answer content routes on it.

The host in target is the interface bound, and the whole of what decides who can reach those routes: "127.0.0.1:8080" answers programs on this machine, "0.0.0.0:8080" answers any host that routes to this machine on that port — a phone on the same wifi, and whatever else the network lets through. Bind loopback unless you want that. A port of 0 asks the operating system for a free one, which the returned record reports, and http:// may be spelled out in front.

This address serves the routes registered under the /app mount. The engine's own /engine/* tree answers on the loopback server it booted with, whose interface stays what the boot bound.

The address belongs to the chunk that opened it and is released when that chunk runs again, so an edited module holds the address its current source names. Asking for the address already held is the same address back.

Parameters

  • target string — Interface and port to hold, e.g. "0.0.0.0:8080".
local l = assert(httpServer.listen("0.0.0.0:8080"))

modules/httpServer/route

route(

Serve one method and path from this engine, answering each matching request with handler.

The path is relative to the /app mount, and a trailing /* segment matches the rest of the path — "/files/*" answers /app/files/a/b, with "a/b" in request.wildcard. An exact path answers ahead of a wildcard, and among wildcards the longest one wins.

One method and path is served by one handler. Registering an address another chunk serves returns nil and a reason naming the handle and the chunk holding it; httpServer.routes() finds that handle and httpServer.unroute frees the address. Registering an address this same chunk already serves takes it back and releases the handler it replaces, so a chunk that runs twice serves the handler it just built.

The handler runs on the script thread; a route registered from a worker job keeps its handler in that worker's VM, which runs it once per request as the job that registered it while the request waits for its value and the engine thread waits for nothing. The route lasts until it is unregistered or the world changes or the scripting state resets, whatever became of the job that made it. Raising inside the handler answers 500 and writes the error to the engine log; returning something that is not a response table or a string answers 500 saying what arrived.

local h = httpServer.route("GET", "/status", function(req)

modules/httpServer/routes

routes(): { HttpRoute }

Every route this engine currently serves, in registration order — handle, method, registered path, the address it answers on, its full URL, the chunk that registered it, and how long a request for it waits.

for _, r in ipairs(httpServer.routes()) do print(r.method, r.url, r.owner) end

modules/httpServer/status

status(): HttpServerStatus

Whether this engine serves content routes, on which interface, port and mount, who can reach them, and how many routes and waiting requests it holds. host, port, url and reach are read from the socket routes answer on — the one listen opened while one is open, and the engine's own server otherwise — and listeners carries every address, each with its own reach. When supported is false, reason says why: a browser tab answers HTTP requests and holds no address of its own.

local s = httpServer.status(); print(s.url, s.reach)

modules/httpServer/unlisten

unlisten(): boolean

Release the address listen opened. Returns once the socket is free, so the same port binds again straight after.

httpServer.unlisten()

modules/httpServer/unroute

unroute(handle: number): boolean

Stop serving a route and release its handler. The address is free for another registration once this returns true.

Parameters

  • handle number — The handle httpServer.route returned.
httpServer.unroute(h)

typed/builtin//modules/api/engine/httpServer/httpServer/address

httpServer.address(path: string) -> (string?, string?)

The URL a path answers on — scheme, host, port and the /app mount, ready to be fetched or printed for someone to open. Takes the same path spelling route does, and reads the interface and port from the socket routes answer on: the address listen opened while one is open, and the engine's own server otherwise.

Parameters

  • path string — Path under the /app mount, e.g. "/status".

Returns (string?, string?) — The URL, or nil plus the reason there is none — this engine holds no address, or the path is not one a route can be registered at.

print(httpServer.address("/status")) --> http://127.0.0.1:7607/app/status

typed/builtin//modules/api/engine/httpServer/httpServer/listen

httpServer.listen(target: string) -> (HttpListener?, string?)

Hold an interface and port of this world's own, and answer content routes on it.

The host in target is the interface bound, and the whole of what decides who can reach those routes: "127.0.0.1:8080" answers programs on this machine, "0.0.0.0:8080" answers any host that routes to this machine on that port — a phone on the same wifi, and whatever else the network lets through. Bind loopback unless you want that. A port of 0 asks the operating system for a free one, which the returned record reports, and http:// may be spelled out in front.

This address serves the routes registered under the /app mount. The engine's own /engine/* tree answers on the loopback server it booted with, whose interface stays what the boot bound.

The address belongs to the chunk that opened it and is released when that chunk runs again, so an edited module holds the address its current source names. Asking for the address already held is the same address back.

Parameters

  • target string — Interface and port to hold, e.g. "0.0.0.0:8080".

Returns (HttpListener?, string?) — The listener record, or nil plus the reason the target or the bind was refused.

local l = assert(httpServer.listen("0.0.0.0:8080"))

typed/builtin//modules/api/engine/httpServer/httpServer/route

httpServer.route(method: string, path: string, handler: HttpHandler, options: HttpRouteOptions?) -> (number?, string?)

Serve one method and path from this engine, answering each matching request with handler.

The path is relative to the /app mount, and a trailing /* segment matches the rest of the path — "/files/*" answers /app/files/a/b, with "a/b" in request.wildcard. An exact path answers ahead of a wildcard, and among wildcards the longest one wins.

One method and path is served by one handler. Registering an address another chunk serves returns nil and a reason naming the handle and the chunk holding it; httpServer.routes() finds that handle and httpServer.unroute frees the address. Registering an address this same chunk already serves takes it back and releases the handler it replaces, so a chunk that runs twice serves the handler it just built.

The handler runs on the script thread; a route registered from a worker job keeps its handler in that worker's VM, which runs it once per request as the job that registered it while the request waits for its value and the engine thread waits for nothing. The route lasts until it is unregistered or the world changes or the scripting state resets, whatever became of the job that made it. Raising inside the handler answers 500 and writes the error to the engine log; returning something that is not a response table or a string answers 500 saying what arrived.

Parameters

  • method string — HTTP verb, e.g. "GET" or "POST".
  • path string — Path under the /app mount, e.g. "/status" or "/files/*".
  • handler HttpHandler — Called with the request table; returns a response table or a body string.
  • options HttpRouteOptions (optional) — { timeoutMs? } — how long a request waits for this handler.

Returns (number?, string?) — The route handle, or nil plus the reason it was not registered.

local h = httpServer.route("GET", "/status", function(req)

typed/builtin//modules/api/engine/httpServer/httpServer/routes

httpServer.routes() -> { HttpRoute }

Every route this engine currently serves, in registration order — handle, method, registered path, the address it answers on, its full URL, the chunk that registered it, and how long a request for it waits.

Returns { HttpRoute } — An array of route records.

for _, r in ipairs(httpServer.routes()) do print(r.method, r.url, r.owner) end

typed/builtin//modules/api/engine/httpServer/httpServer/status

httpServer.status() -> HttpServerStatus

Whether this engine serves content routes, on which interface, port and mount, who can reach them, and how many routes and waiting requests it holds. host, port, url and reach are read from the socket routes answer on — the one listen opened while one is open, and the engine's own server otherwise — and listeners carries every address, each with its own reach. When supported is false, reason says why: a browser tab answers HTTP requests and holds no address of its own.

Returns HttpServerStatus — { supported, reason?, host?, port?, url?, reach?, prefix, routeCount, pending, listeners }.

local s = httpServer.status(); print(s.url, s.reach)

typed/builtin//modules/api/engine/httpServer/httpServer/unlisten

httpServer.unlisten() -> boolean

Release the address listen opened. Returns once the socket is free, so the same port binds again straight after.

Returns boolean — True when an address was held.

httpServer.unlisten()

typed/builtin//modules/api/engine/httpServer/httpServer/unroute

httpServer.unroute(handle: number) -> boolean

Stop serving a route and release its handler. The address is free for another registration once this returns true.

Parameters

  • handle number — The handle httpServer.route returned.

Returns boolean — True when a route with this handle was registered.

httpServer.unroute(h)
  • api
  • reference