呼んで受け取る — 拒否は例外ではない
5 分2 編目で何があるかを知った。呼ぶ。
結果は値ではない
final ok = await client.callTool('desk.admit', {'count': 1});返るのは数ではなく コンテンツの一覧 だ。テキストかもしれないし画像かもしれないし複数かもしれない。だから取り出す場所を一箇所に置く。
Map<String, dynamic> decode(CallToolResult r) {
final first = r.content.first;
if (first is! TextContent) {
throw StateError('expected text content, got ${first.runtimeType}');
}
return jsonDecode(first.text) as Map<String, dynamic>;
}is! TextContent の検査を抜いて直にキャストすれば、サーバーがいつか画像を混ぜて送る日に まったく別の場所で 壊れる。ここで濾すからスタックが読める。
admit 1 -> {waiting: 2}拒否は throw で来ない
サーバー 3 編目で作った拒否を呼んでみる。
final refused = await client.callTool('desk.admit', {'count': 99});
final text = (refused.content.first as TextContent).text;
stdout.writeln('admit 99 -> isError=${refused.isError} "$text"');admit 99 -> isError=true "only 2 waiting"例外は出なかった。 await が正常に完了し、結果に isError: true が付き、理由がテキストに入っている。
これを知らないとこう書く。
try {
await client.callTool('desk.admit', {'count': 99});
// ここまで来たので成功
} catch (e) {
// 拒否はここに来る ... と思うが来ない
}拒否が catch に行かないので 成功として処理される。 画面に「99人入場」と出て、サーバーは誰も入れていない。
なぜ例外ではないのか
例外は「この呼び出しが成立しなかった」という意味だ。接続が切れた、そんな道具が無い。
拒否は 呼び出しが成立し、サーバーが判断した結果 だ。道具は正常に走り、答えが「だめだ」なのである。そしてその答えには理由が付いている — その理由を使うには結果として受け取らねばならない。
only 2 waiting を読めば 2 以下で送り直せる。例外として捕まえれば、その文はスタックトレースの中に埋まる。
検証
run step3 > captures/s3.txt || die "step3: failed (a refusal should not throw)"
grep -q 'admit 1 -> {waiting: 2}' captures/s3.txt || die "the call did not go through"
grep -q 'admit 99 -> isError=true' captures/s3.txt || die "the refusal was not surfaced"
grep -q 'only 2 waiting' captures/s3.txt || die "the reason did not survive"一行目が重要だ。拒否が例外で来ればプログラムが死に、終了コードが 0 でなくなる — || die がそこで捕まえる。
三つ目の検査(only 2 waiting)が実際に欠陥をひとつ捕まえた。サーバーの 4〜6 段階が、3 段階で分けた二つの拒否をひとつに潰していたのだ。サーバーだけを回していれば 3 段階の検査は通り、以降の段階はそのメッセージを見ないので永遠に分からなかった。 二つのサンプルを噛み合わせて回したから露見した。
自分で動かす
dart run bin/step3.dart
bash verify.sh持ち帰るもの
decode一箇所 — コンテンツ種別の検査をここでisErrorを読む — 拒否は結果だ- 拒否が例外で来ないかの検査 — 終了コードで確認される
次の編
画面を受け取る。クライアントに画面が一行も無い状態で。
サンプルを実行する
makemind-academy/course_client/git clone https://github.com/makemind-academy/course_client cd course_client dart pub get (cd course-server && dart pub get) dart run bin/step3.dart