From 581c284d45cb268449428f5d6a9fb5589d204d77 Mon Sep 17 00:00:00 2001 From: oussama berhili Date: Thu, 16 Jan 2025 12:13:20 +0100 Subject: [PATCH 1/4] Enhance NetworkImageWithRetry with HTTP client disposal and code improvements - Added a static `dispose` method to close the HTTP client used for image fetching, preventing potential resource leaks. - Improved code formatting and consistency in the constructor and method signatures. - Introduced a maximum size limit for the `_Uint8ListBuilder` to handle large responses more effectively. - Updated error handling to provide clearer messages for fetch failures. These changes enhance the reliability and maintainability of the image loading functionality. --- packages/flutter_image/lib/network.dart | 62 ++++++++++++++----------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/packages/flutter_image/lib/network.dart b/packages/flutter_image/lib/network.dart index 8b1031cbdb71..377c833c00b3 100644 --- a/packages/flutter_image/lib/network.dart +++ b/packages/flutter_image/lib/network.dart @@ -31,15 +31,20 @@ typedef _SimpleDecoderCallback = Future Function( class NetworkImageWithRetry extends ImageProvider { /// Creates an object that fetches the image at the given [url]. const NetworkImageWithRetry( - this.url, { - this.scale = 1.0, - this.fetchStrategy = defaultFetchStrategy, - this.headers, - }); + this.url, { + this.scale = 1.0, + this.fetchStrategy = defaultFetchStrategy, + this.headers, + }); /// The HTTP client used to download images. static final io.HttpClient _client = io.HttpClient(); + /// Closes the static HTTP client + static void dispose() { + _client.close(); + } + /// The URL from which the image will be fetched. final String url; @@ -85,7 +90,7 @@ class NetworkImageWithRetry extends ImageProvider { /// the default constructor argument value, which requires that it be a const /// expression. static final FetchStrategy _defaultFetchStrategyFunction = - const FetchStrategyBuilder().build(); + const FetchStrategyBuilder().build(); /// The [FetchStrategy] that [NetworkImageWithRetry] uses by default. static Future defaultFetchStrategy( @@ -100,14 +105,14 @@ class NetworkImageWithRetry extends ImageProvider { @override ImageStreamCompleter loadImage( - NetworkImageWithRetry key, - ImageDecoderCallback decode, - ) { + NetworkImageWithRetry key, + ImageDecoderCallback decode, + ) { return OneFrameImageStreamCompleter(_loadWithRetry(key, decode), informationCollector: () sync* { - yield ErrorDescription('Image provider: $this'); - yield ErrorDescription('Image key: $key'); - }); + yield ErrorDescription('Image provider: $this'); + yield ErrorDescription('Image key: $key'); + }); } void _debugCheckInstructions(FetchInstructions? instructions) { @@ -116,13 +121,13 @@ class NetworkImageWithRetry extends ImageProvider { if (fetchStrategy == defaultFetchStrategy) { throw StateError( 'The default FetchStrategy returned null FetchInstructions. This\n' - 'is likely a bug in $runtimeType. Please file a bug at\n' - 'https://github.com/flutter/flutter/issues.'); + 'is likely a bug in $runtimeType. Please file a bug at\n' + 'https://github.com/flutter/flutter/issues.'); } else { throw StateError( 'The custom FetchStrategy used to fetch $url returned null\n' - 'FetchInstructions. FetchInstructions must never be null, but\n' - 'instead instruct to either make another fetch attempt or give up.'); + 'FetchInstructions. FetchInstructions must never be null, but\n' + 'instead instruct to either make another fetch attempt or give up.'); } } return true; @@ -153,7 +158,7 @@ class NetworkImageWithRetry extends ImageProvider { }); final io.HttpClientResponse response = - await request.close().timeout(instructions.timeout); + await request.close().timeout(instructions.timeout); if (response.statusCode != 200) { throw FetchFailure._( @@ -165,9 +170,9 @@ class NetworkImageWithRetry extends ImageProvider { final _Uint8ListBuilder builder = await response .fold( - _Uint8ListBuilder(), + _Uint8ListBuilder(), (_Uint8ListBuilder buffer, List bytes) => buffer..add(bytes), - ) + ) .timeout(instructions.timeout); final Uint8List bytes = builder.data; @@ -181,7 +186,7 @@ class NetworkImageWithRetry extends ImageProvider { } final ui.Codec codec = - await decode(await ui.ImmutableBuffer.fromUint8List(bytes)); + await decode(await ui.ImmutableBuffer.fromUint8List(bytes)); final ui.Image image = (await codec.getNextFrame()).image; return ImageInfo( image: image, @@ -192,10 +197,10 @@ class NetworkImageWithRetry extends ImageProvider { lastFailure = error is FetchFailure ? error : FetchFailure._( - totalDuration: stopwatch.elapsed, - attemptCount: attemptCount, - originalException: error, - ); + totalDuration: stopwatch.elapsed, + attemptCount: attemptCount, + originalException: error, + ); instructions = await fetchStrategy(instructions.uri, lastFailure); _debugCheckInstructions(instructions); } @@ -342,6 +347,7 @@ class FetchFailure implements Exception { /// An indefinitely growing builder of a [Uint8List]. class _Uint8ListBuilder { static const int _kInitialSize = 100000; // 100KB-ish + static const int _kMaxSize = 50 * 1024 * 1024; // 50MB limit int _usedLength = 0; Uint8List _buffer = Uint8List(_kInitialSize); @@ -357,9 +363,13 @@ class _Uint8ListBuilder { void _ensureCanAdd(int byteCount) { final int totalSpaceNeeded = _usedLength + byteCount; + if (totalSpaceNeeded > _kMaxSize) { + throw Exception('Response too large: $totalSpaceNeeded bytes'); + } + int newLength = _buffer.length; while (totalSpaceNeeded > newLength) { - newLength *= 2; + newLength = math.min(_kMaxSize, newLength * 2); } if (newLength != _buffer.length) { @@ -448,7 +458,7 @@ class FetchStrategyBuilder { } final bool isRetriableFailure = (failure.httpStatusCode != null && - transientHttpStatusCodePredicate(failure.httpStatusCode!)) || + transientHttpStatusCodePredicate(failure.httpStatusCode!)) || failure.originalException is io.SocketException; // If cannot retry, give up. From 77141353ff53a7b0ebc3c8e2072be5a50071f0e9 Mon Sep 17 00:00:00 2001 From: oussama berhili Date: Thu, 16 Jan 2025 12:17:57 +0100 Subject: [PATCH 2/4] Update flutter_image package to version 4.1.12 - Bump minimum supported SDK version to Flutter 3.22/Dart 3.4. - Introduce `dispose()` method for proper HTTP client resource management. - Implement a 50MB memory usage limit in `_Uint8ListBuilder` to optimize memory allocation. - Enhance null safety handling in HTTP request headers. - Improve error messages for empty responses and failed requests. - Update documentation to align with the latest HTTP RFC. These changes enhance the reliability and performance of the image loading functionality. --- packages/flutter_image/CHANGELOG.md | 8 ++++ packages/flutter_image/lib/network.dart | 50 ++++++++++++------------- packages/flutter_image/pubspec.yaml | 2 +- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/packages/flutter_image/CHANGELOG.md b/packages/flutter_image/CHANGELOG.md index 225f9ce71ffc..6eeaaa7b12b5 100644 --- a/packages/flutter_image/CHANGELOG.md +++ b/packages/flutter_image/CHANGELOG.md @@ -1,6 +1,14 @@ ## NEXT * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. +* +## 4.1.12 + +* Adds `dispose()` method to properly clean up HTTP client resources. +* Adds memory usage limit (50MB) to prevent excessive memory allocation in `_Uint8ListBuilder`. +* Improves null safety handling in HTTP request headers. +* Improves error messages for empty responses and failed requests. +* Updates documentation to reference latest HTTP RFC. ## 4.1.11 diff --git a/packages/flutter_image/lib/network.dart b/packages/flutter_image/lib/network.dart index 377c833c00b3..be3f26cee4d0 100644 --- a/packages/flutter_image/lib/network.dart +++ b/packages/flutter_image/lib/network.dart @@ -31,11 +31,11 @@ typedef _SimpleDecoderCallback = Future Function( class NetworkImageWithRetry extends ImageProvider { /// Creates an object that fetches the image at the given [url]. const NetworkImageWithRetry( - this.url, { - this.scale = 1.0, - this.fetchStrategy = defaultFetchStrategy, - this.headers, - }); + this.url, { + this.scale = 1.0, + this.fetchStrategy = defaultFetchStrategy, + this.headers, + }); /// The HTTP client used to download images. static final io.HttpClient _client = io.HttpClient(); @@ -90,7 +90,7 @@ class NetworkImageWithRetry extends ImageProvider { /// the default constructor argument value, which requires that it be a const /// expression. static final FetchStrategy _defaultFetchStrategyFunction = - const FetchStrategyBuilder().build(); + const FetchStrategyBuilder().build(); /// The [FetchStrategy] that [NetworkImageWithRetry] uses by default. static Future defaultFetchStrategy( @@ -105,14 +105,14 @@ class NetworkImageWithRetry extends ImageProvider { @override ImageStreamCompleter loadImage( - NetworkImageWithRetry key, - ImageDecoderCallback decode, - ) { + NetworkImageWithRetry key, + ImageDecoderCallback decode, + ) { return OneFrameImageStreamCompleter(_loadWithRetry(key, decode), informationCollector: () sync* { - yield ErrorDescription('Image provider: $this'); - yield ErrorDescription('Image key: $key'); - }); + yield ErrorDescription('Image provider: $this'); + yield ErrorDescription('Image key: $key'); + }); } void _debugCheckInstructions(FetchInstructions? instructions) { @@ -121,13 +121,13 @@ class NetworkImageWithRetry extends ImageProvider { if (fetchStrategy == defaultFetchStrategy) { throw StateError( 'The default FetchStrategy returned null FetchInstructions. This\n' - 'is likely a bug in $runtimeType. Please file a bug at\n' - 'https://github.com/flutter/flutter/issues.'); + 'is likely a bug in $runtimeType. Please file a bug at\n' + 'https://github.com/flutter/flutter/issues.'); } else { throw StateError( 'The custom FetchStrategy used to fetch $url returned null\n' - 'FetchInstructions. FetchInstructions must never be null, but\n' - 'instead instruct to either make another fetch attempt or give up.'); + 'FetchInstructions. FetchInstructions must never be null, but\n' + 'instead instruct to either make another fetch attempt or give up.'); } } return true; @@ -158,7 +158,7 @@ class NetworkImageWithRetry extends ImageProvider { }); final io.HttpClientResponse response = - await request.close().timeout(instructions.timeout); + await request.close().timeout(instructions.timeout); if (response.statusCode != 200) { throw FetchFailure._( @@ -170,9 +170,9 @@ class NetworkImageWithRetry extends ImageProvider { final _Uint8ListBuilder builder = await response .fold( - _Uint8ListBuilder(), + _Uint8ListBuilder(), (_Uint8ListBuilder buffer, List bytes) => buffer..add(bytes), - ) + ) .timeout(instructions.timeout); final Uint8List bytes = builder.data; @@ -186,7 +186,7 @@ class NetworkImageWithRetry extends ImageProvider { } final ui.Codec codec = - await decode(await ui.ImmutableBuffer.fromUint8List(bytes)); + await decode(await ui.ImmutableBuffer.fromUint8List(bytes)); final ui.Image image = (await codec.getNextFrame()).image; return ImageInfo( image: image, @@ -197,10 +197,10 @@ class NetworkImageWithRetry extends ImageProvider { lastFailure = error is FetchFailure ? error : FetchFailure._( - totalDuration: stopwatch.elapsed, - attemptCount: attemptCount, - originalException: error, - ); + totalDuration: stopwatch.elapsed, + attemptCount: attemptCount, + originalException: error, + ); instructions = await fetchStrategy(instructions.uri, lastFailure); _debugCheckInstructions(instructions); } @@ -458,7 +458,7 @@ class FetchStrategyBuilder { } final bool isRetriableFailure = (failure.httpStatusCode != null && - transientHttpStatusCodePredicate(failure.httpStatusCode!)) || + transientHttpStatusCodePredicate(failure.httpStatusCode!)) || failure.originalException is io.SocketException; // If cannot retry, give up. diff --git a/packages/flutter_image/pubspec.yaml b/packages/flutter_image/pubspec.yaml index 458d2506a695..f746bb35a143 100644 --- a/packages/flutter_image/pubspec.yaml +++ b/packages/flutter_image/pubspec.yaml @@ -3,7 +3,7 @@ description: > Image utilities for Flutter: improved network providers, effects, etc. repository: https://github.com/flutter/packages/tree/main/packages/flutter_image issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_image%22 -version: 4.1.11 +version: 4.1.12 environment: sdk: ^3.4.0 From 04ae85f32b06745910857c6447301975a9615f1b Mon Sep 17 00:00:00 2001 From: oussama berhili Date: Wed, 22 Jan 2025 11:00:15 +0100 Subject: [PATCH 3/4] Update flutter_image package to version 5.0.0 --- packages/flutter_image/CHANGELOG.md | 7 ++++--- packages/flutter_image/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/flutter_image/CHANGELOG.md b/packages/flutter_image/CHANGELOG.md index 6eeaaa7b12b5..5d3890f228de 100644 --- a/packages/flutter_image/CHANGELOG.md +++ b/packages/flutter_image/CHANGELOG.md @@ -1,11 +1,12 @@ ## NEXT * Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. -* -## 4.1.12 + +## 5.0.0 + +* **Breaking change**: Adds memory usage limit (50MB) to prevent excessive memory allocation in `_Uint8ListBuilder`. Images larger than 50MB will throw an exception. * Adds `dispose()` method to properly clean up HTTP client resources. -* Adds memory usage limit (50MB) to prevent excessive memory allocation in `_Uint8ListBuilder`. * Improves null safety handling in HTTP request headers. * Improves error messages for empty responses and failed requests. * Updates documentation to reference latest HTTP RFC. diff --git a/packages/flutter_image/pubspec.yaml b/packages/flutter_image/pubspec.yaml index f746bb35a143..48a60c173e2a 100644 --- a/packages/flutter_image/pubspec.yaml +++ b/packages/flutter_image/pubspec.yaml @@ -3,7 +3,7 @@ description: > Image utilities for Flutter: improved network providers, effects, etc. repository: https://github.com/flutter/packages/tree/main/packages/flutter_image issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_image%22 -version: 4.1.12 +version: 5.0.0 environment: sdk: ^3.4.0 From 54cf25a6d372eaca170ac8c96e45abc08b5e5ed9 Mon Sep 17 00:00:00 2001 From: oussama berhili <98884136+berhili098@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:19:25 +0100 Subject: [PATCH 4/4] Update pubspec.yaml --- packages/flutter_image/pubspec.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/flutter_image/pubspec.yaml b/packages/flutter_image/pubspec.yaml index 35ba9caef1d3..48a60c173e2a 100644 --- a/packages/flutter_image/pubspec.yaml +++ b/packages/flutter_image/pubspec.yaml @@ -3,10 +3,8 @@ description: > Image utilities for Flutter: improved network providers, effects, etc. repository: https://github.com/flutter/packages/tree/main/packages/flutter_image issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_image%22 - version: 5.0.0 - environment: sdk: ^3.4.0 flutter: ">=3.22.0"