リトライハンドラ常時提供とコールドブートリトライ修正、ロギング拡充

hotwired/hotwire-native-ios

この PR は、失敗時のリトライハンドラを常に提供し、コールドブート時にリロードが無効になる問題を修正するとともに、主要フローに対する情報取得を容易にするロギングを体系的に拡充します。

背景

リトライハンドラが error.isRetryable に依存していた ため、404·401·403 等の非リトライ可能と判定されたエラーでは UI にリトライボタンが表示されず、ユーザーが手動で再試行できませんでした。また、アプリ起動直後の失敗では session.topmostVisitablenil になるため session.reload() が何もしないというコールドブート時のリトライ欠如がありました。これらの不整合は、エラー時のユーザー体験とデバッグ効率を阻害する要因として指摘されました。

技術的な変更

リトライハンドラの常時提供

Navigator.session(_:didFailRequestForVisitable:error:) でエラー種別に関係なくクロージャを生成し、session.topmostVisitable が存在しない場合は session.visit(visitable, reload: true)、存在する場合は従来通り session.reload() を呼び出すように変更しました。これにより、全エラーでリトライアクションが利用可能になり、コールドブート時でも実質的なリトライが実行されます。

@@
-        let retryHandler: (() -> Void)? = error.isRetryable ? { session.reload() } : nil
-        delegate?.visitableDidFailRequest(visitable, error: error, retryHandler: retryHandler)
+        let retryHandler: (() -> Void)? = {
+            if session.topmostVisitable == nil {
+                session.visit(visitable, reload: true)
+            } else {
+                session.reload()
+            }
+        }
+        delegate?.visitableDidFailRequest(visitable, error: error, retryHandler: retryHandler)

Session.reload の安全化とロギング

Session.reload()topmostVisitable が無い場合に早期リターンし、理由を log で出力します。topmostVisitable がある場合は対象 visitable を明示的にログに記録した上でリロードを実行します。

@@
-        guard let visitable = topmostVisitable else { return }
+        guard let visitable = topmostVisitable else {
+            log("Skipping session reload: no visitable found")
+            return
+        }
+
+        log("Reloading session with visitable: \(visitable)")

isRetryable の除去

HTTPError.swiftHotwireNativeError.swift から isRetryable プロパティと関連テストが削除され、API がシンプルになりました。リトライ判定はハンドラ側ロジックに委譲され、エラー種別に依存しない統一的な振る舞いが実現されています。

ハンドラ名とエラー型のリネーム

RedirectHandlerJSFetchRecoveryHandlerRedirectHandlerErrorJSFetchRecoveryError に名称が変更され、Turbo.js のフェッチ全般を対象にしたことを明示しました。実装ロジックは変わらず、URL 取得とリダイレクト判定のみを行います。デバッグ用に resolve 開始時の debug ログが追加されています。

@@
-enum RedirectHandlerError: LocalizedError {
+enum JSFetchRecoveryError: LocalizedError {
@@
-struct RedirectHandler {
+struct JSFetchRecoveryHandler {
@@
-            let result = try await RedirectHandler().resolve(location: location)
+            let result = try await JSFetchRecoveryHandler().resolve(location: location)
@@
-        } catch let error as RedirectHandlerError {
+        } catch let error as JSFetchRecoveryError {
@@
-        logger.debug("[JSFetchRecoveryHandler] resolve: \(location.absoluteString)")
+        logger.debug("[JSFetchRecoveryHandler] resolve: \(location.absoluteString)")

ロギングの体系的拡充

  • Navigator が開始時とルーティング決定時に info ログを出力し、開始 URL とルーティング対象 URL を記録します。
  • inspectAllSessions でセッション群を走査する際に info ログを出力し、デバッグ時にすべてのセッション状態を可視化します。
  • AppNavigationRouteDecisionHandler がルート決定時に同様の info ログを出力します。
  • ReloadWebViewPolicyDecisionHandler がリロード開始時に対象 URL を含む info ログを出力します。
  • WebViewPolicyManager の「ハンドラ未一致」メッセージを warning から info に格下げし、過剰な警告出力を抑制しました。
  • ColdBootVisit が HTTP ステータス失敗時に警告ログを残し、失敗原因の可視化を支援します。
  • VisitableViewController がデアクティベート時と位置解決時に現在の URL(または初期 URL)を info ログで出力し、状態遷移の追跡を容易にします。

設計判断

リトライハンドラを常時提供する方針 は、ErrorPresenter がハンドラ有無でリトライボタン表示を切り替える設計から、全エラーで UI にリトライ手段を提示できるよう統一した判断です。isRetryable の除去 は、導入後に破壊的変更として許容でき、ハンドラ側ロジックに委譲することで API を簡素化する意図があります。ハンドラ名のリネーム は、機能範囲がリダイレクトに留まらず Turbo.js フェッチ全般であることをコードベース上で明示し、可読性と意図の一貫性を高める設計判断です。ロギングのレベル調整 は、重要なフローは info に、実際の障害は warning に限定し、開発者が必要な情報を過不足なく取得できるよう配慮した結果です。

まとめ

本 PR は、リトライハンドラの提供をエラー種別に依存しない形で統一し、コールドブート時のリトライ欠如を修正しました。併せて isRetryable プロパティを除去し、ハンドラ名とエラー型をリネーム、ロギングを体系的に拡充したことで、エラー時の UI 挙動が一貫し、デバッグ情報が充実し、コードベースの意図が明確になりました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
c19e6cc6

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
3回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断(任意)、まとめの3部構成が明確に示されており、各要素が欠落していません。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは `swift:Source/...` 形式で正しく記述され、GitHubリンクは PR 番号 #247 が正しくリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的な iOS/Swift エンジニア向けの内容で、初心者向けの過度な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論→各論→結論の流れで構成され、段落はトピックセンテンスで始まり、1段落1トピック・長さも適切です。

Diff内容との照合 ⚠ WARNING

コードブロックとDiff内容の一致

多くのコード変更は Diff と一致していますが、Navigator の開始時やルーティング時に追加された `logger.info` 行、`inspectAllSessions` のロギング追加が記事に省略されています。内容の誤りではなく情報の抜けだけなので Warning としました。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている用語(retry handler、cold‑boot、JSFetchRecoveryHandler など)は PR と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明は Diff と PR の記載内容に基づいており、根拠があり論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張は PR のタイトル、説明、Diff で裏付けられており、推測や外部知識の付与はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #247 やコード中の行番号は正しく記載されています。数値的な誤りはありません。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR タイトルの意味を日本語で正確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事内に PR で言及されていないバージョン情報やリリース予定などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PR の記述と一致しています。