fix(media): parse responses before caching

This commit is contained in:
edde746
2026-07-25 17:38:09 +02:00
parent 516bd69c19
commit 12b826faff
3 changed files with 52 additions and 3 deletions
+4 -2
View File
@@ -750,10 +750,11 @@ mixin MediaServerCacheMixin implements MediaServerClient {
try {
final response = await networkCall();
throwIfHttpError(response);
final parsed = parseResponse(response);
if (cacheResponse) {
await _putCacheResponse(cacheScope, cacheKey, response.data);
}
return parseResponse(response);
return parsed;
} catch (e) {
if (shouldFallback != null && !shouldFallback(e)) rethrow;
appLogger.w('Network request failed for $cacheKey, trying cache', error: e);
@@ -784,10 +785,11 @@ mixin MediaServerCacheMixin implements MediaServerClient {
if (isOfflineMode) return null;
final response = await networkCall();
throwIfHttpError(response);
final parsed = parseResponse(response);
if (cacheResponse) {
await _putCacheResponse(cacheScope, cacheKey, response.data);
}
return parseResponse(response);
return parsed;
}
Future<void> _putCacheResponse(ServerId cacheScope, String cacheKey, dynamic data) async {
@@ -111,6 +111,25 @@ void main() {
expect(await cache.get(client.serverId, '/metadata/no-cache'), isNull);
});
test('cache-first leaves a rejected response uncached', () async {
const key = '/metadata/rejected-response';
const body = {'value': 'invalid'};
final parseError = FormatException('invalid metadata');
await expectLater(
client.fetchWithCacheFirst<String>(
cacheScope: client.serverId,
cacheKey: key,
networkCall: () async => MediaServerResponse(statusCode: 200, data: body, headers: const {}),
parseCache: (_) => 'cached',
parseResponse: (_) => throw parseError,
),
throwsA(same(parseError)),
);
expect(await cache.get(client.serverId, key), isNull);
});
test('successful miss is parsed and cached exactly once', () async {
var networkCalls = 0;
var parserCalls = 0;
@@ -214,6 +233,27 @@ void main() {
expect(responseParserCalls, 0);
});
test('omitted selector serves the previous cache after response parsing fails', () async {
const key = '/metadata/parser-fallback';
await cache.put(client.serverId, key, cachedBody);
var cacheParserCalls = 0;
final value = await client.fetchWithCacheFallback<String>(
cacheKey: key,
networkCall: () async =>
MediaServerResponse(statusCode: 200, data: const {'value': 'invalid'}, headers: const {}),
parseCache: (cached) {
cacheParserCalls++;
return (cached as Map<String, dynamic>)['value'] as String;
},
parseResponse: (_) => throw const FormatException('invalid metadata'),
);
expect(value, 'cached');
expect(cacheParserCalls, 1);
expect(await cache.get(client.serverId, key), cachedBody);
});
test('rejecting selector rethrows an HTTP status without reading cached data', () async {
const key = '/metadata/rejected-fallback';
await cache.put(client.serverId, key, cachedBody);
@@ -219,7 +219,7 @@ void main() {
test('${api.name} propagates a valid JSON payload rejected by the item DTO', () async {
final id = '${api.name}-cached-unmappable';
await seedItem(id);
final seededResponse = await seedItem(id);
var requestCount = 0;
final client = makeClient((_) async {
requestCount++;
@@ -236,6 +236,13 @@ void main() {
await expectLater(lookup(client, id, includeOnDeck: api.includeOnDeck), throwsA(isA<TypeError>()));
expect(requestCount, 1);
expect(await PlexApiCache.instance.get(defaultProfileScopeId.cacheServerId, endpointFor(id)), seededResponse);
client.setOfflineMode(true);
final offlineResult = await lookup(client, id, includeOnDeck: api.includeOnDeck);
expect(offlineResult.item?.id, id);
expect(offlineResult.item?.title, 'Cached item');
expect(requestCount, 1);
});
test('${api.name} uses profile-scoped cache without HTTP while explicitly offline', () async {