Skip to content

(Will PR) When resized network image has error, all future unrelated images using the same url will fail, even if the network becomes OK #127265

@fzyzcjy

Description

@fzyzcjy

Some possible approaches

Approach 1: Add ImageCache.evictWhere((key) => ...) in addition to evict(...).

Approach 2: Let ImageCache automatically evict failed caches.

Approach 3: Add a cache key in addition to the current key.


Is there an existing issue for this?

Steps to reproduce

(I will PR for this bug)

Hi thanks for the Flutter framework! When resized network image has error, all future unrelated images using the same url will fail, even if the network becomes OK.

After digging into source code, the cause is as follows: Even if NetworkImage does call imageCache.evict(), it is using the wrong key when it is wrapped with a ResizedImage. In other words, it is imageCache.evict(instanceOfNetworkImage), while the real key is an instance of the ResizedImage.

The following reproducible sample, though seemingly quite lengthy, is indeed very simple: Copy-and-paste (and slightly simplify) the https://github.com/flutter/flutter/blob/master/packages/flutter/test/painting/image_provider_network_image_test.dart. Then, just modify one line:

    // final ImageProvider imageProvider = NetworkImage(nonconst('testing.url'));
    final ImageProvider imageProvider = ResizeImage(NetworkImage(nonconst('testing.url')), width: 5, height: 5);

i.e. Add a ResizeImage. Then the tests fail.

Run test.

Expected results

no error.

Actual results

has error.

Code sample

Code sample
import 'dart:async';
import 'dart:io';
import 'dart:ui' show Codec, FrameInfo;

import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late _FakeHttpClient httpClient;

  setUp(() {
    httpClient = _FakeHttpClient();
    debugNetworkImageHttpClientProvider = () => httpClient;
  });

  tearDown(() {
    debugNetworkImageHttpClientProvider = null;
    PaintingBinding.instance.imageCache.clear();
    PaintingBinding.instance.imageCache.clearLiveImages();
  });

  test('Expect thrown exception with statusCode - evicts from cache and drains', () async {
    const int errorStatusCode = HttpStatus.notFound;
    const String requestUrl = 'foo-url';

    httpClient.request.response.statusCode = errorStatusCode;

    final Completer<dynamic> caughtError = Completer<dynamic>();

    // final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
    final ImageProvider imageProvider = ResizeImage(NetworkImage(nonconst(requestUrl)), width: 5, height: 5);

    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>()
          .having((NetworkImageLoadException e) => e.statusCode, 'statusCode', errorStatusCode)
          .having((NetworkImageLoadException e) => e.uri, 'uri', Uri.base.resolve(requestUrl)),
    );
    expect(httpClient.request.response.drained, true);
  }, skip: isBrowser); // [intended] Browser implementation does not use HTTP client but an <img> tag.

  test('NetworkImage is evicted from cache on SocketException', () async {
    final _FakeHttpClient mockHttpClient = _FakeHttpClient();
    mockHttpClient.thrownError = const SocketException('test exception');
    debugNetworkImageHttpClientProvider = () => mockHttpClient;

    // final ImageProvider imageProvider = NetworkImage(nonconst('testing.url'));
    final ImageProvider imageProvider = ResizeImage(NetworkImage(nonconst('testing.url')), width: 5, height: 5);

    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);
    final Completer<dynamic> caughtError = Completer<dynamic>();
    result.addListener(ImageStreamListener(
      (ImageInfo info, bool syncCall) {},
      onError: (dynamic error, StackTrace? stackTrace) {
        caughtError.complete(error);
      },
    ));

    final dynamic err = await caughtError.future;

    expect(err, isA<SocketException>());

    expect(imageCache.pendingImageCount, 0);
    expect(imageCache.statusForKey(imageProvider).untracked, true);
    expect(imageCache.containsKey(result), isFalse);

    debugNetworkImageHttpClientProvider = null;
  }, skip: isBrowser); // [intended] Browser does not resolve images this way.
}

class _FakeHttpClient extends Fake implements HttpClient {
  final _FakeHttpClientRequest request = _FakeHttpClientRequest();
  Object? thrownError;

  @override
  Future<HttpClientRequest> getUrl(Uri url) async {
    if (thrownError != null) {
      throw thrownError!;
    }
    return request;
  }
}

class _FakeHttpClientRequest extends Fake implements HttpClientRequest {
  final _FakeHttpClientResponse response = _FakeHttpClientResponse();

  @override
  Future<HttpClientResponse> close() async {
    return response;
  }
}

class _FakeHttpClientResponse extends Fake implements HttpClientResponse {
  bool drained = false;

  @override
  int statusCode = HttpStatus.ok;

  @override
  int contentLength = 0;

  @override
  HttpClientResponseCompressionState get compressionState => HttpClientResponseCompressionState.notCompressed;

  late List<List<int>> content;

  @override
  StreamSubscription<List<int>> listen(void Function(List<int> event)? onData,
      {Function? onError, void Function()? onDone, bool? cancelOnError}) {
    return Stream<List<int>>.fromIterable(content).listen(
      onData,
      onDone: onDone,
      onError: onError,
      cancelOnError: cancelOnError,
    );
  }

  @override
  Future<E> drain<E>([E? futureValue]) async {
    drained = true;
    return futureValue ?? futureValue as E; // Mirrors the implementation in Stream.
  }
}

class FakeCodec implements Codec {
  @override
  void dispose() {}

  @override
  int get frameCount => throw UnimplementedError();

  @override
  Future<FrameInfo> getNextFrame() {
    throw UnimplementedError();
  }

  @override
  int get repetitionCount => throw UnimplementedError();
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
[Paste your logs here]

Flutter Doctor output

Doctor output
[Paste your output here]

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work lista: imagesLoading, displaying, rendering imagesfound in release: 3.10Found to occur in 3.10found in release: 3.11Found to occur in 3.11frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onr: fixedIssue is closed as already fixed in a newer versionteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions