Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 304ee81

Browse files
authored
[web] Catch image load failures in --release builds (#25602)
1 parent 507828e commit 304ee81

File tree

7 files changed

+163
-10
lines changed

7 files changed

+163
-10
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
import 'package:flutter/material.dart';
5+
6+
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
7+
8+
void main() {
9+
runApp(MyApp());
10+
}
11+
12+
class MyApp extends StatelessWidget {
13+
@override
14+
Widget build(BuildContext context) {
15+
return MaterialApp(
16+
debugShowCheckedModeBanner: false,
17+
home: Scaffold(
18+
body: MyWidget(),
19+
),
20+
);
21+
}
22+
}
23+
24+
class MyWidget extends StatelessWidget {
25+
@override
26+
Widget build(BuildContext context) {
27+
return Material(
28+
child: Stack(
29+
fit: StackFit.expand,
30+
children: [
31+
Image.asset(
32+
'assets/images/wallpaper2.jpg',
33+
fit: BoxFit.cover,
34+
),
35+
ColorFiltered(
36+
colorFilter: ColorFilter.mode(
37+
Colors.black.withOpacity(0.8),
38+
BlendMode.srcOut,
39+
),
40+
child: Stack(
41+
fit: StackFit.expand,
42+
children: [
43+
Container(
44+
decoration: const BoxDecoration(
45+
color: Colors.black,
46+
backgroundBlendMode: BlendMode.dstOut,
47+
),
48+
),
49+
Align(
50+
alignment: Alignment.topCenter,
51+
child: Container(
52+
margin: const EdgeInsets.only(top: 80),
53+
height: 200,
54+
width: 200,
55+
decoration: BoxDecoration(
56+
color: Colors.red,
57+
borderRadius: BorderRadius.circular(100),
58+
),
59+
),
60+
),
61+
const Center(
62+
child: Text(
63+
'Hello World',
64+
style: TextStyle(
65+
fontSize: 40,
66+
fontWeight: FontWeight.w600,
67+
),
68+
),
69+
)
70+
],
71+
),
72+
),
73+
],
74+
),
75+
);
76+
}
77+
}

e2etests/web/regular_integration_tests/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dev_dependencies:
1515
sdk: flutter
1616
flutter_test:
1717
sdk: flutter
18-
integration_test: ^0.9.2+2
18+
integration_test: ^1.0.2+2
1919
http: 0.12.0+2
2020
web_test_utils:
2121
path: ../../../web_sdk/web_test_utils
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:html' as html;
7+
import 'dart:js' as js;
8+
import 'dart:js_util' as js_util;
9+
import 'package:flutter_test/flutter_test.dart';
10+
import 'package:regular_integration_tests/image_load_failure_main.dart' as app;
11+
12+
import 'package:integration_test/integration_test.dart';
13+
14+
/// Tests
15+
void main() {
16+
final IntegrationTestWidgetsFlutterBinding binding =
17+
IntegrationTestWidgetsFlutterBinding.ensureInitialized()
18+
as IntegrationTestWidgetsFlutterBinding;
19+
testWidgets('Image load fails on incorrect asset',
20+
(WidgetTester tester) async {
21+
final StringBuffer buffer = StringBuffer();
22+
await runZoned(() async {
23+
app.main();
24+
await tester.pumpAndSettle();
25+
}, zoneSpecification: ZoneSpecification(
26+
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
27+
buffer.writeln(line);
28+
}));
29+
final dynamic exception1 = tester.takeException();
30+
expect(exception1, isNotNull);
31+
});
32+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:integration_test/integration_test_driver.dart' as test;
6+
7+
Future<void> main() async => test.integrationDriver();

lib/web_ui/lib/src/engine/assets.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,34 @@ class AssetManager {
4848
return Uri.encodeFull((_baseUrl ?? '') + '$assetsDir/$asset');
4949
}
5050

51+
/// Returns true if buffer contains html document.
52+
static bool _responseIsHtmlPage(ByteData data) {
53+
const String htmlDocTypeResponse = '<!DOCTYPE html>';
54+
final int testLength = htmlDocTypeResponse.length;
55+
if (data.lengthInBytes < testLength) {
56+
return false;
57+
}
58+
for (int i = 0; i < testLength; i++) {
59+
if (data.getInt8(i) != htmlDocTypeResponse.codeUnitAt(i))
60+
return false;
61+
}
62+
return true;
63+
}
64+
5165
Future<ByteData> load(String asset) async {
5266
final String url = getAssetUrl(asset);
5367
try {
5468
final html.HttpRequest request =
5569
await html.HttpRequest.request(url, responseType: 'arraybuffer');
56-
70+
// Development server will return index.html for invalid urls.
71+
// The check below makes sure when it is returned for non html assets
72+
// we report an error instead of silent failure.
5773
final ByteBuffer response = request.response;
58-
return response.asByteData();
74+
final ByteData data = response.asByteData();
75+
if (!url.endsWith('html') && _responseIsHtmlPage(data)) {
76+
throw AssetManagerException(url, 404);
77+
}
78+
return data;
5979
} on html.ProgressEvent catch (e) {
6080
final html.EventTarget? target = e.target;
6181
if (target is html.HttpRequest) {

lib/web_ui/lib/src/engine/html_image_codec.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class HtmlCodec implements ui.Codec {
8686
loadSubscription?.cancel();
8787
errorSubscription.cancel();
8888
completer.completeError(event);
89+
throw ArgumentError('Unable to load image asset: $src');
8990
});
9091
loadSubscription = imgElement.onLoad.listen((html.Event event) {
9192
if (chunkCallback != null) {

lib/web_ui/lib/src/engine/platform_dispatcher.dart

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,20 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
314314
};
315315
}
316316

317+
void _reportAssetLoadError(String url,
318+
ui.PlatformMessageResponseCallback? callback, String error) {
319+
const MethodCodec codec = JSONMethodCodec();
320+
final String message = 'Error while trying to load an asset $url';
321+
if (!assertionsEnabled) {
322+
/// For web/release mode log the load failure on console.
323+
printWarning(message);
324+
}
325+
_replyToPlatformMessage(
326+
callback, codec.encodeErrorEnvelope(code: 'errorCode',
327+
message: message,
328+
details: error));
329+
}
330+
317331
void _sendPlatformMessage(
318332
String name,
319333
ByteData? data,
@@ -335,7 +349,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
335349
}
336350

337351
switch (name) {
338-
339352
/// This should be in sync with shell/common/shell.cc
340353
case 'flutter/skia':
341354
const MethodCodec codec = JSONMethodCodec();
@@ -363,12 +376,15 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
363376

364377
case 'flutter/assets':
365378
final String url = utf8.decode(data!.buffer.asUint8List());
366-
ui.webOnlyAssetManager.load(url).then((ByteData assetData) {
367-
_replyToPlatformMessage(callback, assetData);
368-
}, onError: (dynamic error) {
369-
printWarning('Error while trying to load an asset: $error');
370-
_replyToPlatformMessage(callback, null);
371-
});
379+
ui.webOnlyAssetManager.load(url)
380+
.then((ByteData assetData) {
381+
_replyToPlatformMessage(callback, assetData);
382+
}, onError: (dynamic error) {
383+
_reportAssetLoadError(url, callback, error);
384+
}
385+
).catchError((dynamic e) {
386+
_reportAssetLoadError(url, callback, e);
387+
});
372388
return;
373389

374390
case 'flutter/platform':

0 commit comments

Comments
 (0)