Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[web] HttpGet: read/decode failures are not reported to the image stream (placeholder forever, pending ImageCache entry)

Aperta
#1,060 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
45/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
dart, flutter
Ambito
frontend

Direzione di ricerca

Start in cached_network_image_web/lib/cached_network_image_web.dart, in _loadAsyncHttpGet, and review how file reads, decoding, stream errors, and controller closure are handled. The reproduction test in the issue shows the expected behavior: read or decode failures reach the image stream, evict the pending image, close both controllers, and leave no uncaught error; removing the broken cache entry is described as ideal.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

🐛 Bug Report

On web with ImageRenderMethodForWeb.HttpGet, a failure while reading the cached file or decoding its bytes is never reported to the image stream.

In cached_network_image_web/lib/cached_network_image_web.dart, _loadAsyncHttpGet handles a FileInfo like this:

event.file.readAsBytes().then((value) => decode(value)).then((data) {
  streamController.add(data);
  ...
});

Neither future has an error handler. If the server returns 200 with bytes that are not a valid image (or the read fails), the exception goes to the zone as an uncaught error. The onError of the getFileStream subscription only covers errors from the download stream, so:

  • streamController never gets the error, so errorWidget / errorListener are not called and the placeholder stays forever
  • evictImage is not called, so the pending entry stays in ImageCache; showing the same URL again reuses the pending completer and never retries
  • the broken file stays in the cache manager, so a later load reads the same bytes again
  • streamController and chunkEvents are never closed

With HtmlImage the same broken response is reported as an error, so switching to HttpGet changes how a bad image fails.

A related detail in the same function: when the cached FileInfo finishes decoding before the download stream calls onDone, the state goes waitingForData → closing after the decode has already completed, and nothing closes streamController / chunkEvents.

Expected behavior

A read or decode failure for a FileInfo is delivered to the returned stream as an error (so MultiImageStreamCompleter reports it), evictImage is called, and both controllers are closed. Ideally the broken cache entry is removed too, so the next attempt downloads again.

Reproduction steps

This VM test feeds the web ImageLoader a FileInfo with non-image bytes and a decoder that throws, as the real decoder does for such bytes. It fails on cached_network_image_web 2.0.3: the output is uncaught=[Bad state: invalid image bytes] streamErrors=[] evicted=0 done=false.

import 'dart:async';
import 'dart:ui' as ui;

import 'package:cached_network_image_platform_interface/cached_network_image_platform_interface.dart'
    show ImageRenderMethodForWeb;
import 'package:cached_network_image_web/cached_network_image_web.dart';
import 'package:file/memory.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_test/flutter_test.dart';

class FakeCacheManager implements BaseCacheManager {
  final events = StreamController<FileResponse>();
  @override
  Stream<FileResponse> getFileStream(String url,
          {String? key, Map<String, String>? headers, bool withProgress = false}) =>
      events.stream;
  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  test('HttpGet: a decode failure should reach the image stream', () async {
    final uncaught = <Object>[];
    final streamErrors = <Object>[];
    var evicted = 0;
    var done = false;

    await runZonedGuarded(() async {
      final cache = FakeCacheManager();
      final file = MemoryFileSystem().file('/broken.webp')
        ..writeAsBytesSync([0, 1, 2, 3]); // not an image
      final stream = ImageLoader().loadImageAsync(
        'https://example.com/broken.webp',
        null,
        StreamController<ImageChunkEvent>(),
        (buffer, {getTargetSize}) async {
          buffer.dispose();
          throw StateError('invalid image bytes'); // what a real decoder does
        },
        cache,
        null,
        null,
        null,
        ImageRenderMethodForWeb.HttpGet,
        () => evicted++,
      );
      stream.listen((_) {}, onError: streamErrors.add, onDone: () => done = true);
      cache.events.add(FileInfo(file, FileSource.Online,
          DateTime.now().add(const Duration(days: 1)), 'https://example.com/broken.webp'));
      await cache.events.close();
      await Future<void>.delayed(const Duration(milliseconds: 100));
    }, (e, _) => uncaught.add(e));

    print('uncaught=$uncaught streamErrors=$streamErrors evicted=$evicted done=$done');
    expect(streamErrors, isNotEmpty);
    expect(evicted, 1);
    expect(done, isTrue);
    expect(uncaught, isEmpty);
  });
}

In an app, the visible symptom is a CachedNetworkImage that keeps showing its placeholder for a URL whose response body is not a valid image, and that does not recover when the widget is rebuilt or the screen is reopened (until the page is reloaded).

What we did locally

We vendored 2.0.3 and changed only _loadAsyncHttpGet: the read and decode are awaited inside try/catch; on failure it schedules evictImage, calls cacheManager.removeFile(cacheKey ?? url) (ignoring a failure there so it does not hide the original error), adds the original error and stack trace to streamController, and closes streamController and chunkEvents. It also counts in-flight decodes so that overlapping cached/refreshed FileInfos are both delivered before closing, and closes in onDone when every decode has already finished. Happy to open a PR with this change and tests if that would help.

Configuration

Version: cached_network_image 4.0.4, cached_network_image_web 2.0.3, cached_network_image_platform_interface 5.0.3 (the same code is on main today)

Flutter: 3.47.7 stable

Platform:

  • 🌐 Web (CanvasKit, ImageRenderMethodForWeb.HttpGet)
  • 📱 iOS
  • 🤖 Android
Lingua principale
Dart
Stelle
2.6k
Fork
738
Merge medio
11h 55m
PR unite (30g)
14

Preparare l'ambiente

  • Nessun Dockerfile né file Docker Compose
  • Ha un modello di pull request
  • Nessuna guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di Baseflow/flutter_cached_network_image

Tutte le issue di Baseflow/flutter_cached_network_image

Issue simili

Altre issue su Dart

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.