Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions flutter_cache_manager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)).
Expand Down
60 changes: 43 additions & 17 deletions flutter_cache_manager/lib/src/cache_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,32 @@ class CacheStore {
}
if (!_futureCache.containsKey(key)) {
final completer = Completer<CacheObject?>();
_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];
Expand Down Expand Up @@ -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;
Expand All @@ -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,
);
}),
);
});
}

Expand Down
2 changes: 1 addition & 1 deletion flutter_cache_manager/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
48 changes: 48 additions & 0 deletions flutter_cache_manager/test/cache_store_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>.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<Exception>()),
);

// Verify that subsequent calls are not permanently stuck on a dead completer
await expectLater(
() => store.retrieveCacheData('failed-key'),
throwsA(isA<Exception>()),
);
},
);

test('Store should return null if file is not cached', () async {
var config = createTestConfig();
await config.returnsFile(fileName);
Expand Down