Avoid caching image load failures that are retriable (#51398)

This commit is contained in:
Dan Field 2020-02-25 14:56:04 -08:00 committed by GitHub
parent bc4bd7bd55
commit 7cbe55cf1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 7 deletions

View file

@ -9,6 +9,7 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'binding.dart';
import 'debug.dart';
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';
@ -86,8 +87,13 @@ class NetworkImage extends image_provider.ImageProvider<image_provider.NetworkIm
request.headers.add(name, value);
});
final HttpClientResponse response = await request.close();
if (response.statusCode != HttpStatus.ok)
if (response.statusCode != HttpStatus.ok) {
// The network may be only temporarily unavailable, or the file will be
// added on the server later. Avoid having future calls to resolve
// fail to check the network again.
PaintingBinding.instance.imageCache.evict(key);
throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);
}
final Uint8List bytes = await consolidateHttpClientResponseBytes(
response,

View file

@ -642,9 +642,19 @@ abstract class AssetBundleImageProvider extends ImageProvider<AssetBundleImageKe
/// This function is used by [load].
@protected
Future<ui.Codec> _loadAsync(AssetBundleImageKey key, DecoderCallback decode) async {
final ByteData data = await key.bundle.load(key.name);
if (data == null)
throw 'Unable to read data';
ByteData data;
// Hot reload/restart could change whether an asset bundle or key in a
// bundle are available, or if it is a network backed bundle.
try {
data = await key.bundle.load(key.name);
} on FlutterError {
PaintingBinding.instance.imageCache.evict(key);
rethrow;
}
if (data == null) {
PaintingBinding.instance.imageCache.evict(key);
throw StateError('Unable to read data');
}
return await decode(data.buffer.asUint8List());
}
}
@ -827,8 +837,12 @@ class FileImage extends ImageProvider<FileImage> {
assert(key == this);
final Uint8List bytes = await file.readAsBytes();
if (bytes.lengthInBytes == 0)
if (bytes.lengthInBytes == 0) {
// The file may become available later.
PaintingBinding.instance.imageCache.evict(key);
throw StateError('$file is empty and cannot be loaded as an image.');
}
return await decode(bytes);
}

View file

@ -10,6 +10,7 @@ import 'dart:typed_data';
import 'package:file/memory.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
@ -34,6 +35,8 @@ void main() {
tearDown(() {
FlutterError.onError = oldError;
PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
});
group('ImageProvider', () {
@ -42,6 +45,50 @@ void main() {
imageCache.clear();
});
test('AssetImageProvider - evicts on failure to load', () async {
final Completer<FlutterError> error = Completer<FlutterError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as FlutterError);
};
const ImageProvider provider = ExactAssetImage('does-not-exist');
final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true);
expect(imageCache.pendingImageCount, 1);
await error.future;
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
}, skip: isBrowser);
test('AssetImageProvider - evicts on null load', () async {
final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as StateError);
};
final ImageProvider provider = ExactAssetImage('does-not-exist', bundle: TestAssetBundle());
final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true);
expect(imageCache.pendingImageCount, 1);
await error.future;
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
}, skip: isBrowser);
test('ImageProvider can evict images', () async {
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage imageProvider = MemoryImage(bytes);
@ -168,7 +215,7 @@ void main() {
expect(uncaught, false);
});
test('File image with empty file throws expected error - (image cache)', () async {
test('File image with empty file throws expected error and evicts from cache', () async {
final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) {
error.complete(details.exception as StateError);
@ -177,9 +224,17 @@ void main() {
final File file = fs.file('/empty.png')..createSync(recursive: true);
final FileImage provider = FileImage(file);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).pending, true);
expect(imageCache.pendingImageCount, 1);
expect(await error.future, isStateError);
expect(imageCache.statusForKey(provider).untracked, true);
expect(imageCache.pendingImageCount, 0);
});
group('NetworkImage', () {
@ -194,7 +249,7 @@ void main() {
debugNetworkImageHttpClientProvider = null;
});
test('Expect thrown exception with statusCode', () async {
test('Expect thrown exception with statusCode - evicts from cache', () async {
final int errorStatusCode = HttpStatus.notFound;
const String requestUrl = 'foo-url';
@ -207,13 +262,24 @@ void main() {
final Completer<dynamic> caughtError = Completer<dynamic>();
final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1);
expect(imageCache.statusForKey(imageProvider).pending, true);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) {
caughtError.complete(error);
}));
final dynamic err = await caughtError.future;
expect(imageCache.pendingImageCount, 0);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect(
err,
isA<NetworkImageLoadException>()
@ -465,3 +531,10 @@ class AsyncKeyMemoryImage extends MemoryImage {
class MockHttpClient extends Mock implements HttpClient {}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
class TestAssetBundle extends CachingAssetBundle {
@override
Future<ByteData> load(String key) async {
return null;
}
}