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 規則に乗る)。
ショートカット (get〜patch) は ヘッダー指定のない常用形。リクエストヘッダーや任意メソッドが要るときは 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 | 応答本文の生バイト列 |
body と body_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.headers と request の req.headers (§8) はともに std:map の InsertionMap (../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):
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。bodyは String または 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 にはその応答の Location と Set-Cookie が載る |
"manual" を持つのは、追従の途中で捨てられる情報があるからである。 自動追従は各ホップの応答を返さないので、3xx が焼いた Set-Cookie を呼び手が受け取れない。このクライアントは cookie を持ち回らない (§12) ので、追わせると POST → 303 → GET の形で組んだログインの往復が書けない — 焼かれた cookie が次のホップへも呼び手へも渡らない。"manual" なら呼び手が 3xx を見て、自分で cookie を載せて次を投げられる。
"manual"ではホップが起きないので、上限超過のconnect_error(§10) も起きない- 追う・追わないの判断だけを変える。method の落とし方 (§10) やタイムアウトは変わらない
- ショートカット (
get〜patch) はこのスロットを持たない。設定を取る口は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:map の InsertionMap で組み立てる (§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 |
request の req レコードのスロットが不正 (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 - 生の制御文字を含む URL →
invalid_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)。requestのreqレコード内のスロット不正のみinvalid_requestで回復可能 (§8)
message は http.get: ... のように関数名を含む人間可読文字列。分岐は match res { Ok(v) => … Err(e) => match e.kind {...} } で行い、message の文字列マッチに依存しないこと。
10. 既定動作 (タイムアウト・リダイレクト)
リクエスト単位の設定は持たず、以下の既定で動く (設定可能化は将来枠 §12):
- タイムアウト: 全関数に 30 秒の上限を内蔵する (接続〜本文読み取り完了まで)。超過は
timeout(§9)。無制限ハングを避けるための既定で、無効化・延長の手段は持たない - リダイレクト: 3xx を自動追従する (上限 10 ホップ)。上限超過は
connect_error。Response.statusには最終応答のステータスが入る。request(§8.1) だけがこの既定を要求ごとに外せる — 他の関数は外す口を持たない
11. ネットワークと capability
std:http/client の各関数は ホストのネットワーク経由でリクエストを送る。std:http/client を import すること自体が「このコードはネットワーク能力を使う」という宣言になり、どのコードが外部通信するかが静的に追える (../index.md の capability 可視化)。実際の到達可否は実行環境のネットワークポリシー (プロキシ・許可ホスト等) に従い、std:http/client はそれを上書きしない。
hikari build (../../build.md) が生成する embed バイナリにも std:http/client モジュール (ホストコード) は常に含まれ、std:http/client の import は常に解決可能 (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/server の serve (server.md §14) が待ち受けている port へ loopback の綴り (127.0.0.1 / localhost) で要求を出すと、その要求はソケットを介さずにその待ち受けへ届く。生の HTTP を組んで渡すので、応答の形も本書が定めるとおりになる。
待ち受けが始まる前に叩けば connect_error である。したがって「繋がるまで短く刻んで待つ」形はブラウザーでもそのまま働く。
12. 将来枠
以下は持たない:
- リクエスト単位のタイムアウト / プロキシ設定 (§10 の既定固定)。リダイレクトだけは
requestがredirectで選べる (§8.1) - クッキー管理・自動リトライ・コネクションプールの明示制御
- ストリーミング (本文は一括読み込み)・チャンク送信・multipart 構築ヘルパー
Future / scheduler との関係は prelude.md §9.4 と同じ。root リテラル本体の評価が完了した時点で未解決の http Future は language-spec.md §13.3 のとおり放棄される (実装は進行中の要求を打ち切る責任を持つ)。