(rontolisp) docs

Serving HTTP (http-handler)

Hand-rolling HTTP over read-line/write-line (as the TCP Sockets guide demonstrates with http-hello.lisp) is instructive, but for a plain request/response server rontolisp:http-handler does the parsing for you. You write a handler that takes a request property list (:method / :path / :query / :headers / :body) and returns a response property list (:status / :headers / :body) — the same value model as rontolisp:fetch, just incoming instead of outgoing:

(defun handle (request)
  (list :status 200
        :headers (list (cons "content-type" "text/plain"))
        :body (format nil "Hello from rontolisp!~%~a ~a~%"
                      (getf request :method) (getf request :path))))

(rontolisp:http-handler 'handle 8080)

Save it as app.lisp (also shipped as examples/net/http-handler.lisp), then run it on any of the three supported backends below.

On the interpreter

http-handler starts a blocking embedded HTTP server on port 8080 (one virtual thread per request) and serves until the process is stopped with Ctrl-C:

$ rontolisp app.lisp
$ curl http://127.0.0.1:8080/hello
Hello from rontolisp!
GET /hello

Compiled to a JVM class

The same source compiles to a JVM class serving the same way. Unlike other compiled rontolisp programs, the class is not self-contained: it implements the embedded server's handler interface, so the rontolisp executable JAR (rontolisp-0.1.0-SNAPSHOT-exec.jar, the same download as in Build & Install) must be on the classpath when running it:

$ rontolisp app.lisp -o App.class
$ java -cp rontolisp-0.1.0-SNAPSHOT-exec.jar:. App
$ curl http://127.0.0.1:8080/hello
Hello from rontolisp!
GET /hello

Compiled to a WASI HTTP component

It also compiles to a WASI HTTP component that runs under wasmtime serve (wasmtime 46+):

$ rontolisp app.lisp -o app.wasm --component
$ wasmtime serve -W gc=y app.wasm
$ curl http://127.0.0.1:8080/hello
Hello from rontolisp!
GET /hello

There the module exports wasi:http/incoming-handler and the host owns the socket, so the port argument is ignored. Note the command needs none of the component-model-async flags that wasmtime run needs for a regular rontolisp component: the serve component is plain WASI 0.2, so its only non-default host requirement is the WebAssembly GC proposal (-W gc=y).

Other WASI HTTP runtimes

Because the component only asks its host for wasi:http 0.2 plus wasm-GC, wasmtime is not the only runtime that can serve it.

jco (the Bytecode Alliance's JavaScript toolchain, running on Node.js/V8 — where wasm-GC is enabled by default) runs it with no extra configuration:

$ npx @bytecodealliance/jco serve app.wasm --port 8080
$ curl http://127.0.0.1:8080/hello
Hello from rontolisp!
GET /hello

wasmCloud (wash 2.x) runs it once the gc proposal is switched on. For wash dev, point the project's .wash/config.yaml at the prebuilt component and list the proposal (the no-op build.command skips wash's own build step):

build:
  command: "true"
  component_path: app.wasm
dev:
  wasm_proposals:
    - gc
$ wash dev
$ curl http://127.0.0.1:8000/hello
Hello from rontolisp!
GET /hello

wash host exposes the same switch as --wasm-proposal gc (or the WASH_WASM_PROPOSALS=gc environment variable).

Spin (spin up) cannot run the component yet: its embedded wasmtime does not enable the WebAssembly GC proposal that every rontolisp component needs, and it offers no flag to turn it on.

Query strings

:path carries the path only, so route comparisons are exact. When the request has a query string it arrives separately under :query — the raw text after the ?, or nil when there is none. Parse it with the query-string functions of the URL library, rontolisp:query-param and rontolisp:query-params (both url-decode keys and values, and both accept nil):

(defun handle (request)
  (list :status 200
        :body (format nil "Hello, ~a!~%"
                      (or (rontolisp:query-param (getf request :query) "name")
                          "world"))))

(rontolisp:http-handler 'handle 8080)
$ curl 'http://127.0.0.1:8080/greet?name=ronto%20lisp'
Hello, ronto lisp!
$ curl http://127.0.0.1:8080/greet
Hello, world!

Calling other services from a handler

rontolisp:fetch works inside a served handler on all three backends, enabling the classic proxy / aggregator shape:

(defun handle (request)
  (let ((res (rontolisp:await
              (rontolisp:fetch "http://127.0.0.1:9000/upstream"))))
    (list :status (getf res :status)
          :body (getf res :body))))

(rontolisp:http-handler 'handle 8080)

On the WASI component backend the outgoing-request machinery rides along in the same component, so the only change is granting the host outbound HTTP — -S http=y for wasmtime (the component-model-async flags are still not needed):

$ rontolisp proxy.lisp -o proxy.wasm --component
$ wasmtime serve -W gc=y -S http=y proxy.wasm

A complete example is examples/net/dog-fetcher.lisp, a reproduction of wasmCloud's dog-fetcher example: every request fetches a random dog picture URL from the dog.ceo API and answers it as JSON.

Limitations

On the WASI component backend, request and response headers are not marshalled yet: the handler sees :headers nil and :headers in the response is ignored. The interpreter and the JVM backend pass headers through.

Inside a served component handler, random, the time built-ins and print (to the host's stdout) all work — the component bridges them to the wasi:random, wasi:clocks and wasi:cli interfaces every wasi:http host provides. getenv returns nil (the serving host exposes no environment) and file streams are unavailable. See the rontolisp:http-handler reference page for the details.

For the client side of HTTP, use rontolisp:fetch — see the HTTP Requests guide. To work at the raw socket level instead (any TCP protocol, or TLS), see the TCP Sockets guide.