rontolisp:fetch
(rontolisp:fetch url &optional options)
JavaScript の fetch API を模した送信 HTTP リクエストを開始し、リクエストが
非同期に実行されている間に future を即座に返します。future は不透明な値で、
#<FUTURE> と印字され、rontolisp:futurep を満たします。
future を rontolisp:await に渡すと、
レスポンスの到着までサスペンドし、結果のプロパティリスト
(:status <integer> :headers <alist> :body <stream>) が得られます。
:body ストリームは rontolisp:read-all で読み尽くします。
fetch が返った時点でリクエストは既に送信されているため、複数のリクエストを
並行させることができます。
(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)))
オプション
省略可能な第 2 引数はオプションのプロパティリストです。認識されるキーは次のとおりです。
:method— HTTP メソッドを文字列で指定します (デフォルトは"GET")。サポート されるメソッドはGET、HEAD、POST、PUT、DELETE、OPTIONS、PATCHで、 大文字小文字を区別せずに照合されます。それ以外のメソッドはエラーです。:headers— リクエストヘッダ。(name . value)の文字列ペアの連想リストです。:body— リクエストボディを文字列で指定します (ボディがなければ省略します)。
:headers がそのフィールドを指定していない限り、すべてのリクエストは
User-Agent: rontolisp/<version> (<git-commit>) を伴います — バージョンと短縮コミットは
rontolisp:version が報告するものと同じで、ビルド時に git
リポジトリがなければコミットは省略されます (大文字小文字を区別せずに照合するため、
指定した場合はその綴りと値が優先されます)。ブラウザ プレイグラウンドと
--host-fetch リアクタでは、このフィールドはそれを所有するホストに任せます。
オプションは fetch の呼び出し時に検証されます (不正な引数に対して同期的に
例外を投げる JavaScript の fetch と同じです)。
;; 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\"}"))
結果
fetch 自体は future を返します。それを await するとプロパティリスト
(:status <integer> :headers <alist> :body <stream>) が得られます。:headers
はレスポンスヘッダの (name . value) ペアの連想リストで、:body はボディの
オクテットチャンク ((unsigned-byte 8) ベクタ、届いたままのバイト) の
非同期ストリームです —
rontolisp:read-all でデコード済みの 1 つの文字列に
読み切るか、rontolisp:stream-read でチャンクを
1 つずつ取るか、ストリームそのものをサーブ側のレスポンスボディとして返して
応答をバイト単位で正確に中継します:
(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 はどのバックエンドでもこのストリームです。JVM (クライアントが応答全体を
一度に受け取る) では 1 チャンクを持ちます。
JSON のレスポンスボディは
rontolisp:json-parse で Lisp の値にパースでき、
rontolisp:json-stringify で S 式から JSON の
リクエスト :body を組み立てられます。
バックエンドのサポート
- インタプリタ および JVM: JDK の
java.net.http.HttpClientを使用します。fetchが返った瞬間からリクエストはバックグラウンドスレッドで実行されます。 - WASM: コンポーネント専用で、非同期の
wasi:http@0.3.0の上で動作します — fetch は wit-import したwasi:http/client@0.3.0を呼ぶ通常の Lisp グルーであり、 コンポーネントは一様に WASI 0.3 です。future は処理中の非同期client.sendサブタスクをラップしているので、複数のリクエストが実際に並行します。--componentでコンパイルし、wasmtime run -S http=yで実行してください (wasmtime 46+。-S http=yはホストにwasi:httpを提供させる フラグです)。ホストのwasi:httpを持たない Preview 1 (コアモジュール) モードでは fetch はコンパイルエラーのままです。汎用の future 操作 (await、then、futurep) はどのモードでもコンパイルできます。fetch はrontolisp:http-handlerの serve コンポーネント内 (プロキシ型のハンドラ) でも動作します。wasmtime serveで 実行してください — serve ホストはwasi:http/clientをデフォルトで提供するため、-S http=yは不要です。 --no-wasiリアクタ +--host-fetch: 同じソースが (WASI を一切インポート しない) リアクタでもコンパイルできます。注入される 2 つのホストインポートenv.fetch(request-json) -> response-head-jsonとenv.readResponseBody(ptr, cap) -> i32— ホスト自身の HTTP クライアント (JSPI 越しの Cloudflare Worker のfetch、あるいは任意の同期実装) — に 下ろされます。結果の plist は同一で、:bodyも同じ非同期ストリームです: ヘッドは呼び出しと同時に届き、ボディは読み切りが要求するたびに 1 チャンク ずつ引き込まれるので、大きなレスポンスもバイナリのレスポンスも JSON 文字列 になりません。future は生成時点で確定済み (ホスト呼び出しがヘッダまで スタックをブロックした) なので、リクエストは重ならず、ヘッドより前の トランスポート失敗はawaitではなくfetch呼び出しでシグナルされます — ボディ途中の失敗は他のバックエンドと同様、読み切りでシグナルされます。 フラグなしの--no-wasiはこれまで通りコンパイルエラーです。- ブラウザ プレイグラウンド: 真に非同期です。インタプリタは Web Worker 内で
実行され、
fetchはリクエストをページのメインスレッドに引き渡します。メイン スレッドがブラウザの本物のfetch()を (CORS の制約の下で) 実行している間も プログラムは動き続けるためリクエストは並行し、awaitはレスポンスの到着まで ワーカーをブロックします。クロスオリジン分離が使えない環境 (SharedArrayBufferが無効) では、fetch ごとに同期リクエストへフォールバック します — プログラムの動作は同じですが、リクエストは並行しません。
制限事項
- メソッドは
GET、HEAD、POST、PUT、DELETE、OPTIONS、PATCHのいずれかで なければなりません。サポートされない:methodはエラーです。インタプリタと JVM はfetchの時点で拒否します。WASM バックエンドはメソッドを静的に解決し、静的に判明している サポート外の:methodをコンパイル時に拒否します (実行時に計算されるメソッドはそこで チェックできないため GET として扱われます。一方で実行時に計算される:bodyは通常 どおり送信されます)。 - 失敗したリクエスト (例えば接続拒否) は future を await した時点で顕在化します —
JavaScript の
awaitの reject と同じタイミングで、どのバックエンドもそこで エラーをシグナルします (WASM ではhandler-caseで捕捉できるrontolisp:wit-errorコンディションです)。リクエストの開始自体ができない場合 (例えば不正な URL、インタプリタ/JVM での実行時計算のサポート外メソッド) はfetch自体がエラーになるか、WASM では future の代わりにnilを返します —nilを await するとnilになります。