HTTP Requests (fetch)
The rontolisp package provides outgoing HTTP modeled on the JavaScript
fetch API, plus the JSON functions that pair naturally with it. None of
these are part of Common Lisp; reference them with the rontolisp: qualifier
(see Packages). rontolisp:fetch starts a request
and immediately returns a promise; the generic promise operations resolve
or transform it, and rontolisp:json-parse / rontolisp:json-stringify
convert between JSON documents and Lisp values.
| Function | Purpose |
|---|---|
rontolisp:fetch | Start an HTTP request: (rontolisp:fetch url &optional options) |
rontolisp:await | Block until a promise settles and return its value |
rontolisp:then | Derive a new promise that applies a callback to the settled value |
rontolisp:promisep | t if a value is a promise |
rontolisp:json-parse | Parse a JSON string into Lisp values |
rontolisp:json-stringify | Serialize a Lisp value to a JSON string |
Backend support. The interpreter and JVM-compiled classes use the JDK
java.net.http.HttpClient; the request runs on a background thread from the momentfetchreturns. The WASM backend is component-only (--component, a hybrid that importswasi:http@0.2):fetchis a compile error in Preview 1 (core-module) mode, and a fetch component must run with-S http=yon top of the async flags. In the browser playgroundfetchruns the real browserfetch()(subject to CORS) while the program continues. The promise operations (await/then/promisep) and the JSON functions work on every backend and in every WASM mode — onlyfetchitself is restricted.
A first request
fetch returns as soon as the request is in flight. Passing the promise to
rontolisp:await blocks until the response arrives and yields the result
property list (:status <integer> :body <string> :headers <alist>):
Reading the individual fields:
(let ((res (rontolisp:await (rontolisp:fetch "http://example.com/"))))
(print (getf res :status)) ; => 200
(print (getf res :body)) ; => "<html>...</html>"
(print (getf res :headers))) ; => (("content-type" . "text/html") ...)
Request options
The optional second argument is an options property list with :method
(a string, default "GET"), :headers (an alist of (name . value) string
pairs) and :body (a string):
;; GET with request headers (an alist of (name . value) string pairs)
(rontolisp:fetch "http://example.com/api"
(list :headers (list (cons "Accept" "application/json"))))
;; POST with a request body
(rontolisp:fetch "http://example.com/api"
(list :method "POST"
:headers (list (cons "Content-Type" "application/json"))
:body "{\"name\":\"rontolisp\"}"))
The supported methods are GET, HEAD, POST, PUT, DELETE, OPTIONS
and PATCH; see the fetch
reference page for validation timing and error behavior per backend (a failed
request surfaces at await, not at fetch — the interpreter and JVM signal
an error, WASM returns nil).
Promises
Because the request is already running when fetch returns, several requests
overlap — start them all, then await each:
(let ((p1 (rontolisp:fetch "http://example.com/a"))
(p2 (rontolisp:fetch "http://example.com/b"))) ; both requests running
(list (rontolisp:await p1) (rontolisp:await p2)))
rontolisp:then derives a new promise that transforms the settled value, like
JavaScript's Promise.prototype.then — calls chain, and a callback returning
a promise is flattened:
Both operations are generic: await passes a non-promise value through
unchanged, and then accepts one, so a value that may or may not be a promise
is handled uniformly. rontolisp:promisep tells the two apart:
A then callback runs lazily at first await and its result is memoized (a
settled promise can be awaited any number of times); see the
then reference page for the exact
timing.
Working with JSON
rontolisp:json-parse turns a JSON document into Lisp values. By default a
JSON object becomes a property list with keyword keys, so the result reads
with getf; arrays become lists, true/false/null become t/nil:
Passing :hash-table returns hash tables with string keys instead — use it
when keys are arbitrary strings or when an empty object must stay
distinguishable from nil:
rontolisp:json-stringify is the inverse: keyword property lists and hash
tables serialize to objects, other lists to arrays:
Both functions are written in rontolisp itself and compile into the program
on every backend; the full value mappings and the edge cases (integer width,
nil ambiguity, key order) are on the
json-parse and
json-stringify
reference pages.
A complete program
The pieces combine into the typical JSON-API round trip: build the request
body with json-stringify, POST it, await the response and parse the body
with json-parse. Save the following as fetch-post.lisp:
(let* ((payload (rontolisp:json-stringify (list :name "rontolisp" :stars 1)))
(res (rontolisp:await
(rontolisp:fetch "https://httpbin.org/post"
(list :method "POST"
:headers (list (cons "Content-Type" "application/json"))
:body payload))))
(json (rontolisp:json-parse (getf res :body))))
(print (getf res :status))
(write-line (getf json :data)))
200
{"name":"rontolisp","stars":1}
Running it
On the interpreter:
rontolisp fetch-post.lisp
Compiled to a JVM class (the class is named after the output file):
rontolisp fetch-post.lisp -o FetchPost.class
java FetchPost
Compiled to a WASM component (wasmtime 46+; note -S http=y, which grants
outgoing HTTP — without it instantiation fails because the wasi:http
imports are unavailable):
rontolisp fetch-post.lisp -o fetch-post.wasm --component
wasmtime run -W gc=y -W component-model-more-async-builtins=y -S http=y fetch-post.wasm
For raw TCP instead of HTTP — or to implement the server side — see the TCP Sockets guide.