diff --git a/e2etests/web/regular_integration_tests/lib/image_load_failure_main.dart b/e2etests/web/regular_integration_tests/lib/image_load_failure_main.dart new file mode 100644 index 0000000000000..3527b1478f2d1 --- /dev/null +++ b/e2etests/web/regular_integration_tests/lib/image_load_failure_main.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'package:flutter/material.dart'; + +const Color darkBlue = Color.fromARGB(255, 18, 32, 47); + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: MyWidget(), + ), + ); + } +} + +class MyWidget extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Material( + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset( + 'assets/images/wallpaper2.jpg', + fit: BoxFit.cover, + ), + ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withOpacity(0.8), + BlendMode.srcOut, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Container( + decoration: const BoxDecoration( + color: Colors.black, + backgroundBlendMode: BlendMode.dstOut, + ), + ), + Align( + alignment: Alignment.topCenter, + child: Container( + margin: const EdgeInsets.only(top: 80), + height: 200, + width: 200, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(100), + ), + ), + ), + const Center( + child: Text( + 'Hello World', + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.w600, + ), + ), + ) + ], + ), + ), + ], + ), + ); + } +} diff --git a/e2etests/web/regular_integration_tests/pubspec.yaml b/e2etests/web/regular_integration_tests/pubspec.yaml index edff2da9d7896..8cbb6d4a6d067 100644 --- a/e2etests/web/regular_integration_tests/pubspec.yaml +++ b/e2etests/web/regular_integration_tests/pubspec.yaml @@ -15,7 +15,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - integration_test: ^0.9.2+2 + integration_test: ^1.0.2+2 http: 0.12.0+2 web_test_utils: path: ../../../web_sdk/web_test_utils diff --git a/e2etests/web/regular_integration_tests/test_driver/image_load_failure_integration.dart b/e2etests/web/regular_integration_tests/test_driver/image_load_failure_integration.dart new file mode 100644 index 0000000000000..73b2b39cf9fc8 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/image_load_failure_integration.dart @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:js' as js; +import 'dart:js_util' as js_util; +import 'package:flutter_test/flutter_test.dart'; +import 'package:regular_integration_tests/image_load_failure_main.dart' as app; + +import 'package:integration_test/integration_test.dart'; + +/// Tests +void main() { + final IntegrationTestWidgetsFlutterBinding binding = + IntegrationTestWidgetsFlutterBinding.ensureInitialized() + as IntegrationTestWidgetsFlutterBinding; + testWidgets('Image load fails on incorrect asset', + (WidgetTester tester) async { + final StringBuffer buffer = StringBuffer(); + await runZoned(() async { + app.main(); + await tester.pumpAndSettle(); + }, zoneSpecification: ZoneSpecification( + print: (Zone self, ZoneDelegate parent, Zone zone, String line) { + buffer.writeln(line); + })); + final dynamic exception1 = tester.takeException(); + expect(exception1, isNotNull); + }); +} diff --git a/e2etests/web/regular_integration_tests/test_driver/image_load_failure_integration_test.dart b/e2etests/web/regular_integration_tests/test_driver/image_load_failure_integration_test.dart new file mode 100644 index 0000000000000..96b5ad0bf52a4 --- /dev/null +++ b/e2etests/web/regular_integration_tests/test_driver/image_load_failure_integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart' as test; + +Future main() async => test.integrationDriver(); diff --git a/lib/web_ui/lib/src/engine/assets.dart b/lib/web_ui/lib/src/engine/assets.dart index 6dbd62fdd1fa7..36bb9d2e22fa8 100644 --- a/lib/web_ui/lib/src/engine/assets.dart +++ b/lib/web_ui/lib/src/engine/assets.dart @@ -49,14 +49,34 @@ class AssetManager { return Uri.encodeFull((_baseUrl ?? '') + '$assetsDir/$asset'); } + /// Returns true if buffer contains html document. + static bool _responseIsHtmlPage(ByteData data) { + const String htmlDocTypeResponse = ''; + final int testLength = htmlDocTypeResponse.length; + if (data.lengthInBytes < testLength) { + return false; + } + for (int i = 0; i < testLength; i++) { + if (data.getInt8(i) != htmlDocTypeResponse.codeUnitAt(i)) + return false; + } + return true; + } + Future load(String asset) async { final String url = getAssetUrl(asset); try { final html.HttpRequest request = await html.HttpRequest.request(url, responseType: 'arraybuffer'); - + // Development server will return index.html for invalid urls. + // The check below makes sure when it is returned for non html assets + // we report an error instead of silent failure. final ByteBuffer response = request.response; - return response.asByteData(); + final ByteData data = response.asByteData(); + if (!url.endsWith('html') && _responseIsHtmlPage(data)) { + throw AssetManagerException(url, 404); + } + return data; } on html.ProgressEvent catch (e) { final html.EventTarget? target = e.target; if (target is html.HttpRequest) { diff --git a/lib/web_ui/lib/src/engine/html_image_codec.dart b/lib/web_ui/lib/src/engine/html_image_codec.dart index b6de4cc334887..4857808ab7e6a 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -79,6 +79,7 @@ class HtmlCodec implements ui.Codec { loadSubscription?.cancel(); errorSubscription.cancel(); completer.completeError(event); + throw ArgumentError('Unable to load image asset: $src'); }); loadSubscription = imgElement.onLoad.listen((html.Event event) { if (chunkCallback != null) { diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 378a4c8a33a55..67415c8f08168 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -315,6 +315,20 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { }; } + void _reportAssetLoadError(String url, + ui.PlatformMessageResponseCallback? callback, String error) { + const MethodCodec codec = JSONMethodCodec(); + final String message = 'Error while trying to load an asset $url'; + if (!assertionsEnabled) { + /// For web/release mode log the load failure on console. + printWarning(message); + } + _replyToPlatformMessage( + callback, codec.encodeErrorEnvelope(code: 'errorCode', + message: message, + details: error)); + } + void _sendPlatformMessage( String name, ByteData? data, @@ -336,7 +350,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { } switch (name) { - /// This should be in sync with shell/common/shell.cc case 'flutter/skia': const MethodCodec codec = JSONMethodCodec(); @@ -364,12 +377,15 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { case 'flutter/assets': final String url = utf8.decode(data!.buffer.asUint8List()); - ui.webOnlyAssetManager.load(url).then((ByteData assetData) { - _replyToPlatformMessage(callback, assetData); - }, onError: (dynamic error) { - printWarning('Error while trying to load an asset: $error'); - _replyToPlatformMessage(callback, null); - }); + ui.webOnlyAssetManager.load(url) + .then((ByteData assetData) { + _replyToPlatformMessage(callback, assetData); + }, onError: (dynamic error) { + _reportAssetLoadError(url, callback, error); + } + ).catchError((dynamic e) { + _reportAssetLoadError(url, callback, e); + }); return; case 'flutter/platform':