本文へ移動
Hikari 仕様

std:http/client — HTTP クライアント

標準ライブラリモジュール (../index.md)。本書中の裸の §N は本書の節を指す。言語の意味論は ../../language-spec.md を参照。共通型 (Request / Response / Handler) は ../http.md、HTTP サーバーは server.md を参照。

import http := "std:http/client"get / head / delete / post / put / patch / request の 7 slot を持つ namespace object を http に束縛する。名前 destructure (language-spec.md §6.3) で個別に取り出すか、namespace のまま slot アクセスする:

# 個別に引く
import { get, post } := "std:http/client"
res := get("https://example.com")!.unwrap!
print(res.status)                       # 200

# namespace のまま使う
import http := "std:http/client"
res := http.get("https://example.com")!.unwrap!
名前 効果 method body 意味
get get(url) Net GET なし url を GET し、Future (Result(Response)) を返す
head head(url) Net HEAD なし url を HEAD し、Future (Result(Response)) を返す (本文なし応答)
delete delete(url) Net DELETE なし url を DELETE し、Future (Result(Response)) を返す
post post(url, body) Net POST あり url へ body を POST し、Future (Result(Response)) を返す
put put(url, body) Net PUT あり url へ body を PUT し、Future (Result(Response)) を返す
patch patch(url, body) Net PATCH あり url へ body を PATCH し、Future (Result(Response)) を返す
request request(req) Net 任意 任意 req レコードで method/url/headers/body/redirect を指定し、Future (Result(Response)) を返す

以下の例では import http := "std:http/client" で束縛したものとして http.get 等で記す。全関数は Future を返し、! で resolve 値 (Result) を待つ (prelude.md §9.4 のブロッキング I/O 規則に乗る)。

ショートカット (getpatch) は ヘッダー指定のない常用形。リクエストヘッダーや任意メソッドが要るときは request (§8) を使う — ヘッダー指定の口は request だけ。

非 2xx はエラーにしない。404 / 500 等のサーバー応答も Ok(Response) で返り、Response.status (Int) で判定する。Err になるのは URL 不正・接続失敗・タイムアウト等の トランスポート層の失敗 だけ (§9)。

1. Response レコード

成功時の Ok(res)res はスロット {status, headers, body, body_bytes} を持つ closed・immutable なレコード:

スロット 意味
status Int HTTP ステータスコード (例 200 / 404)
headers InsertionMap(String, String) 応答ヘッダー。ヘッダー名 (String) → 値 (String) (§2)
body String 応答本文を UTF-8 String として読んだもの (デコードなし。生バイトをそのまま String 化)
body_bytes Bytes 応答本文の生バイト列

bodybody_bytes は同じ本文の 2 つの見方。テキスト API は body、バイナリ (画像等) は body_bytes を使う。本文に不正な UTF-8 が含まれる場合、body はバイト列をそのまま String として持ち後続の文字列処理が壊れ得る (fs.read と同じ実装定義の振る舞い)。確実に扱うなら body_bytes を使うこと。HEAD 応答など本文が無い場合は body が空 String、body_bytes が空 Bytes。

この形は std:http (../http.md) の構造的型 Response を満たす。std:http/server のハンドラー応答としてそのまま返せる。

match http.get("https://example.com/api")! {
  Ok(res) => match res.status {
    200 => use(res.body)
    404 => print("not found")
    _ => print("http status: " + res.status)
  }
  Err(e) => print(e.message)
}

2. ヘッダーマップ

Response.headersrequestreq.headers (§8) はともに std:mapInsertionMap (../map.md) で表す。キーがヘッダー名 (String)、値がヘッダー値 (String) である。

ヘッダー名は - を含み、Hikari の識別子として不正なのでオブジェクトのスロットでは表せない。加えてヘッダー集合は実行時に決まるものであり、書いた時点で名前が決まっている record のフィールドではない (language-spec.md §7.5)。挿入順で反復する InsertionMap を使うのは、受信・送信の順序を保つためである。

  • 応答側 (Response.headers): 同名ヘッダーが複数あるときは値を , (カンマ+空白) で連結した 1 つの String にする。キーは受信したヘッダー名を正規化した形 (各語頭大文字、例 Content-Type)。反復順は正規化後のヘッダー名の昇順で、同じ応答に対して決定的である。
  • 要求側 (req.headers): キーをヘッダー名、値を String としてそのまま送る。値が非 String のエントリがあると invalid_request (§9)。InsertionMap 以外を渡した場合も invalid_request
  • 名前と値の CR (0x0D) / LF (0x0A) は線へ出す前に除去する。 ヘッダーの区切りは CR LF なので、名前や値にそれを含めたまま送れると、ヘッダー 1 つの値がヘッダーの列そのものになる — 外から来た綴りをヘッダーへ載せたプログラムは、1 本の接続へ 2 本目の要求行を差し込まれる (要求分割)。CR / LF はヘッダーの名前にも値にも正当に現れないので、除去して失うものは無い。符号化ではなく除去である (符号化すると出る文字そのものが変わり、名前をそのまま読む受け手と噛み合わなくなる)。invalid_request にはしない — 落として失うものが無いところに分岐を増やさない。同じ規則を応答の組み立て側も持つ (server.md §3)。

この正規化・連結の規則定義は本節が正であり、std:http (../http.md) の Request.headers / std:http/server の Request / Response の headers からも参照される。

import m := "std:map"

res := http.get("https://example.com")!.unwrap!
res.headers.get("Content-Type")         # 受信した値。ヘッダーが無ければ None
res.headers.get("Content-Type").or("")  # 同じ値。無ければ ""

hdrs := m.insertion.of([("Content-Type", "application/json")])
hdrs.get("Content-Type")                #> Some("application/json")

3. http.get

  • 引数: url (String) 1 個。非 String を渡すと呼び出し位置で Error (panic 型, language-spec.md §16)
  • GET リクエストを送る。リクエスト本文は無し
  • 戻り値: Future! (= ()) で resolve 値を待つ
  • resolve 値は Result(Response):
    • 成功時: Ok(res)res は Response レコード (§1)。HTTP ステータスに関わらず応答が返れば成功 (非 2xx も Ok)
    • 失敗時: Err(e) — 回復可能エラー {kind, message} (§9)
match http.get("https://example.com")! {
  Ok(res) => print(res.body)
  Err(e) => print(e.message)
}

4. http.head

  • 引数: url (String) 1 個。非 String を渡すと呼び出し位置で Error
  • HEAD リクエストを送る。応答にヘッダーのみが期待され、Response.body / body_bytes は通常空
  • 戻り値: Future (Result(Response))。挙動は get (§3) と同じ (method のみ HEAD)

5. http.delete

  • 引数: url (String) 1 個。非 String を渡すと呼び出し位置で Error
  • DELETE リクエストを送る。リクエスト本文は無し
  • 戻り値: Future (Result(Response))。挙動は get (§3) と同じ (method のみ DELETE)

6. http.post

  • 引数: (url, body) 2-tuple。url は String。bodyString または Bytes。型違いは呼び出し位置で Error
  • POST リクエストを送る。body (String ならその UTF-8 バイト列、Bytes ならそのまま) をリクエスト本文にする
  • Content-Type ヘッダーは付けない。必要なら request (§8) でヘッダー指定する
  • 戻り値: Future (Result(Response))。応答の扱いは get (§3) と同じ
match http.post("https://example.com/api", "{\"name\":\"hikari\"}")! {
  Ok(res) => print(res.status)
  Err(e) => print(e.message)
}

8.1 redirect (3xx を追うか)

振る舞い
"follow" (既定) 3xx を自動追従する (§10)。Response は最終応答
"manual" 3xx を追わずにそのまま返すResponse.status は 3xx で、headers にはその応答の LocationSet-Cookie が載る

"manual" を持つのは、追従の途中で捨てられる情報があるからである。 自動追従は各ホップの応答を返さないので、3xx が焼いた Set-Cookie を呼び手が受け取れない。このクライアントは cookie を持ち回らない (§12) ので、追わせると POST → 303 → GET の形で組んだログインの往復が書けない — 焼かれた cookie が次のホップへも呼び手へも渡らない。"manual" なら呼び手が 3xx を見て、自分で cookie を載せて次を投げられる。

  • "manual" ではホップが起きないので、上限超過の connect_error (§10) も起きない
  • 追う・追わないの判断だけを変える。method の落とし方 (§10) やタイムアウトは変わらない
  • ショートカット (getpatch) はこのスロットを持たない。設定を取る口は request だけであり、ヘッダーと同じ区分けである (§6)

7. http.put / http.patch

  • 引数: (url, body) 2-tuple。post (§6) と同形 (url は String、body は String または Bytes)。型違いは呼び出し位置で Error
  • それぞれ PUT / PATCH リクエストを送る。挙動は method を除き post と同じ

8. http.request

任意 method・カスタムヘッダー付きリクエストを送る汎用形。ショートカット (§3〜7) で足りない場合に使う。

  • 引数: req (レコード) 1 個。非レコード (スロットを持たない値) を渡すと呼び出し位置で Error
  • req のスロット:
スロット 必須 意味
method 必須 String HTTP メソッド ("GET" / "POST" 等)。大文字小文字はそのまま使う
url 必須 String 送信先 URL
headers 任意 InsertionMap(String, String) リクエストヘッダー (§2)。省略時はヘッダー無し
body 任意 String / Bytes リクエスト本文。省略時は本文無し
redirect 任意 String 3xx の扱い。"follow" (既定) か "manual" (§8.1)
  • method / url の欠落・非 String、headers が非 InsertionMap・値が非 String、body が String でも Bytes でもない場合は 回復可能エラー Err(invalid_request) (§9)。redirect が String でない、または "follow" / "manual" のどちらでもない場合も同じ。req 自体が非レコードのときだけ呼び出し位置で Error (引数の型ミス)
  • 戻り値: Future (Result(Response))。応答の扱いは get (§3) と同じ

ヘッダーは std:mapInsertionMap で組み立てる (§2):

import m := "std:map"

hdrs := m.insertion.of([("Content-Type", "application/json")])
match http.request([
  method  := "POST",
  url     := "https://example.com/api",
  headers := hdrs,
  body    := "{\"name\":\"hikari\"}"
])! {
  Ok(res) => print(res.status)
  Err(e) => print(e.message)
}

9. エラーモデル

失敗時の err は回復可能エラーオブジェクト {kind, message} (language-spec.md §16)。std:http/client が返す正規 kind (閉じた一覧。全関数で共通):

kind 意味
invalid_url URL がパースできない、スキームが http / https 以外、または生の制御文字 (U+0000〜U+001F・U+007F〜U+009F) を含む (下記)
invalid_request requestreq レコードのスロットが不正 (method/url 欠落・型違い、headers/body の型違い)。§8
timeout 既定タイムアウト (§10) を超過した
connect_error DNS 解決失敗・接続拒否・TLS 失敗など接続を確立/完了できなかった
io_error 接続後の応答本文読み取り等のその他 I/O 失敗

新しい kind は設けず、上記 5 種で全関数を表す。種別が紛れやすい代表的な対応:

  • 非 2xx の HTTP 応答 (404 / 500 等) → エラーではなく Ok(Response)Response.status で判定する (§1)
  • 応答の綴りが壊れている (開始行・ヘッダー行が読めない、Content-Length が読めない — 数でない・負・64bit に収まらない・同名で値が食い違う —、chunked の綴りが壊れている) → connect_error読めない Content-Length を「接続が閉じるまでが本文」と読み替えない — 長さの分からない本文として読むと、途中で切れた応答を完全な応答として呼び手へ渡すことになる
  • リダイレクト上限超過・リダイレクト先での接続失敗 → connect_error
  • 生の制御文字を含む URLinvalid_url。CR (U+000D) / LF (U+000A) は要求行とヘッダーの区切りなので、含んだまま送れると要求行そのものが割れて 1 本の接続へ 2 本目の要求を差し込める (http.get(base + id) のように外から来た綴りを URL へ継ぐ形で踏む)。ヘッダーの値のように除去はしない — URL は運ぶデータではなくどの資源を取るかを選ぶ綴りなので、/a + CR LF + b/ab へ縮めると書き手が指していないものを黙って取りに行くことになる。RFC 3986 の URI に生の制御文字は現れないので、断って失う正当な綴りも無い (載せたい制御文字は percent 符号化して書く。%0d%0a は従来どおり素通しし、復号するのは受け手である)。空白は断らない — 要求行を割る力が無く、符号化せずに渡す呼び手を切ることになる。同じ規則を応答の組み立て側も持つ (server.md §3)
  • 引数の が違う (get(123) など) → Err ではなく呼び出し位置の Error (panic)。requestreq レコード内のスロット不正のみ invalid_request で回復可能 (§8)

messagehttp.get: ... のように関数名を含む人間可読文字列。分岐は match res { Ok(v) => … Err(e) => match e.kind {...} } で行い、message の文字列マッチに依存しないこと。

10. 既定動作 (タイムアウト・リダイレクト)

リクエスト単位の設定は持たず、以下の既定で動く (設定可能化は将来枠 §12):

  • タイムアウト: 全関数に 30 秒の上限を内蔵する (接続〜本文読み取り完了まで)。超過は timeout (§9)。無制限ハングを避けるための既定で、無効化・延長の手段は持たない
  • リダイレクト: 3xx を自動追従する (上限 10 ホップ)。上限超過は connect_errorResponse.status には最終応答のステータスが入る。request (§8.1) だけがこの既定を要求ごとに外せる — 他の関数は外す口を持たない

11. ネットワークと capability

std:http/client の各関数は ホストのネットワーク経由でリクエストを送る。std:http/clientimport すること自体が「このコードはネットワーク能力を使う」という宣言になり、どのコードが外部通信するかが静的に追える (../index.md の capability 可視化)。実際の到達可否は実行環境のネットワークポリシー (プロキシ・許可ホスト等) に従い、std:http/client はそれを上書きしない。

hikari build (../../build.md) が生成する embed バイナリにも std:http/client モジュール (ホストコード) は常に含まれ、std:http/clientimport は常に解決可能 (language-spec.md §13.4)。

ブラウザー (wasm) では自分のプログラムが立てた待ち受けにだけ届く

wasm ビルドはブラウザーの中で処理系を走らせる。ブラウザーは網を持たないので、外部の host へは届かないconnect_error (§9) になり、理由は「この環境が網を持たない」ことを述べる。「相手が落ちている」とは言わない。

https は届かないのではなく、呼び出しで止まる。 ブラウザーは TLS の実装を持たず、ページが積むのも平文のバイト列なので、https:// の URL を渡した http.get などは要求を 1 バイトも出さずに止まる。持っていない能力を「送れなかった」の顔をした Err に化けさせない規律 (§9) に従う — tls を渡した serve呼び出しで止まる (server.md §14) のと対である。走り出した要求が https へ転送された場合だけは、止めるには遅いので connect_error (§9) になる。

例外は同じプログラムが立てた待ち受けである。std:http/serverserve (server.md §14) が待ち受けている port へ loopback の綴り (127.0.0.1 / localhost) で要求を出すと、その要求はソケットを介さずにその待ち受けへ届く。生の HTTP を組んで渡すので、応答の形も本書が定めるとおりになる。

待ち受けが始まる前に叩けば connect_error である。したがって「繋がるまで短く刻んで待つ」形はブラウザーでもそのまま働く。

12. 将来枠

以下は持たない:

  • リクエスト単位のタイムアウト / プロキシ設定 (§10 の既定固定)。リダイレクトだけは requestredirect で選べる (§8.1)
  • クッキー管理・自動リトライ・コネクションプールの明示制御
  • ストリーミング (本文は一括読み込み)・チャンク送信・multipart 構築ヘルパー

Future / scheduler との関係は prelude.md §9.4 と同じ。root リテラル本体の評価が完了した時点で未解決の http Future は language-spec.md §13.3 のとおり放棄される (実装は進行中の要求を打ち切る責任を持つ)。