リトライハンドラ常時提供とコールドブートリトライ修正、ロギング拡充
この PR は、失敗時のリトライハンドラを常に提供し、コールドブート時にリロードが無効になる問題を修正するとともに、主要フローに対する情報取得を容易にするロギングを体系的に拡充します。
背景
リトライハンドラが error.isRetryable に依存していた ため、404·401·403 等の非リトライ可能と判定されたエラーでは UI にリトライボタンが表示されず、ユーザーが手動で再試行できませんでした。また、アプリ起動直後の失敗では session.topmostVisitable が nil になるため 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.swift と HotwireNativeError.swift から isRetryable プロパティと関連テストが削除され、API がシンプルになりました。リトライ判定はハンドラ側ロジックに委譲され、エラー種別に依存しない統一的な振る舞いが実現されています。
ハンドラ名とエラー型のリネーム
RedirectHandler → JSFetchRecoveryHandler、RedirectHandlerError → JSFetchRecoveryError に名称が変更され、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 挙動が一貫し、デバッグ情報が充実し、コードベースの意図が明確になりました。