0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

みんなのバリアフリーマップAIで投稿APIタイムアウト時のフォールバックを実装した話

Posted at

title: みんなのバリアフリーマップAIで投稿APIタイムアウト時のフォールバックを実装した話
tags:

  • JavaScript
  • FastAPI
  • AbortController
  • LocalStorage
  • FieldTest
    private: false

はじめに

入院先病院と自宅Wi-Fiで実施したフィールドテスト中、投稿APIが net::ERR_CONNECTION_TIMED_OUT を吐くケースに遭遇しました。ユーザー体験を止めないため、posts.jsAbortController を組み込み、タイムアウト時はローカルストレージへフォールバックさせる仕組みを実装したので共有します。

背景

  • フロントエンド URL: http://localhost:8002(PC)/http://192.168.1.5:8002(iPhone, Vivaldi)
  • バックエンド: FastAPI (uvicorn backend.main:app --reload --host 0.0.0.0 --port 8002)
  • 病院Wi-FiではAP Isolationによりスマホからアクセス不可。自宅Wi-Fiへ切り替えて検証を継続。
  • タイムアウト発生中はUIが「読み込み中」のまま止まり、フィールドテスト用のフローが中断される課題があった。

実装のポイント

  1. AbortController で2.5秒タイムアウト
  2. 失敗時はローカルストレージに投稿内容を保存
  3. 再取得時もまずAPIを試し、失敗したらローカルから復元

実際のコード

class PostsManager {
  constructor() {
    this.storageKey = 'facility_posts';
    const baseUrl = window.API_BASE_URL || 'http://localhost:8002';
    this.apiUrl = `${baseUrl.replace(/\/$/, '')}/api/posts`;
    this.fetchTimeoutMs = 2500;
  }

  async createPost(facilityName, comment, imageFile = null) {
    const timestamp = new Date().toISOString();
    try {
      const formData = new FormData();
      formData.append('facility_name', facilityName);
      formData.append('comment', comment);
      if (imageFile) formData.append('image', imageFile);

      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.fetchTimeoutMs);

      const response = await fetch(this.apiUrl, {
        method: 'POST',
        body: formData,
        signal: controller.signal
      });
      clearTimeout(timeoutId);

      if (!response.ok) throw new Error(`APIエラー: ${response.status}`);
      const data = await response.json();
      return data;
    } catch (error) {
      console.warn('投稿APIタイムアウト/失敗→ローカル保存へフォールバック', error);
      this.saveToLocalFallback({ facilityName, comment, imageFile: null, timestamp });
      return { status: 'fallback', facility_name: facilityName, comment, timestamp };
    }
  }

  saveToLocalFallback(post) {
    const stored = JSON.parse(localStorage.getItem(this.storageKey) || '[]');
    stored.push(post);
    localStorage.setItem(this.storageKey, JSON.stringify(stored));
  }

  async getFacilityPosts(facilityName) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.fetchTimeoutMs);
    try {
      const response = await fetch(`${this.apiUrl}?facility_name=${encodeURIComponent(facilityName)}`, {
        signal: controller.signal
      });
      clearTimeout(timeoutId);

      if (!response.ok) throw new Error(`APIエラー: ${response.status}`);
      return await response.json();
    } catch (error) {
      clearTimeout(timeoutId);
      console.warn('投稿取得に失敗→ローカルから復元', error);
      return this.getFromLocalFallback(facilityName);
    }
  }

  getFromLocalFallback(facilityName) {
    const stored = JSON.parse(localStorage.getItem(this.storageKey) || '[]');
    return stored.filter(post => post.facilityName === facilityName);
  }
}

UIでの挙動

  • タイムアウト発生時はトースト通知で「ローカル保存に切り替え」を案内。
  • 取得時にフォールバックしたデータは、UI上で「仮保存(ローカル)」バッジを表示して区別。

フィールドテスト結果

  • PC6枚・モバイル5枚のスクリーンショットを取得し、UIが止まらないことを確認。
  • ローカル保存された投稿は、API復旧後に再送する運用フローをRunbookに追記。
  • SafariはURLを検索扱いにする課題があり、Vivaldiデスクトップ表示を推奨。

今後の改善

  • APIレスポンス時間をPC/モバイルで継続計測し、閾値を最適化。
  • iPhoneの位置情報許可が取得できない問題を解消。
  • navigator.onLine 判定やService Worker導入でオフライン対応を補完。

さいごに

タイムアウトや一時的なネットワーク断でもUXを守るためには、ローカルフォールバックが有効に働きます。FastAPI×フロントエンド構成でも AbortControllerlocalStorage を組み合わせれば簡潔に実装できました。同様の課題に直面している方の参考になれば幸いです。


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?