Skip to content

Commit 2033119

Browse files
[flutter_markdown] fix invalid URI's causing unhandled image errors (flutter#8058)
This PR adds an error builder for images, so that any errors from those are caught. *List which issues are fixed by this PR. You must list at least one issue.* Fixes flutter/flutter#158428
1 parent e9a6e24 commit 2033119

6 files changed

Lines changed: 148 additions & 14 deletions

File tree

packages/flutter_markdown/CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
## 0.7.4+3
2+
3+
* Passes a default error builder to image widgets.
4+
15
## 0.7.4+2
26

3-
* Fixes pub.dev detection of WebAssembly support.
7+
* Fixes pub.dev detection of WebAssembly support.
48

59
## 0.7.4+1
610

packages/flutter_markdown/lib/src/_functions_io.dart

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,62 @@ final ImageBuilder kDefaultImageBuilder = (
2424
double? height,
2525
) {
2626
if (uri.scheme == 'http' || uri.scheme == 'https') {
27-
return Image.network(uri.toString(), width: width, height: height);
27+
return Image.network(
28+
uri.toString(),
29+
width: width,
30+
height: height,
31+
errorBuilder: kDefaultImageErrorWidgetBuilder,
32+
);
2833
} else if (uri.scheme == 'data') {
2934
return _handleDataSchemeUri(uri, width, height);
3035
} else if (uri.scheme == 'resource') {
31-
return Image.asset(uri.path, width: width, height: height);
36+
return Image.asset(
37+
uri.path,
38+
width: width,
39+
height: height,
40+
errorBuilder: kDefaultImageErrorWidgetBuilder,
41+
);
3242
} else {
3343
final Uri fileUri = imageDirectory != null
3444
? Uri.parse(imageDirectory + uri.toString())
3545
: uri;
3646
if (fileUri.scheme == 'http' || fileUri.scheme == 'https') {
37-
return Image.network(fileUri.toString(), width: width, height: height);
47+
return Image.network(
48+
fileUri.toString(),
49+
width: width,
50+
height: height,
51+
errorBuilder: kDefaultImageErrorWidgetBuilder,
52+
);
3853
} else {
39-
return Image.file(File.fromUri(fileUri), width: width, height: height);
54+
try {
55+
return Image.file(
56+
File.fromUri(fileUri),
57+
width: width,
58+
height: height,
59+
errorBuilder: kDefaultImageErrorWidgetBuilder,
60+
);
61+
} catch (error, stackTrace) {
62+
// Handle any invalid file URI's.
63+
return Builder(
64+
builder: (BuildContext context) {
65+
return kDefaultImageErrorWidgetBuilder(context, error, stackTrace);
66+
},
67+
);
68+
}
4069
}
4170
}
4271
};
4372

73+
/// A default error widget builder for handling image errors.
74+
// ignore: prefer_function_declarations_over_variables
75+
final ImageErrorWidgetBuilder kDefaultImageErrorWidgetBuilder = (
76+
BuildContext context,
77+
Object error,
78+
StackTrace? stackTrace,
79+
) {
80+
return const SizedBox();
81+
};
82+
4483
/// A default style sheet generator.
4584
final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?)
4685
// ignore: prefer_function_declarations_over_variables
@@ -76,6 +115,7 @@ Widget _handleDataSchemeUri(
76115
uri.data!.contentAsBytes(),
77116
width: width,
78117
height: height,
118+
errorBuilder: kDefaultImageErrorWidgetBuilder,
79119
);
80120
} else if (mimeType.startsWith('text/')) {
81121
return Text(uri.data!.contentAsString());

packages/flutter_markdown/lib/src/_functions_web.dart

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,68 @@ final ImageBuilder kDefaultImageBuilder = (
2525
double? height,
2626
) {
2727
if (uri.scheme == 'http' || uri.scheme == 'https') {
28-
return Image.network(uri.toString(), width: width, height: height);
28+
return Image.network(
29+
uri.toString(),
30+
width: width,
31+
height: height,
32+
errorBuilder: kDefaultImageErrorWidgetBuilder,
33+
);
2934
} else if (uri.scheme == 'data') {
3035
return _handleDataSchemeUri(uri, width, height);
3136
} else if (uri.scheme == 'resource') {
32-
return Image.asset(uri.path, width: width, height: height);
37+
return Image.asset(
38+
uri.path,
39+
width: width,
40+
height: height,
41+
errorBuilder: kDefaultImageErrorWidgetBuilder,
42+
);
3343
} else {
34-
final Uri fileUri = imageDirectory != null
35-
? Uri.parse(p.join(imageDirectory, uri.toString()))
36-
: uri;
44+
final Uri fileUri;
45+
46+
if (imageDirectory != null) {
47+
try {
48+
fileUri = Uri.parse(p.join(imageDirectory, uri.toString()));
49+
} catch (error, stackTrace) {
50+
// Handle any invalid file URI's.
51+
return Builder(
52+
builder: (BuildContext context) {
53+
return kDefaultImageErrorWidgetBuilder(context, error, stackTrace);
54+
},
55+
);
56+
}
57+
} else {
58+
fileUri = uri;
59+
}
60+
3761
if (fileUri.scheme == 'http' || fileUri.scheme == 'https') {
38-
return Image.network(fileUri.toString(), width: width, height: height);
62+
return Image.network(
63+
fileUri.toString(),
64+
width: width,
65+
height: height,
66+
errorBuilder: kDefaultImageErrorWidgetBuilder,
67+
);
3968
} else {
4069
final String src = p.join(p.current, fileUri.toString());
41-
return Image.network(src, width: width, height: height);
70+
return Image.network(
71+
src,
72+
width: width,
73+
height: height,
74+
errorBuilder: kDefaultImageErrorWidgetBuilder,
75+
);
4276
}
4377
}
4478
};
4579

80+
/// A default error widget builder for handling image errors.
81+
// ignore: prefer_function_declarations_over_variables
82+
final ImageErrorWidgetBuilder kDefaultImageErrorWidgetBuilder = (
83+
BuildContext context,
84+
Object error,
85+
StackTrace? stackTrace,
86+
) {
87+
return const SizedBox();
88+
};
89+
4690
/// A default style sheet generator.
4791
final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?)
4892
// ignore: prefer_function_declarations_over_variables
@@ -72,6 +116,7 @@ Widget _handleDataSchemeUri(
72116
uri.data!.contentAsBytes(),
73117
width: width,
74118
height: height,
119+
errorBuilder: kDefaultImageErrorWidgetBuilder,
75120
);
76121
} else if (mimeType.startsWith('text/')) {
77122
return Text(uri.data!.contentAsString());

packages/flutter_markdown/lib/src/builder.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,12 @@ class MarkdownBuilder implements md.NodeVisitor {
601601
}
602602
}
603603

604-
final Uri uri = Uri.parse(path);
604+
final Uri? uri = Uri.tryParse(path);
605+
606+
if (uri == null) {
607+
return const SizedBox();
608+
}
609+
605610
Widget child;
606611
if (imageBuilder != null) {
607612
child = imageBuilder!(uri, title, alt);

packages/flutter_markdown/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: A Markdown renderer for Flutter. Create rich text output,
44
formatted with simple Markdown tags.
55
repository: https://github.com/flutter/packages/tree/main/packages/flutter_markdown
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_markdown%22
7-
version: 0.7.4+2
7+
version: 0.7.4+3
88

99
environment:
1010
sdk: ^3.3.0

packages/flutter_markdown/test/image_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,46 @@ void defineTests() {
333333
},
334334
);
335335

336+
testWidgets(
337+
'should gracefully handle image URLs with empty scheme',
338+
(WidgetTester tester) async {
339+
const String data = '![alt](://img#x50)';
340+
await tester.pumpWidget(
341+
boilerplate(
342+
const Markdown(data: data),
343+
),
344+
);
345+
346+
expect(find.byType(Image), findsNothing);
347+
expect(tester.takeException(), isNull);
348+
},
349+
);
350+
351+
testWidgets(
352+
'should gracefully handle image URLs with invalid scheme',
353+
(WidgetTester tester) async {
354+
const String data = '![alt](ttps://img#x50)';
355+
await tester.pumpWidget(
356+
boilerplate(
357+
const Markdown(data: data),
358+
),
359+
);
360+
361+
// On the web, any URI with an unrecognized scheme is treated as a network image.
362+
// Thus the error builder of the Image widget is called.
363+
// On non-web, any URI with an unrecognized scheme is treated as a file image.
364+
// However, constructing a file from an invalid URI will throw an exception.
365+
// Thus the Image widget is never created, nor is its error builder called.
366+
if (kIsWeb) {
367+
expect(find.byType(Image), findsOneWidget);
368+
} else {
369+
expect(find.byType(Image), findsNothing);
370+
}
371+
372+
expect(tester.takeException(), isNull);
373+
},
374+
);
375+
336376
testWidgets(
337377
'should gracefully handle width parsing failures',
338378
(WidgetTester tester) async {

0 commit comments

Comments
 (0)