(rontolisp) docs
← Functions

rontolisp:fetch

(rontolisp:fetch url &optional options)

Starts an outgoing HTTP request, modeled on the JavaScript fetch API, and immediately returns a future while the request runs asynchronously. A future is an opaque value (it prints as #<FUTURE> and satisfies rontolisp:futurep); pass it to rontolisp:await to suspend until the response arrives and obtain the result property list (:status <integer> :headers <alist> :body <stream>); the :body stream is drained with rontolisp:read-all.

Because the request is already in flight when fetch returns, several requests can overlap:

(let ((p1 (rontolisp:fetch "https://httpbin.ik.am/status/200"))
      (p2 (rontolisp:fetch "https://httpbin.ik.am/status/201")))  ; both requests running
  (list (rontolisp:await p1) (rontolisp:await p2)))

Options

The optional second argument is an options property list. Recognized keys:

  • :method — the HTTP method as a string (default "GET"). Supported methods are GET, HEAD, POST, PUT, DELETE, OPTIONS and PATCH, matched case-insensitively; any other method is an error.
  • :headers — request headers, an alist of (name . value) string pairs.
  • :body — the request body as a string (omit for no body).

Every request carries User-Agent: rontolisp/<version> (<git-commit>) — the version and abbreviated commit rontolisp:version reports, the commit omitted when the build had no git repository — unless :headers already names that field (matched case-insensitively, so your own spelling and value win). The browser playground and a --host-fetch reactor leave the field to the host, which owns it there.

The options are validated when fetch is called (like the JavaScript fetch, which throws synchronously on invalid arguments).

;; GET with request headers (an alist of (name . value) string pairs)
(rontolisp:fetch "https://httpbin.ik.am/get"
                 '(:headers (("Accept" . "application/json"))))

;; POST with a request body
(rontolisp:fetch "https://httpbin.ik.am/post"
                 '(:method "POST"
                   :headers (("Content-Type" . "application/json"))
                   :body "{\"name\":\"rontolisp\"}"))

Result

fetch itself returns the future. Awaiting it yields the property list (:status <integer> :headers <alist> :body <stream>), where :headers is an alist of (name . value) response-header pairs and :body is an asynchronous stream of the body's octet chunks ((unsigned-byte 8) vectors, the bytes as they arrive) — drain it to one decoded string with rontolisp:read-all, take the chunks one at a time with rontolisp:stream-read, or answer the stream itself as a served response body to relay the reply byte-exact:

(let ((res (rontolisp:await (rontolisp:fetch "https://httpbin.ik.am/get"))))
  (print (getf res :status))    ; => 200
  (print (rontolisp:await (rontolisp:read-all (getf res :body))))
                                ; => "{...}"
  (print (getf res :headers)))  ; => (("content-type" . "application/json") ...)

:body is that stream on every backend; on the JVM (whose client takes the whole reply at once) it holds one chunk.

A JSON response body parses into Lisp values with rontolisp:json-parse, and rontolisp:json-stringify builds a JSON request :body from an s-expression.

Backend support

  • Interpreter and JVM: use the JDK java.net.http.HttpClient; the request runs on a background thread from the moment fetch returns.
  • WASM: component-only, over the async wasi:http@0.3.0 — fetch is ordinary Lisp glue calling the wit-imported wasi:http/client@0.3.0, so the component is uniformly WASI 0.3. The future wraps the in-flight async client.send subtask, so multiple requests genuinely overlap. Compile with --component and run with wasmtime run -S http=y (wasmtime 46+; -S http=y makes the host provide wasi:http). fetch remains a compile error in Preview 1 (core-module) mode, which has no host wasi:http; the generic future operations (await, then, futurep) compile in every mode. fetch also works inside a rontolisp:http-handler serve component (a proxy-style handler): run it with wasmtime serve — the serve host provides wasi:http/client by default, no -S http=y needed.
  • --no-wasi reactor with --host-fetch: the same source compiles on a reactor (which imports no WASI), lowered onto two injected host imports — env.fetch(request-json) -> response-head-json and env.readResponseBody(ptr, cap) -> i32 — over the host's own HTTP client (a Cloudflare Worker's fetch behind JSPI, or any synchronous implementation). The result plist is the same, :body the same asynchronous stream: the head arrives with the call and the body is pulled a chunk at a time as the drain asks for it, so a large or binary reply never becomes a JSON string. The future is settled at creation (the host call blocked the stack until the headers): requests never overlap, and a transport failure before the head signals at the fetch call rather than at await — one during the body signals at the drain, as on every other backend. Without the flag, --no-wasi keeps the compile error.
  • Browser playground: truly asynchronous. The interpreter runs in a Web Worker; fetch hands the request to the page's main thread, which runs the real browser fetch() (subject to CORS) while the program continues, so requests overlap, and await blocks the worker until the response arrives. When cross-origin isolation is unavailable (SharedArrayBuffer disabled) the playground falls back to a synchronous request per fetch — programs behave the same, requests simply do not overlap.

Limitations

  • The method must be one of GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH. An unsupported :method is an error: the interpreter and JVM reject it at fetch time; the WASM backend resolves the method statically and rejects a statically-known unsupported :method at compile time (a method computed at runtime cannot be checked there and is treated as GET, while a runtime-computed :body is sent normally).
  • A failed request (for example a refused connection) surfaces when the future is awaited — the same timing as a JavaScript await rejection: every backend signals an error there (on WASM it is a rontolisp:wit-error condition, catchable with handler-case). A request that cannot even be started (for example a malformed URL, or an unsupported runtime-computed method on the interpreter/JVM) makes fetch itself error or, on WASM, return nil instead of a future — and awaiting nil yields nil.