From 93d6db5e7cb2ebf39e1b77c4ddf32d4aacebb24d Mon Sep 17 00:00:00 2001 From: mehmetbahadirgungor Date: Fri, 23 Oct 2026 20:41:16 +0300 Subject: [PATCH] fix: catch unhandled errors when updating cache-hit metadata (#535) --- flutter_cache_manager/CHANGELOG.md | 4 ++ .../lib/src/cache_store.dart | 60 +++++++++++++------ flutter_cache_manager/pubspec.yaml | 2 +- .../test/cache_store_test.dart | 48 +++++++++++++++ 4 files changed, 96 insertions(+), 18 deletions(-) diff --git a/flutter_cache_manager/CHANGELOG.md b/flutter_cache_manager/CHANGELOG.md index 9513fb80..bd1dd16b 100644 --- a/flutter_cache_manager/CHANGELOG.md +++ b/flutter_cache_manager/CHANGELOG.md @@ -1,3 +1,7 @@ +## [3.4.6] - 2026-10-23 + +* Catches unhandled asynchronous errors when updating cache-hit metadata and during scheduled cleanup in `CacheStore` ([#535](https://github.com/Baseflow/flutter_cache_manager/issues/535)). + ## [3.4.5] - 2026-09-18 * Prevents image resize decoding from completing more than once and propagates decoding errors ([#400](https://github.com/Baseflow/flutter_cache_manager/issues/400)). diff --git a/flutter_cache_manager/lib/src/cache_store.dart b/flutter_cache_manager/lib/src/cache_store.dart index 44ffbfeb..453292c7 100644 --- a/flutter_cache_manager/lib/src/cache_store.dart +++ b/flutter_cache_manager/lib/src/cache_store.dart @@ -75,21 +75,32 @@ class CacheStore { } if (!_futureCache.containsKey(key)) { final completer = Completer(); - _getCacheDataFromDatabase(key).then((cacheObject) async { - if (cacheObject?.id != null && !await _fileExists(cacheObject)) { - final provider = await _cacheInfoRepository; - await provider.delete(cacheObject!.id!); - cacheObject = null; - } - - if (cacheObject == null) { - _memCache.remove(key); - } else { - _memCache[key] = cacheObject; - } - completer.complete(cacheObject); - _futureCache.remove(key); - }); + _getCacheDataFromDatabase(key).then( + (cacheObject) async { + try { + if (cacheObject?.id != null && !await _fileExists(cacheObject)) { + final provider = await _cacheInfoRepository; + await provider.delete(cacheObject!.id!); + cacheObject = null; + } + + if (cacheObject == null) { + _memCache.remove(key); + } else { + _memCache[key] = cacheObject; + } + completer.complete(cacheObject); + } catch (e, s) { + completer.completeError(e, s); + } finally { + _futureCache.remove(key); + } + }, + onError: (Object e, StackTrace s) { + completer.completeError(e, s); + _futureCache.remove(key); + }, + ); _futureCache[key] = completer.future; } return _futureCache[key]; @@ -121,7 +132,15 @@ class CacheStore { final provider = await _cacheInfoRepository; final data = await provider.get(key); if (await _fileExists(data)) { - _updateCacheDataInDatabase(data!); + unawaited( + _updateCacheDataInDatabase(data!).catchError((Object e) { + cacheLogger.log( + 'CacheStore: Failed to update cache metadata for $key with error:\n$e', + CacheManagerLogLevel.warning, + ); + return null; + }), + ); } _scheduleCleanup(); return data; @@ -133,7 +152,14 @@ class CacheStore { } _scheduledCleanup = Timer(cleanupRunMinInterval, () { _scheduledCleanup = null; - _cleanupCache(); + unawaited( + _cleanupCache().catchError((Object e) { + cacheLogger.log( + 'CacheStore: Failed to cleanup cache with error:\n$e', + CacheManagerLogLevel.warning, + ); + }), + ); }); } diff --git a/flutter_cache_manager/pubspec.yaml b/flutter_cache_manager/pubspec.yaml index 3f0c9ce3..8456c4b5 100644 --- a/flutter_cache_manager/pubspec.yaml +++ b/flutter_cache_manager/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_cache_manager description: Generic cache manager for flutter. Saves web files on the storages of the device and saves the cache info using sqflite. -version: 3.4.5 +version: 3.4.6 homepage: https://github.com/Baseflow/flutter_cache_manager topics: - cache diff --git a/flutter_cache_manager/test/cache_store_test.dart b/flutter_cache_manager/test/cache_store_test.dart index 412a35f1..b37d0007 100644 --- a/flutter_cache_manager/test/cache_store_test.dart +++ b/flutter_cache_manager/test/cache_store_test.dart @@ -54,6 +54,54 @@ void main() { expect(await store.getFile('baseflow.com/test.png'), isNotNull); }); + test( + 'Store should return FileInfo and handle error when database update fails on cache hit', + () async { + var config = createTestConfig(); + await config.returnsFile(fileName); + config.returnsCacheObject( + fileUrl, + fileName, + DateTime.now(), + id: fileId, + ); + when( + config.mockRepo.updateOrInsert(any), + ).thenAnswer((_) => Future.error(Exception('Database update failed'))); + + var store = CacheStore(config); + + final fileInfo = await store.getFile(fileUrl); + expect(fileInfo, isNotNull); + + // Allow any unawaited async operations to complete without unhandled errors + await Future.delayed(const Duration(milliseconds: 50)); + }, + ); + + test( + 'Store should propagate error and clean up pending cache when get from database fails', + () async { + var config = createTestConfig(); + when( + config.mockRepo.get(any), + ).thenAnswer((_) => Future.error(Exception('Database read failed'))); + + var store = CacheStore(config); + + await expectLater( + () => store.retrieveCacheData('failed-key'), + throwsA(isA()), + ); + + // Verify that subsequent calls are not permanently stuck on a dead completer + await expectLater( + () => store.retrieveCacheData('failed-key'), + throwsA(isA()), + ); + }, + ); + test('Store should return null if file is not cached', () async { var config = createTestConfig(); await config.returnsFile(fileName);