title: みんなのバリアフリーマップAIで投稿APIタイムアウト時のフォールバックを実装した話
tags:
- JavaScript
- FastAPI
- AbortController
- LocalStorage
- FieldTest
private: false
はじめに
入院先病院と自宅Wi-Fiで実施したフィールドテスト中、投稿APIが net::ERR_CONNECTION_TIMED_OUT を吐くケースに遭遇しました。ユーザー体験を止めないため、posts.js に AbortController を組み込み、タイムアウト時はローカルストレージへフォールバックさせる仕組みを実装したので共有します。
背景
-
フロントエンド 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が「読み込み中」のまま止まり、フィールドテスト用のフローが中断される課題があった。
実装のポイント
AbortControllerで2.5秒タイムアウト- 失敗時はローカルストレージに投稿内容を保存
- 再取得時もまず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×フロントエンド構成でも AbortController と localStorage を組み合わせれば簡潔に実装できました。同様の課題に直面している方の参考になれば幸いです。