HTTP クライアント (rontolisp.http-client)
rontolisp.http-client は HTTP リクエストを送ります。API は
babashka.http-client と同じで、オプションマップを
受け取る request と、URL とオプションを受け取る get、post、put、delete、head、
patch があります。組み込みなので、clojure.string と同じように require します。
すべてのリクエストは rontolisp:fetch を通るため、fetch が
使えるすべてのトランスポートで同じソースが動きます。
| ターゲット | ビルド | トランスポート |
|---|---|---|
| インタプリタ、JVM | rontolisp app.clj、-o App.class | JDK の HttpClient |
| WASM コンポーネント | -o app.wasm --component、wasmtime run -S http=y で実行 | wasi:http |
| WASM リアクター | -o app.wasm --no-wasi --host-fetch | ホストの fetch(Cloudflare Worker、node) |
| ネイティブ実行ファイル | --native | 実行ファイル自身のクライアント |
素の Preview 1 モジュールと、--host-fetch なしの --no-wasi モジュールにはトランスポートが
ありません。コンパイルはそのプログラムを拒否し、トランスポートを与えるフラグを示します。
clojure.java.io も同じ fetch で http: URL を読みます(HTTP の URL)。
200 application/json
オプション
| オプション | 意味 |
|---|---|
:uri(または :url) | URL の文字列。http か https の絶対 URL |
:method(または :request-method) | :get(既定)、:head、:post、:put、:delete、:options、:patch |
:headers | 名前(文字列かキーワード)から文字列、または文字列の seq へのマップ。seq の要素はそれぞれ一つのフィールドとして送る |
:query-params | URL のクエリに URL エンコードして連結するマップ。値がコレクションならキーを繰り返す |
:form-params | application/x-www-form-urlencoded のボディとして送るマップ |
:body | 文字列。バイト配列、java.io.File、入力ストリーム(clojure.java.io のストリーム、応答の :as :stream のボディ、Ring のリクエストの :body)はそのオクテットを送る |
:multipart | パートの seq。:body と :form-params に代わって multipart/form-data のボディとして送る |
:basic-auth | [user pass] か {:user ... :pass ...}。Authorization: Basic ヘッダーになる |
:oauth-token | Authorization: Bearer ヘッダーになる |
:accept | :json で Accept: application/json |
:as | :string(既定。ボディを UTF-8 としてデコードする)、:bytes(ボディのオクテットをバイト配列で渡す)、:stream(ボディを読まずに渡す) |
:decompress-body | false なら圧縮されたボディを届いたまま渡す |
:throw | false ならどのステータスもそのまま返す |
:async | true ならレスポンスのフューチャーを返す |
:async-then、:async-catch | :async と併用し、レスポンスに適用する関数と、失敗を表すマップに適用する関数 |
リクエストは、:headers が指定しない限り Accept: */* と
Accept-Encoding: gzip, deflate を送り、加えて fetch 自身の User-Agent を送ります。
それ以外の :body は ex-info を投げます。
マルチパートのボディ
:multipart のパートは、:name(または :part-name)、:content(文字列、バイト配列、
java.io.File、入力ストリーム)と、省略可能な :file-name、:content-type からなるマップです。ボディは
babashka.http-client のものとオクテット単位で同じです。各パートは Content-Disposition
(File か :file-name があれば filename つき)、Content-Type(文字列は
text/plain; charset=UTF-8、File は拡張子から決め、それ以外は
application/octet-stream)、Content-Transfer-Encoding を持ちます。境界は
babashka_http_client_Boundary とランダムな UUID で、リクエストの content-type がそれを
示します(:headers で指定したものは置き換えます)。--host-random なしの --no-wasi
リアクターでは、UUID は random が使う生成器から引きます。出力したグルーがこれをシードします。
200 multipart/form-data; boundary=
入力ストリームとして開けない :content は IllegalArgumentException
(Cannot open <42> as an InputStream.)を、存在しない File は
java.io.FileNotFoundException を投げます。
レスポンス
:status、:headers(小文字の名前。応答が繰り返したフィールドは値のベクター)、:body、
:uri(リダイレクト後に応答した URL)、:request(送ったときのオプション)のマップです。
リダイレクト(301、302、303、307、308)は babashka.http-client の既定のクライアントと同じく
たどります。たどるのは 4 回までで、https から http へは移らず、303(と POST の 301、
302)はボディなしの GET にし、別のオリジンへ移るときはリクエストの Authorization、
Cookie、Origin、Referer、Host を外します。
200-207、300-304、307 以外のステータスは、:throw false でない限り
Exceptional status code: N という ex-info を投げます。そのデータはレスポンスです。
[Exceptional status code: 404 404]
404
トランスポートの失敗(接続できない、転送が途中で切れたなど)は、トランスポートのメッセージを
持つ java.io.IOException を投げます。JDK が受け付けない URL は、呼び出しの時点で
IllegalArgumentException になります(Illegal character in path at index 24: ...、
URI with undefined scheme)。
圧縮された応答
Content-Encoding が gzip か deflate(zlib。サーバーがこの名前で送る生の DEFLATE も
含む)の応答は、:as が読む前に展開します。そのため :body はテキスト、:as :stream では
展開したオクテットです。:headers には符号化の名前が残ります。HEAD リクエスト、
:decompress-body false、それ以外の符号化では、ボディは届いたままです。符号化が示す形式に
なっていない応答は、java.util.zip と同じものを投げます。java.util.zip.ZipException
(Not in GZIP format、Corrupt GZIP trailer、invalid block type など)か、途中で
切れていれば java.io.EOFException(Unexpected end of ZLIB input stream)です。gzip の
ヘッダーは呼び出しの時点で、残りはボディを読むときに読みます。
gzip {
非同期リクエスト
:async true はすぐにフューチャーを返します。deref(と @)はレスポンスを待ち、
(deref f ms timeout-val) は最大 ms ミリ秒だけ待ちます。future? は true で、
future-done? はレスポンスが届いたかを答えます。リクエストは取り消せないので
future-cancel は false を返し、future-cancelled? も false です。失敗は deref の
時点で、その例外を原因とする java.util.concurrent.ExecutionException として投げられます。
true 200 true
Preview 1 モジュール(--native、--host-fetch)では、呼び出しが戻る前にリクエストが
完了するため、時間制限つきの deref もレスポンスを返します。
ストリームとしての応答
:as :stream を指定すると、:body は応答のオクテットを読まずに保持する
java.io.InputStream です。JDK のクライアントの応答ストリーム、または圧縮された応答を
読むための java.util.zip.GZIPInputStream か InflaterInputStream です。.read は届いた
順に次のオクテットを返し(終端では -1)、バイト配列への .read は手元に届いたオクテットで
配列を埋めます。.readNBytes と .readAllBytes は求めた分がそろうまで読みます。.skip、
.available、.transferTo、clojure.java.io/copy(File か出力ストリームへ、オクテットの
まま)もこれを受け付けます。
slurp と clojure.java.io/reader はテキストとして読み(:encoding で別の文字セットを
指定しない限り UTF-8)、with-open と .close は閉じます。Ring ハンドラがこれを
レスポンスの :body として返すと、オクテットは届いた順にそのまま中継されます。
true {
ボディに対するリーダーは、届いたオクテットから順にデコードします。.readLine、.read、
read、遅延する line-seq は、そのテキストが届いた時点で返るため、終わらない応答
(server-sent events、NDJSON、ログの追尾)も一行ずつ追えます。UTF-8 は Java の
InputStreamReader と同じくデコードし、不正なシーケンスはそれぞれ一つの U+FFFD になります。
:as :string のボディと、ストリームに対する slurp も同じです。
{
Ring のプロキシ:
$ cat proxy.clj
(ns proxy
(:require [ring.adapter.rontolisp :refer [run-server]]
[rontolisp.http-client :as http]))
(defn handler [req]
(let [r (http/get (str "https://example.com" (:uri req)) {:as :stream :throw false})]
{:status (:status r) :body (:body r)}))
(run-server handler {:port 3000})
$ rontolisp proxy.clj -o src/worker.wasm --no-wasi --host-fetch --host-boundary=streaming --emit-js-glue
相違点
- レスポンスに
:versionはなく、:uriは文字列です(babashka.http-client ではjava.net.URI)。 - 名前を挙げて拒否するもの: オプションの
:client、:interceptors、:timeout、:version、:raw、:expect-continue、java.net.httpのクライアントを作る var のclient、default-client-optsと->で始まるビルダー、そして名前空間babashka.http-clientそのもの(このページを案内します)。 - トランスポートの失敗は
java.io.IOExceptionです(babashka.http-client ではjava.net.ConnectExceptionなどのサブクラス)。 --host-fetchではホスト自身のfetchがリダイレクトを(20 回まで)たどるため、:uriは リクエストした URL になります。また応答の展開もホストが行うため、:headersにはそのcontent-encodingとcontent-lengthがありません。