---
title: "httpServer"
description: "Serve HTTP from this engine."
section: "API Reference"
slug: "api-httpserver"
canonical: "https://origozero.ai/docs/api-httpserver"
updated: "2026-10-16T20:06:46.836253683+00:00"
tags: ["api", "reference"]
---

# httpServer

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.

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

The `httpServer` namespace: 21 functions.

## globals/httpServer/address {#globals-httpserver-address}

```lua
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.

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

## globals/httpServer/listen {#globals-httpserver-listen}

```lua
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.

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

## globals/httpServer/route {#globals-httpserver-route}

```lua
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.

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

## globals/httpServer/routes {#globals-httpserver-routes}

```lua
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.

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

## globals/httpServer/status {#globals-httpserver-status}

```lua
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 }`.

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

## globals/httpServer/unlisten {#globals-httpserver-unlisten}

```lua
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.

```lua
httpServer.unlisten()
```

## globals/httpServer/unroute {#globals-httpserver-unroute}

```lua
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.

```lua
httpServer.unroute(h)
```

## modules/httpServer/address {#modules-httpserver-address}

```lua
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".

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

## modules/httpServer/listen {#modules-httpserver-listen}

```lua
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".

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

## modules/httpServer/route {#modules-httpserver-route}

```lua
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.

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

## modules/httpServer/routes {#modules-httpserver-routes}

```lua
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.

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

## modules/httpServer/status {#modules-httpserver-status}

```lua
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.

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

## modules/httpServer/unlisten {#modules-httpserver-unlisten}

```lua
unlisten(): boolean
```

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

```lua
httpServer.unlisten()
```

## modules/httpServer/unroute {#modules-httpserver-unroute}

```lua
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.

```lua
httpServer.unroute(h)
```

## typed/builtin//modules/api/engine/httpServer/httpServer/address {#typed-builtin-modules-api-engine-httpserver-httpserver-address}

```lua
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.

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

## typed/builtin//modules/api/engine/httpServer/httpServer/listen {#typed-builtin-modules-api-engine-httpserver-httpserver-listen}

```lua
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.

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

## typed/builtin//modules/api/engine/httpServer/httpServer/route {#typed-builtin-modules-api-engine-httpserver-httpserver-route}

```lua
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.

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

## typed/builtin//modules/api/engine/httpServer/httpServer/routes {#typed-builtin-modules-api-engine-httpserver-httpserver-routes}

```lua
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.

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

## typed/builtin//modules/api/engine/httpServer/httpServer/status {#typed-builtin-modules-api-engine-httpserver-httpserver-status}

```lua
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 }`.

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

## typed/builtin//modules/api/engine/httpServer/httpServer/unlisten {#typed-builtin-modules-api-engine-httpserver-httpserver-unlisten}

```lua
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.

```lua
httpServer.unlisten()
```

## typed/builtin//modules/api/engine/httpServer/httpServer/unroute {#typed-builtin-modules-api-engine-httpserver-httpserver-unroute}

```lua
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.

```lua
httpServer.unroute(h)
```
