(rontolisp) docs
← rontolisp パッケージの関数

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 — リクエストボディです。文字列なら UTF-8 で、(unsigned-byte 8) のベクターなら そのオクテットのまま送ります (ボディがなければ省略します)。

: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) ペアの連想リスト (名前は小文字、値 1 つに つき 1 ペアで set-cookie のように繰り返すフィールドはそれぞれ残り、名前の昇順) で、: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 はどのバックエンドでもこのストリームで、チャンクはヘッダーの後から届きます。 同じ future をもう一度 await すると同じリストが返り、そのボディストリームは最初の 読み切りで消費済みです。

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 (コアモジュール) の .wasm では 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 はこれまで通りコンパイルエラーです。JavaScript の fetch は gzip、deflate、br の応答を自分で展開するため、生成された ホストは展開後のオクテットを返し、その応答の content-encoding と content-length を :headers から除きます。他のバックエンドは符号化された ボディのオクテットを届いたまま返します。
  • ネイティブ実行ファイル (--native): 実行ファイルのランナーが、fetch が 返った瞬間から専用のスレッドでリクエストを HTTP/1.1 で送ります (HTTPS は 実行ファイルに組み込まれた Mozilla のルート証明書か、SSL_CERT_FILE が指す PEM バンドルを信頼します)。JVM と同じくリクエストは並行し、トランスポートの 失敗は await でシグナルされます。HTTP を 参照してください。
  • ブラウザ プレイグラウンド: 真に非同期です。インタプリタは Web Worker 内で 実行され、fetch はリクエストをページのメインスレッドに引き渡します。メイン スレッドがブラウザの本物の fetch() を (CORS の制約の下で) 実行している間も プログラムは動き続けるためリクエストは並行し、await はレスポンスの到着まで ワーカーをブロックします。クロスオリジン分離が使えない環境 (SharedArrayBuffer が無効) では、fetch ごとに同期リクエストへフォールバック します — プログラムの動作は同じですが、リクエストは並行しません。ブラウザは 圧縮された応答を展開するため、--host-fetch と同じく、その content-encoding と content-length は :headers にありません。

制限事項

  • メソッドは GET、HEAD、POST、PUT、DELETE、OPTIONS、PATCH のいずれかで なければなりません。サポートされない :method は、どのバックエンドでも fetch を 呼んだ時点でエラーになります。WASM では、リテラルで書かれたサポート外のメソッドは コンパイルの時点でエラーです。
  • 失敗したリクエスト (例えば接続拒否) は future を await した時点で顕在化します — JavaScript の await の reject と同じタイミングで、どのバックエンドもそこで エラーをシグナルします (WASM では handler-case で捕捉できる rontolisp:wit-error コンディションです)。同じ future を後で await し直すたびに、 同じエラーをシグナルします。リクエストを組み立てられない URL (例えばパスに空白を 含むもの) も同じ扱いで、fetch は future を返し、その await がエラーをシグナルします。
  • 転送が途中で失敗した応答 (予告した長さの本文が届く前に接続が閉じた場合) は、短い 本文として読めてしまうのではなく、エラーをシグナルします。シグナルするのは :body を読み切る時点です。