# httpbin-clack — the same endpoints as a plain Clack application

The five echo endpoints of [`../httpbin`](https://github.com/making/rontolisp/blob/develop/examples/cloudflare-workers/httpbin), written with nothing but
[Clack](https://github.com/fukamachi/clack): an application **function**, a
`cond` over `:path-info` (clack has no router), and one **middleware** — which
in Clack is just a function from application to application:

```lisp
(defun wrap-json (app)
  (lambda (env)
    (let ((response (funcall app env)))
      (list* (first response)
             (list* :content-type "application/json" (second response))
             (cddr response)))))

(clack:clackup (wrap-json #'app) :server :reactor :use-thread nil)
```

Nothing in `worker.lisp` mentions Cloudflare, or a Worker, or an export.

```bash
./build.sh          # worker.lisp -> src/worker.wasm
npx wrangler dev    # http://localhost:8787
npx wrangler deploy
```

```console
$ curl 'http://localhost:8787/get?a=1&b=two'
{"args":{"a":"1","b":"two"},"headers":{"host":"localhost:8787",...},"method":"GET","path":"/get"}

$ curl -X POST -d '{"name":"rontolisp"}' http://localhost:8787/post
{"args":{},...,"data":"{\"name\":\"rontolisp\"}","json":{"name":"rontolisp"}}
```

A wrong method answers 405 with the one it wanted, an unknown path 404, and a
body that does not parse leaves `"json": null`.

## What's in here

| File | Purpose |
| --- | --- |
| [`worker.lisp`](worker.lisp) | **The whole program**: quickload, the endpoints, the middleware, `clackup` |
| [`check.lisp`](check.lisp) | The same handler driven without Cloudflare, on every backend |
| [`build.sh`](build.sh) | `--no-wasi --optimize=size` over `worker.lisp` |
| [`src/index.js`](src/index.js) | Three lines over the generated `worker()`, plus the one hook it leaves to a caller — which header carries the client address |
| [`src/worker.js`](src/worker.js) | The boundary, GENERATED by `--emit-js-glue` from `worker.lisp`'s declarations. Do not edit; `./build.sh` rewrites it |
| `src/worker.wasm` | A build product — run `./build.sh` first |

## What this answers that `../httpbin` does not

| | `../httpbin` | this |
| --- | --- | --- |
| How it reaches the Worker | thirty hand-written lines and an explicit `wasm-export` | `clackup`, with the export synthesized |
| Reads as | a Worker program that happens to speak Clack | **every other Clack program** |
| clack in the module | none | what the tree-shaker keeps of clack and lack |

The trade is honest: you pay for clack and lack to be in the module
([size report](https://github.com/making/rontolisp/blob/develop/examples/cloudflare-workers/size-report/results/cloudflare-workers.md)) so that the
file reads like an ordinary Clack program. Per request you pay nothing — when
this directory switched from a hand-written export to `clackup`, the module grew
9% and `_initialize` roughly doubled, while warm `GET` and `POST` did not move
at all. Everything clack costs on a reactor is module size and startup, and
startup is paid once per isolate.

## The backend is a handler backend, not example code

Nothing here writes an adapter. `clack-handler-reactor` is a built-in Clack
handler backend, and `:server :reactor` means **host-driven on every backend**:
its `run` stores the application where a socket backend would bind a listener.
That is what lets [`check.lisp`](check.lisp) drive this Worker — through the same
`dispatch` the export calls — on the interpreter and the JVM as well.

[`../httpbin-clack-one-source`](https://github.com/making/rontolisp/blob/develop/examples/cloudflare-workers/httpbin-clack-one-source) is the other
designator: `:server :rontolisp` serves on whatever the compile *target*'s
native transport is, which is what lets one file be a socket server locally and
a Worker here without an edit. Both store into the same reactor machinery, so
the two cannot drift.

### Where the export comes from

`clackup` applies the backend's `run`, and a reactor owns no socket, so `run`
stores the application and returns. What replaces the socket is a WASM export —
and `rontolisp:wasm-export` needs a **literal** name at compile time, which a
program whose whole Worker half is a `clackup` call cannot give. So `run`
carries a marker and the compiler answers it, appending the equivalent of

```lisp
(defun %reactor-dispatch (json) (rontolisp::%http-reactor-dispatch json))
(rontolisp:wasm-export '%reactor-dispatch :as "handle-request"
                       :params '(:string) :returns :string)
```

after the program. `%http-reactor-dispatch` is the **shared** reactor machinery
([`http-reactor.lisp`](https://github.com/making/rontolisp/blob/develop/examples/cloudflare-workers/src/main/resources/am/ik/rontolisp/eval/http-reactor.lisp)),
and `clack.handler.reactor:dispatch` is a thin public name over the same
functions — which is why `check.lisp` exercises what the Worker exercises. It
converts nothing either: every backend meets in
[`http-server.lisp`](https://github.com/making/rontolisp/blob/develop/examples/cloudflare-workers/src/main/resources/am/ik/rontolisp/eval/http-server.lisp)'s
two entry points, and the JSON envelope — with the
[two fields the JavaScript side must get right](../httpbin/README.md#the-envelope-and-two-fields-the-javascript-side-must-get-right)
— is `../httpbin`'s.

`:use-thread nil` is the one upstream keyword that matters beyond sockets: the
WASM backends are single-threaded so `clackup` already defaults to `nil` there,
but the interpreter and the JVM have threads and a script wants the foreground.

### And the JavaScript half is not hand-written either

`--emit-js-glue` (`build.sh`) writes [`src/worker.js`](src/worker.js) from
`worker.lisp`'s own declarations: the import object, the `(ptr, len)` staging,
the `__ronto_alloc` bracket, **both body imports** — fed from the `Request` it is
already holding and the `Response` it is already building — and the
`Request -> envelope -> Response` mapping over them.
[`src/index.js`](src/index.js) is:

```js
import module from "./worker.wasm";
import { worker } from "./worker.js";

export default worker(module, {
  remoteAddr: (request) => request.headers.get("cf-connecting-ip"),
});
```

The `remoteAddr` hook is the one thing left to a caller, and deliberately: which
header carries the client address is the platform's business rather than the
boundary's. It becomes Clack's `:remote-addr`.

That the STREAMING boundary needs no more of a host than
[`../hello-clack`](https://github.com/making/rontolisp/blob/develop/examples/cloudflare-workers/hello-clack)'s envelope one is the point of the pair — what
`--host-boundary=streaming` buys is a binary body crossing exactly and a large
one never doubling linear memory, not a bigger host.
[`../httpbin`](https://github.com/making/rontolisp/blob/develop/examples/cloudflare-workers/httpbin) is the one Worker here that still writes its own: it
declares `handle-request` by hand, and the compile path recognises the
*synthesized* bridge, so no `worker()` is emitted for it.

The generated file is **checked in** and pinned by `HostGlueEmitterTest`, so
regenerate it with `./build.sh` rather than editing it — and all four
`httpbin-*` directories that go through `clackup` carry the same one.

## Developing it without Cloudflare

`dispatch` is an ordinary function, so the whole Worker runs locally on every
backend the compiler has:

```bash
rontolisp check.lisp                                  # interpreter
rontolisp check.lisp -o Check.class && java -cp rontolisp-0.1.0-SNAPSHOT-exec.jar:. Check
rontolisp check.lisp -o check.wasm && wasmtime run -W gc -W exceptions=y check.wasm
```

[`examples.yaml`](../../examples.yaml) pins all three. The report on standard
error for the unparseable-body probe is lack's `backtrace` middleware, which
prints even for an error the application catches; on the Worker it goes to a
discarding sink and the `200` with `"json": null` still comes back.

## Middleware, `lack:builder`, request bodies

`wrap-json` above is Clack's whole composition story at one middleware. Several
of them compose with `lack:builder`, and `clackup`'s own default `backtrace`
middleware is already in this module and active, so "middleware works" is not
hypothetical. Neither is the body/params stack: `(ql:quickload "lack-request")`
loads on a `--no-wasi` build, and a `lack:builder` around this application reads
a query string and a urlencoded POST body the way it does anywhere else.

That took a fix rather than a discovery, and it is the shape every heavy library
has. The chain `lack-request -> http-body -> fast-http -> smart-buffer` ends at
one upstream form — smart-buffer names its temporary directory with a top-level
`(random ...)` over `uiop:default-temporary-directory` — and on a `--no-wasi`
module both halves used to be `unreachable` stubs, firing at *load* time inside
`_initialize` before any export existed. Both are answered now: the module
carries its own `random` generator (seeded from `crypto` by the generated
`src/worker.js`), and
`getenv` reports the empty environment a reactor really has.
`lack-middleware-session` followed the same rule: it reads the clock while it
loads, so `src/worker.js` hands the time over through `__ronto_set_time`. Add
`--host-random` — the session id is `rontolisp:random-bytes`, which a fixed-seed
generator must not stand in for — and a `(:session)` builder serves a real
session cookie and recognises it on the next request.

**One caveat if you write your own `lack:builder` stack here**: a `--no-wasi`
build then prints a standing `WITH-OPEN-FILE is reachable` warning. It is a
false alarm — `builder` returns its composed application through `reduce`, so
the compiler can no longer see that what reaches `clackup` is a function rather
than a pathname, and `clackup`'s "the app is a file to load" branch stops being
provably dead. Applying a middleware directly, as `worker.lisp` does, keeps the
branch provably dead and the build quiet.

## Why `clackup` prints, and why that is fine here

`clackup` writes two lines unconditionally — its startup banner and
`clack.handler:run`'s debug NOTICE — and they are upstream clack's, not ours. On
a `--no-wasi` module there is no stdout to write them to. They used to **trap**
the instance at `_initialize`, which is why this example could not call
`clackup` at all; now standard output and error are a **sink** under
`--no-wasi`, so the bytes are simply discarded. That is a deliberate policy
rather than a patch for clack: a reactor host hands the module no file
descriptors, so discarding loses only the bytes, while the alternative was
killing the instance for a log line. Locally the two lines are visible on real
stdout; pass `:silent t :debug nil` to suppress them.

## Limitations

The Worker sandbox and `--no-wasi` limitations of
[`../httpbin`](../httpbin/README.md#limitations) apply unchanged. One more is
specific to the clack directories: a *runtime* `(ql:quickload ...)` cannot work
either — the form in the source is resolved at **compile** time and inlined into
the module, which is why the first `./build.sh` needs network and later ones do
not.

## Rebuilding

```bash
./mvnw clean package -DskipTests   # from the repository root
examples/cloudflare-workers/httpbin-clack/build.sh
```
