本文へ移動
Hikari 仕様

std:http/server — HTTP サーバー

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

import server := "std:http/server"serve 1 slot のみを持つ namespace object を server に束縛する。std:http/serverimport すること自体が「このコードは接続を待ち受ける」という capability 宣言になる (../index.md の capability 可視化)。TLS 終端 (§12) の証明書読み込みもこの capability に含まれ、別途 std:fs を import する必要はない。

import { serve } := "std:http/server"

handler := { req | { status := 200, body := "hello, hikari!" |} }

print "listening on http://localhost:8080"
serve({ port := 8080 |}, handler)!

serveNet を持ち(../../language-spec.md §17.9)、要求ごとに呼ぶ Handler の効果を呼び出し元へ透過させる(EffectConduit)— ファイルを読むハンドラーを渡せば serve の呼びは NetFs を持つ。

1. serve(config, handler)

  • 引数: (config, handler) 2-tuple
    • 第 1 引数は 設定レコード: port 必須 + タイムアウト・停止シグナル・TLS 等の任意スロット (§9)。閉じた・本体なしのレコードであること。ポートのみで待ち受ける最簡形も serve({ port := 8080 |}, handler) とレコードで書く (Int の短縮形は持たない — 設定の入り口を 1 つに揃える)
    • 第 1 引数が設定レコードでない (レコード以外・本体付き)・port が欠落 / 非 Int / 範囲外・任意スロットの型違反等は、呼び出し位置で Error (panic 型, language-spec.md §16)。設定レコードの詳細な検証規則は §9
    • handler: 必須スロット 1 個の callable ({req | ...} 形)。非 callable・必須スロット数が 1 個でない場合は呼び出し位置で Error設定レコードが state を持つときだけ必須スロット 2 個 ({st, req | ...} 形) になる (§13)
  • バインド先は全インターフェース (バインドアドレスを選ぶ手段は持たない。§7)
  • 戻り値: Future! (= ()) で resolve 値を待つと、サーバーが終了するまでブロックする (prelude.md §9.4 のブロッキング I/O 規則に乗る)。root リテラル本体が serve(...)! で待っている限り、root の評価は完了しない
  • resolve 値は Result(State)State は設定レコードの state が運ぶ状態の型で、state を持たない設定では Unit である (§13):
    • 失敗時: Err(e)e は回復可能エラー {kind, message}kindlisten_error (bind 失敗・TLS 証明書読み込み失敗) / io_error (稼働中の致命的失敗) の 2 種のみ (§4)
    • 成功時: Ok(s)graceful shutdown が完了したときに発生する (§10)。s は最後のハンドラーが返した状態で、state を持たない設定では () になる。shutdown を設定しない限り従来どおり成功値には到達しない
  • 停止手段は設定レコードの shutdown (§10) で提供する。shutdown を設定しなければサーバーの停止はプロセス終了 (Ctrl-C 等) に委ねる

2. Request レコード (ハンドラーが受け取るもの)

handler に渡される req はスロット {method, path, query, headers, body, body_bytes} を持つ closed・immutable なレコード。実装が接続ごとに構築する。この形は std:http の型メンバー Request (../http.md §2) を満たす:

スロット 意味
method String "GET" / "POST" 等 (受信した表記のまま。大文字化などの正規化はしない)
path String クエリを除いたパス (例 /todos)
query String 生のクエリ文字列 (例 a=1&b=2。無ければ空 String)。パースはユーザー側の責務
headers InsertionMap(String, String) ヘッダー名 (String) → 値 (String)。正規化・同名連結の規則は client.md §2 と共通
body String 本文を UTF-8 String として読んだもの
body_bytes Bytes 本文の生バイト列

3. Response レコード (ハンドラーが返すもの)

handlerreq を受け取り、スロットを持つレコードを返す (状態を持つサーバーでは (Response, State) の 2-tuple を返す。§13)。この形は std:http の型メンバー Response (../http.md §3) を満たす必要がある:

スロット 必須 意味
status 必須 Int HTTP ステータスコード (100599)
headers 任意 InsertionMap(String, String) ヘッダー名 → 値 (client.md §2 と共通の規則)。省略時はヘッダーなし
body 任意 String / Bytes 応答本文 (一括返却)。省略時は空
body_bytes 任意 Bytes 応答本文の生バイト列 (一括返却)
stream 任意 callable { sink | ... } ストリーミング (逐次) 応答の生産者 (§11)
  • 本文の決め方:
    • stream があればストリーミングモードに入り、body / body_bytes は無視する (§11)
    • stream が無ければ一括返却。bodybody_bytes両方あれば body_bytes を優先する
  • Content-Length 等の実装が付与すべきヘッダーは handler が指定しなくても実装側が補う (ストリーミング時は Content-Length を付けず chunked transfer にする。§11)
  • 余分なスロットは無視する
  • ヘッダー名と値の CR (0x0D) / LF (0x0A) は線へ出す前に除去する。 ヘッダーの区切りは CR LF なので、値にそれを含めたまま書けると、ヘッダー 1 つの値がヘッダーの列そのものになる — 要求の綴りを応答ヘッダーへ写したハンドラーは、任意のヘッダーと任意の本文を注入される (応答分割)。CR / LF はヘッダーの名前にも値にも正当に現れないので、除去して失うものは無い。符号化ではなく除去である (符号化すると出る文字そのものが変わり、名前をそのまま読むクライアントと噛み合わなくなる)。応答を 500 にはしない — 落として失うものが無いところに分岐を増やさない。同じ規則を要求の組み立て側も持つ (client.md §2)。

status を持つ限り headers / body / body_bytes / stream を省略できるため、std:http/client の応答レコード (status に加え headers / body / body_bytes を常に持つ。client.md §1) は幅サブタイプとしてそのまま Response を満たす。これによりプロキシ的なハンドラーが 1 行で書ける:

import { serve } := "std:http/server"
import client := "std:http/client"

serve({ port := 8080 |}, { req | client.get("https://example.com" + req.path)!.unwrap! })!

ハンドラーの評価が panic した場合、または戻り値が Response の形を満たさない場合 (status 欠落・非 Int 等)、その接続には 500 (空本文) を返し、サーバーは稼働を継続する (1 接続の失敗でサーバー全体を落とさない)。この失敗は serveErr にはしない (§4)。ストリーミングでは status 送出後の panic は 500 に差し替えられない — 扱いは §11

4. エラーモデル (serveErr)

serve の resolve 値が運ぶ Err(e)kind は閉じた一覧:

kind 意味
listen_error bind 失敗 (ポート使用中・権限不足等)、および TLS 証明書/鍵の読み込み失敗 (§12)
io_error 稼働中の致命的 I/O 失敗 (accept を継続できない等)
  • ハンドラー由来の失敗 (panic・不正な応答レコード) は Errしない§3 のとおり 500 を返し稼働を継続する
  • 要求の綴りが壊れているときも Err にしない — その接続に 400 (空本文) を返して閉じ、サーバーは稼働を継続する。壊れているとは次のいずれかである:
    • 開始行・ヘッダー行が読めない
    • Content-Length が読めない (数でない・負・64bit に収まらない・同名で値が食い違う)
    • Transfer-Encoding: chunked の本文の綴りが壊れている

読めない Content-Length を「本文なし」と読み替えてはならない。 本文を付けたつもりの要求が本文の無い要求としてハンドラーへ渡ると、ハンドラーは嘘の要求に答えることになる。壊れた chunked を断るのと同じ扱いにする

  • ヘッダーが max_header_bytes (§9) を超えたら 431 (空本文) を返して閉じる
  • 引数の型ミス (port / handler / 設定レコードの各スロット) は Err ではなく呼び出し位置の Error (§1§9std:http/client の引数型ミスと同じ区分け)
  • ストリーミング中の書き込み失敗 (クライアント切断等) は sink.write(chunk)! が運ぶ Err で表れ (§11)、serveErr にはしない — サーバーは稼働を継続する
  • graceful shutdown の完了は Err ではなく Ok(s)s は最後の状態で、state を持たない設定では () (§1§10§13)
match serve({ port := 8080 |}, handler)! {
  Ok(_) => ()  # shutdown を設定しなければ到達しない (§1・§10)
  Err(e) => print e.message
}

5. 並行モデル

リクエストごとに handler の評価を別フローで開始する (fork と同じ機構、prelude.md §9.1)。handler の中で ! (await) すると、その待機中に他のリクエストの処理が scheduler 上で進む — 既存の協調スケジューラーの意味論をそのまま使い、追加の並行規則は設けない。ストリーミング応答の sink.write(chunk)! も同じく中断点になり、flush 中に他リクエストが進む (§11)。

状態を持つサーバー (§13) ではハンドラーが交錯しない。 状態はちょうど 1 つのハンドラーが所有し、次のハンドラーは前のハンドラーが状態を返してから開始する。ハンドラーの中に中断点があっても、その待機中に別のリクエストのハンドラーが同じ状態を見ることはない。所有が 1 つであることの帰結であり、追加の錠前ではない。state を持たない設定の並行性は従来どおり変わらない。

6. root 完了時の扱い

serve(...)! を root リテラル本体が待っている限り root の評価は完了しないため、通常は放棄が起きない。serve の呼び出し元が ! を経ずに Future を放置したまま root 本体の評価が完了した場合、この Futurelanguage-spec.md §13.3 の「未解決 Future の放棄」規則に従う。実装は listener を閉じる責任を負う。これは shutdown 設定 (§10) による穏当停止とは別経路であり、放棄時の resolve 値は Ok(s) ではない (graceful shutdown 完了を意味しないため)。

7. 持たないもの

以下は持たない。頭に「委ねる」と書いたものは、この層に入れないと決めたものである。残りは将来枠。

  • (委ねる) ルーティング・middleware 構造・静的ファイル配信 — pkg: エコシステムに委ねる。Handler 型 (../http.md §4) がその契約点になる
  • 接続の使い回し (keep-alive) — 応答には Connection: close を付け、書き終えたら閉じる。接続ごとにちょうど 1 要求なので、穏当停止 (§10) の drain は「開いている接続を待つ」だけで済み、待機中の遊休接続を数える必要が無い。遊休接続そのものが無いので、アイドル上限の設定 (§9) も持たない
  • HTTP/2・WebSocket
  • リクエスト本文のストリーミング読み (受信側。応答側のストリーミングは §11 で提供)
  • mTLS (クライアント証明書検証)・TLS 最小バージョン / 暗号スイートの明示設定 (TLS は cert_file / key_file による終端のみ。§12)
  • バインドアドレスの選択 (全インターフェース固定)
  • 状態の細かい引き渡しstate (§13) を使うサーバーでは、ハンドラーが状態を返すまで次のハンドラーが始まらない (§5)。中断点の手前で状態を手放して並行性を取り戻す口は持たない。状態を持つハンドラーの中で待つと、その待機がサーバー全体の待機になる点に注意する。待ちを伴う仕事は状態を返した後の stream (§11・状態は捕獲できない) か、状態を持たない別のサーバーへ寄せる
  • 状態を持つサーバーでのストリーミングからの状態更新stream の生産者は応答を返した後に走るので状態を捕獲できない (§13)。生産者から状態を書き換える口は持たない

8. 例: match によるルーティング

ルーティングは組込のルーターを持たず、handler の中で match を使って書く。match のタプルリテラルパターンは型照合であり (prelude.md §8.4)、("GET", "/") のような値の組を要素ごとの == で照合するパターンは持たないため、method → path の入れ子 match で書く。ヘッダーは std:mapInsertionMap で組み立てる (client.md §2):

import { serve } := "std:http/server"
import json := "std:json"
import m := "std:map"

todos := ["milk", "bread"]

handler := { req |
  match req.method {
    "GET" =>
      match req.path {
        "/" => { status := 200, body := "hello, hikari!" |}
        "/todos" =>
          hdrs := m.insertion.of([("Content-Type", "application/json")])
          { status := 200, headers := hdrs, body := json.stringify(todos, 0).unwrap! |}
        _ => { status := 404, body := "not found" |}
      }
    "POST" =>
      match req.path {
        "/todos" => { status := 201, body := "added: ${req.body}" |}
        _ => { status := 404, body := "not found" |}
      }
    _ => { status := 404, body := "not found" |}
  }
}

print "listening on http://localhost:8080"
serve({ port := 8080 |}, handler)!

9. サーバー設定 (設定レコード)

serve の第 1 引数の 設定レコードは、ポート (port) に加えてタイムアウト等のサーバー単位の設定を運ぶ。port だけを持つ最簡形 serve({ port := 8080 |}, handler) から、任意スロットを足していくだけで設定を拡張できる (第 1 引数は常にこのレコード 1 種 — §1)。

スロット 必須 意味 省略時
port 必須 Int (165535) バインドするポート
state 任意 任意 (制限は §13) ハンドラーへ引き渡す初期状態 (§13) なし (ハンドラーは 1 スロット)
read_timeout 任意 Int (ms, 0) リクエスト全体の読み取り時間上限 0 (無制限)
write_timeout 任意 Int (ms, 0) 応答書き出しの時間上限 0 (無制限)
read_header_timeout 任意 Int (ms, 0) ヘッダー読み取りの時間上限 0 (無制限)
max_header_bytes 任意 Int (bytes, 0) リクエストヘッダーの最大バイト数 実装既定
shutdown 任意 Future 穏当停止のシグナル (§10) なし (停止手段なし)
shutdown_timeout 任意 Int (ms, 0) drain の時間上限 (§10) 0 (drain 完了まで待つ)
tls 任意 { cert_file: String, key_file: String |} TLS 終端 (§12) 平文 HTTP
  • 時間値の単位は integer ミリ秒で、prelude の sleep(ms) / Future.timeout(ms) と揃える (std:time への依存を持ち込まない)。0 は「上限なし」を表す
  • 各任意スロットが存在するのに型・範囲が不正 (非 Int、負値、tls が不正な形 等) な場合は、呼び出し位置で Error — bind 前に構造的に判定できる不整合は Err に運ばず即時に落とす (§4)
  • 未知の余分なスロットは無視する — Hikari の record は幅サブタイプ (language-spec.md §17.4) であり、Response (§3) が余分なスロットを無視するのと一貫させる。設定名の綴りエラーは静的には検出されない点に注意
  • 応答をストリーミング (§11) する場合、write_timeout応答全体 (全チャンクの送出完了まで) にかかる。長時間ストリーミングするサーバーは write_timeout := 0 にするか十分大きな値にする
import { serve } := "std:http/server"

serve({ port := 8080,
        read_timeout := 5000,
        write_timeout := 5000,
        max_header_bytes := 1048576 |},
      handler)!

10. graceful shutdown (shutdown)

設定レコードの shutdownFuture を渡すと、その Future解決した時点を停止シグナルとしてサーバーを穏当に終了させる。停止シグナルは fork / race / wait_any (prelude.md §9) など通常の Future の合成でユーザーが組み立てる — サーバーは専用の停止プリミティブを持たず、既存の並行機構との合成でこの口を実現する。

  • shutdownFuture解決した時点で (解決値の中身は問わない。Ok / Err / panic のいずれでも「解決した」ことだけを停止合図とみなす)、サーバーは新規接続の受付を止め、処理中 (in-flight) のハンドラーを drain する
  • drain 完了で serveFutureOk(s) に解決する (§1 で予約されていた成功値)。s は最後のハンドラーが返した状態で、state を持たない設定では () である (§13)
  • shutdown_timeout (ms) を与えると drain の待ち時間を上限で打ち切り、残る接続を強制 close する。超過して強制 close した場合も serveOk(s) に解決する (停止はユーザー意図どおり完了したため)。shutdown_timeout := 0 (既定) は「drain 完了まで待つ」
  • shutdownFuture が永久に解決しなければ、サーバーは従来どおり停止せず走り続ける (プロセス終了に委ねる)
  • graceful shutdown による停止は、root 完了時の放棄 (§6language-spec.md §13.3) や Future.cancel! (prelude.md §9.8) による停止とは別経路である。放棄・キャンセルで止まった場合の resolve 値は Ok(s) ではない (穏当停止の完了を意味しないため)。状態を持つサーバーをこの経路で止めると最後の状態は返らない
import { serve } := "std:http/server"

# 実際はシグナル待ちの Future 等。ここでは 10 秒後に停止する例
stop := fork { sleep 10000 }

match serve({ port := 8080, shutdown := stop, shutdown_timeout := 3000 |}, handler)! {
  Ok(_)  => print "stopped gracefully"
  Err(e) => print e.message
}

11. ストリーミング / チャンク応答 (stream)

応答レコードの stream生産者 (必須スロット 1 個の callable { sink | ... }) を置くと、応答本文を一括ではなく逐次 (チャンク) で送出できる。stream があるとき body / body_bytes は無視される (§3)。

  • serve はまず statusheaders を送出する。このとき Content-Length は付けず chunked transfer encoding にする
  • 続いて生産者を当該リクエストのフローとして評価し、引数に sink (実装が渡す不透明ハンドル) を渡す
  • sink は 1 つのメソッド write を持つ:
sink.write(chunk)!    # chunk: String / Bytes。Future(Result(Unit)) を返す
  • chunk を 1 チャンクとしてクライアントへ flush する。chunk が String / Bytes でなければ呼び出し位置で Error
  • 返り値は Future(Result(Unit))! で待つと、flush 成功で Ok(())、書き込み失敗 (クライアント切断等) で Err({kind, message}) に解決する。ブロッキング I/O 規則 (prelude.md §9.4) に乗り、flush 中は他フローが進む (§5)
  • ハンドラーは Err を観測したら送出を止められる (書き続けても無意味なため)。この Errserve 全体の Err にはしない (§4) — サーバーは稼働を継続する
import { serve } := "std:http/server"

handler := { req |
  { status := 200,
    stream := { sink |
      sink.write("first chunk\n")!.unwrap!
      sleep 100
      sink.write("second chunk\n")!.unwrap!
    } |}
}

serve({ port := 8080 |}, handler)!

12. TLS 終端 (tls)

設定レコードの tls{ cert_file: String, key_file: String |} (PEM ファイルのパス) を渡すと、平文 HTTP ではなく TLS で終端する。

スロット 必須 意味
cert_file 必須 String サーバー証明書 (PEM) のファイルパス
key_file 必須 String 秘密鍵 (PEM) のファイルパス
  • tls を与えたときは cert_file / key_file の両方が必須。欠落・非 String は呼び出し位置で Error (§9)
  • 証明書 / キーの読み込み失敗・bind 失敗は Err(listen_error) (§4)
  • 証明書ファイルの読み込みは serve (= 待ち受け capability) の動作の一部であり、別途 std:fs を import する必要はない (冒頭の capability 記述)
  • TLS はサーバー証明書による終端のみ。mTLS (クライアント証明書検証)・最小バージョン / 暗号スイートの明示設定は §7 のとおり持たない
import { serve } := "std:http/server"

serve({ port := 8443, tls := { cert_file := "server.crt", key_file := "server.key" |} |}, handler)!

13. 状態を持つサーバー (state)

設定レコードの state に初期状態を渡すと、serve はその状態をハンドラーの引数として引き渡し、ハンドラーが返した次の状態を次のリクエストへ回す。要求列に対する畳み込み (foldprelude.md §6) と同じ形である。

  • state を持つ設定では、handler必須スロット 2 個の callable { st, req | ... } になる。第 1 引数が状態・第 2 引数が Request で、順序は fold{ acc, x | ... } に揃える
  • ハンドラーは (Response, State) の 2-tuple を返す。第 1 要素が応答レコード (§3)、第 2 要素が次の状態である
  • 状態は受け付け順に 1 つずつ引き渡される。ハンドラーが交錯しないことは §5 に定める
  • serve の resolve 値は Result(State) で、graceful shutdown (§10) が完了したとき最後の状態が Ok(s) として返る (§1)
  • state を持たない設定では従来どおり handler は 1 スロット、戻りは応答レコードのみ、resolve 値の StateUnit である。state の有無で決まるのはこの 3 つで、他の設定スロットの意味は変わらない
  • handler の必須スロット数が state の有無と食い違う場合は、呼び出し位置で Error (§1§4 の区分けに従う。bind の前に構造的に判定できる)
  • 戻りが 2-tuple でない場合は接続ごとの失敗として扱う — その接続には 500 (空本文) を返し、状態は更新せず前の状態のまま次のリクエストへ回す (§3 の「戻り値が Response の形を満たさない場合」と同じ区分け)。ハンドラーの評価が panic した場合も同じで、状態は前のまま残る。1 接続の失敗で状態を失わない

状態を引数ではなく設定のスロットに置くのは所有のためである。 スロットへ置く形は record リテラルへ入れる形なので、既存の「コンテナーへ入れる=消費」規則 (static-analysis.md §2.14) がそのまま所有の移動を記録する。引数で受ける形にすると宣言型が型変数になり、型変数の位置には規律を課す先が無い (language-spec.md §17.8 の「将来枠」) ので所有が移らない。型の書きやすさより所有を採った — 設定の中身に依存する型は 1 つのシグネチャでは書けないので、その分の手当てを処理系が持つ。

import { Request } := "std:http"
import { serve } := "std:http/server"

# 状態は「これまでに受けた要求の数」。
handler := { st: Int, req: Request |
  n := st + 1
  { status := 200, body := "count=${n}\n" |}, n
}

match serve({ port := 8080, state := 0, shutdown := stop |}, handler)! {
  Ok(n)  => print "served ${n} requests"
  Err(e) => print e.message
}

13.1 状態に多重度型を持たせるとき

状態が多重度型の値を含む (language-spec.md §17.8) なら、状態そのものが追跡対象でなければならない — 多重度型そのものか、多重度を伝播する包み (enum の variant payload・タプル・Futurestatic-analysis.md §2.14) である。満たさない場合は静的検査が報告する (static-analysis.md §2.14state-not-owned)。

理由は所有が 2 つになることである。多重度型でない状態を state へ置いても、そこで消費として記録されない (多重度を伝播しない構造に入れた値の追跡は、そこで終わる — static-analysis.md §2.14)。すると呼び手の手元に同じ表が残り、そこから抜き出した資源と serve の側の資源が同じ実体を指す。二重の消費が静的にも実行時にも捕まらない。

状態を ExactlyOnce / AtMostOnce で包むと、state へ置いた時点が消費になり、以降その名前へ触れると use-after-consume になる。所有が serve へ移ったことが型で表れる。

import { Request } := "std:http"
import { serve } := "std:http/server"

# Lease は ExactlyOnce。表そのものも多重度型で包む。
type Table := ExactlyOnce({ held: {| List(Lease) } |})

handler := { st: Table, req: Request |
  got, rest := take(st, req.body)
  match got {
    Some(l) => { status := 200, body := l.done "ok" |}, rest
    None    => { status := 404, body := "no such lease\n" |}, rest
  }
}

serve({ port := 8080, state := empty_table!, shutdown := stop |}, handler)!

1 リクエストの中の規律はそのまま働く。 抜き出した資源はハンドラーの中でローカルな追跡対象なので、決着し忘れれば never-consumed、二重に決着させれば use-after-consume になる。捕獲が 1 つも無いため affine-not-copyable は起こらない。

14. ブラウザー (wasm) での実行

wasm ビルドでは、Playground (web/play.html) が実行対象ソースの std:http/serverimport を自動判定し、サーバーペインで実行する (ペインの定めは ../../playground.md)。この実行では待ち受けるのも要求を捌くのも同じ実装で、ソケットの代わりにページが積んだ生の HTTP のバイト列を受ける。要求のパースも応答の組み立ても同じ経路を通るため、Request レコードの形 (§2)・応答ヘッダーの補完と Content-Length (§3)・チャンク送出 (§11)・応答の値の形が不正なときの 500 (§3)・使用中の port への Err(listen_error) (§4) は、いずれもホストで走らせたときと同じ答えになる。

同じプログラムが std:http/client で自分の待ち受けへ出した要求も、この経路を通って届く (client.md §11)。

ブラウザーで再現しないのは次の 2 つだけで、本書が定める他の挙動は実機と同じである。

  • TLS 終端 (§12) — ブラウザーは TLS の実装を持たない。tls を渡した serve呼び出しで止まる。持っていない能力を「送れなかった」の顔をした Err に化けさせない規律 (§4) に従う
  • ハンドラーの panic による 500 (§3) — ホストでは panic の巻き戻しをフロー単位で捕まえて 500 へまとめ、稼働を続ける。wasm32-unknown-unknown は安定版で巻き戻せない (panic=abort。../../../internals/wasm-host.md) ので、ブラウザーでは捕まえる代わりに呼び出し全体が trap する。応答の値の形が不正なときの 500 (status を欠いた record・callable) は panic を伴わない値の検査だけで判定するので、そちらは両環境で同じ答えになる — 再現しないのは panic 経由の分だけである。この差は examples/http_panic_test.hika がゲートにしている

read_timeout (§9) はブラウザーでも発火する。刻むのはホストと同じ実時計ではなく、足止めのたびに進む番号 (hostclock::Mono。../../../internals/wasm-host.md) である。

それでも期限が来るのは、サーバーのセッション中はこの番号が実時間とほぼ 1:1 で進むためである — 待ち受けに他へ進む仕事が無い間、スケジューラーはこの番号を進め、サーバーのセッション中に限りそのぶん実時間でも足止めする。したがって read_timeout の期限は、ホストで走らせたときに近い時間で来る。ホストと違うのは、計算に費やした実時間が番号に載らないことだけである (番号が進むのは足止めのときだけなので)。