Skip to content

Commit 31ca4db

Browse files
Flutter web add support for NetworkImage headers (#85954)
1 parent 0e2f51d commit 31ca4db

File tree

4 files changed

+314
-10
lines changed

4 files changed

+314
-10
lines changed

packages/flutter/lib/src/painting/_network_image_web.dart

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,32 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
65
import 'dart:async';
6+
import 'dart:html' as html;
7+
import 'dart:typed_data';
78
import 'dart:ui' as ui;
89

910
import 'package:flutter/foundation.dart';
1011

1112
import 'image_provider.dart' as image_provider;
1213
import 'image_stream.dart';
1314

15+
/// Creates a type for an overridable factory function for testing purposes.
16+
typedef HttpRequestFactory = html.HttpRequest Function();
17+
18+
/// Default HTTP client.
19+
html.HttpRequest _httpClient() {
20+
return html.HttpRequest();
21+
}
22+
23+
/// Creates an overridable factory function.
24+
HttpRequestFactory httpRequestFactory = _httpClient;
25+
26+
/// Restores to the default HTTP request factory.
27+
void debugRestoreHttpRequestFactory() {
28+
httpRequestFactory = _httpClient;
29+
}
30+
1431
/// The dart:html implementation of [image_provider.NetworkImage].
1532
///
1633
/// NetworkImage on the web does not support decoding to a specified size.
@@ -78,18 +95,67 @@ class NetworkImage
7895
NetworkImage key,
7996
image_provider.DecoderCallback decode,
8097
StreamController<ImageChunkEvent> chunkEvents,
81-
) {
98+
) async {
8299
assert(key == this);
83100

84101
final Uri resolved = Uri.base.resolve(key.url);
85-
// This API only exists in the web engine implementation and is not
86-
// contained in the analyzer summary for Flutter.
87-
return ui.webOnlyInstantiateImageCodecFromUrl(// ignore: undefined_function, avoid_dynamic_calls
88-
resolved,
89-
chunkCallback: (int bytes, int total) {
90-
chunkEvents.add(ImageChunkEvent(cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
91-
},
92-
) as Future<ui.Codec>;
102+
103+
// We use a different method when headers are set because the
104+
// `ui.webOnlyInstantiateImageCodecFromUrl` method is not capable of handling headers.
105+
if (key.headers?.isNotEmpty ?? false) {
106+
final Completer<html.HttpRequest> completer =
107+
Completer<html.HttpRequest>();
108+
final html.HttpRequest request = httpRequestFactory();
109+
110+
request.open('GET', key.url, async: true);
111+
request.responseType = 'arraybuffer';
112+
key.headers!.forEach((String header, String value) {
113+
request.setRequestHeader(header, value);
114+
});
115+
116+
request.onLoad.listen((html.ProgressEvent e) {
117+
final int? status = request.status;
118+
final bool accepted = status! >= 200 && status < 300;
119+
final bool fileUri = status == 0; // file:// URIs have status of 0.
120+
final bool notModified = status == 304;
121+
final bool unknownRedirect = status > 307 && status < 400;
122+
final bool success =
123+
accepted || fileUri || notModified || unknownRedirect;
124+
125+
if (success) {
126+
completer.complete(request);
127+
} else {
128+
completer.completeError(e);
129+
throw image_provider.NetworkImageLoadException(
130+
statusCode: request.status ?? 400, uri: resolved);
131+
}
132+
});
133+
134+
request.onError.listen(completer.completeError);
135+
136+
request.send();
137+
138+
await completer.future;
139+
140+
final Uint8List bytes = (request.response as ByteBuffer).asUint8List();
141+
142+
if (bytes.lengthInBytes == 0)
143+
throw image_provider.NetworkImageLoadException(
144+
statusCode: request.status!, uri: resolved);
145+
146+
return decode(bytes);
147+
} else {
148+
// This API only exists in the web engine implementation and is not
149+
// contained in the analyzer summary for Flutter.
150+
// ignore: undefined_function, avoid_dynamic_calls
151+
return ui.webOnlyInstantiateImageCodecFromUrl(
152+
resolved,
153+
chunkCallback: (int bytes, int total) {
154+
chunkEvents.add(ImageChunkEvent(
155+
cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
156+
},
157+
) as Future<ui.Codec>;
158+
}
93159
}
94160

95161
@override
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Copyright 2014 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:typed_data';
8+
9+
import 'package:flutter/foundation.dart';
10+
import 'package:flutter/material.dart';
11+
import 'package:flutter/src/painting/_network_image_web.dart';
12+
import 'package:flutter_test/flutter_test.dart';
13+
14+
import '../image_data.dart';
15+
16+
void runTests() {
17+
tearDown(() {
18+
debugRestoreHttpRequestFactory();
19+
});
20+
21+
testWidgets('loads an image from the network with headers',
22+
(WidgetTester tester) async {
23+
final TestHttpRequest testHttpRequest = TestHttpRequest()
24+
..status = 200
25+
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
26+
html.ProgressEvent('test error'),
27+
])
28+
..response = (Uint8List.fromList(kTransparentImage)).buffer;
29+
30+
httpRequestFactory = () {
31+
return testHttpRequest;
32+
};
33+
34+
const Map<String, String> headers = <String, String>{
35+
'flutter': 'flutter',
36+
'second': 'second'
37+
};
38+
39+
final Image image = Image.network(
40+
'https://www.example.com/images/frame.png',
41+
headers: headers,
42+
);
43+
44+
await tester.pumpWidget(image);
45+
46+
assert(mapEquals(testHttpRequest.responseHeaders, headers), true);
47+
});
48+
49+
testWidgets('loads an image from the network with unsuccessful HTTP code',
50+
(WidgetTester tester) async {
51+
final TestHttpRequest testHttpRequest = TestHttpRequest()
52+
..status = 404
53+
..onError = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
54+
html.ProgressEvent('test error'),
55+
]);
56+
57+
httpRequestFactory = () {
58+
return testHttpRequest;
59+
};
60+
61+
const Map<String, String> headers = <String, String>{
62+
'flutter': 'flutter',
63+
'second': 'second'
64+
};
65+
66+
final Image image = Image.network(
67+
'https://www.example.com/images/frame2.png',
68+
headers: headers,
69+
);
70+
71+
await tester.pumpWidget(image);
72+
expect((tester.takeException() as html.ProgressEvent).type, 'test error');
73+
});
74+
75+
testWidgets('loads an image from the network with empty response',
76+
(WidgetTester tester) async {
77+
final TestHttpRequest testHttpRequest = TestHttpRequest()
78+
..status = 200
79+
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
80+
html.ProgressEvent('test error'),
81+
])
82+
..response = (Uint8List.fromList(<int>[])).buffer;
83+
84+
httpRequestFactory = () {
85+
return testHttpRequest;
86+
};
87+
88+
const Map<String, String> headers = <String, String>{
89+
'flutter': 'flutter',
90+
'second': 'second'
91+
};
92+
93+
final Image image = Image.network(
94+
'https://www.example.com/images/frame3.png',
95+
headers: headers,
96+
);
97+
98+
await tester.pumpWidget(image);
99+
expect(tester.takeException().toString(),
100+
'HTTP request failed, statusCode: 200, https://www.example.com/images/frame3.png');
101+
});
102+
}
103+
104+
// ignore: avoid_implementing_value_types
105+
class TestHttpRequest implements html.HttpRequest {
106+
@override
107+
String responseType = 'invalid';
108+
109+
@override
110+
int? timeout = 10;
111+
112+
@override
113+
bool? withCredentials = false;
114+
115+
@override
116+
void abort() {
117+
throw UnimplementedError();
118+
}
119+
120+
@override
121+
void addEventListener(String type, html.EventListener? listener,
122+
[bool? useCapture]) {
123+
throw UnimplementedError();
124+
}
125+
126+
@override
127+
bool dispatchEvent(html.Event event) {
128+
throw UnimplementedError();
129+
}
130+
131+
@override
132+
String getAllResponseHeaders() {
133+
throw UnimplementedError();
134+
}
135+
136+
@override
137+
String getResponseHeader(String name) {
138+
throw UnimplementedError();
139+
}
140+
141+
@override
142+
html.Events get on => throw UnimplementedError();
143+
144+
@override
145+
Stream<html.ProgressEvent> get onAbort => throw UnimplementedError();
146+
147+
@override
148+
Stream<html.ProgressEvent> onError =
149+
Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
150+
151+
@override
152+
Stream<html.ProgressEvent> onLoad =
153+
Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
154+
155+
@override
156+
Stream<html.ProgressEvent> get onLoadEnd => throw UnimplementedError();
157+
158+
@override
159+
Stream<html.ProgressEvent> get onLoadStart => throw UnimplementedError();
160+
161+
@override
162+
Stream<html.ProgressEvent> get onProgress => throw UnimplementedError();
163+
164+
@override
165+
Stream<html.Event> get onReadyStateChange => throw UnimplementedError();
166+
167+
@override
168+
Stream<html.ProgressEvent> get onTimeout => throw UnimplementedError();
169+
170+
@override
171+
void open(String method, String url,
172+
{bool? async, String? user, String? password}) {}
173+
174+
@override
175+
void overrideMimeType(String mime) {
176+
throw UnimplementedError();
177+
}
178+
179+
@override
180+
int get readyState => throw UnimplementedError();
181+
182+
@override
183+
void removeEventListener(String type, html.EventListener? listener,
184+
[bool? useCapture]) {
185+
throw UnimplementedError();
186+
}
187+
188+
@override
189+
dynamic response;
190+
191+
Map<String, String> headers = <String, String>{};
192+
193+
@override
194+
Map<String, String> get responseHeaders => headers;
195+
196+
@override
197+
String get responseText => throw UnimplementedError();
198+
199+
@override
200+
String get responseUrl => throw UnimplementedError();
201+
202+
@override
203+
html.Document get responseXml => throw UnimplementedError();
204+
205+
@override
206+
void send([dynamic bodyOrData]) {}
207+
208+
@override
209+
void setRequestHeader(String name, String value) {
210+
headers[name] = value;
211+
}
212+
213+
@override
214+
int status = -1;
215+
216+
@override
217+
String get statusText => throw UnimplementedError();
218+
219+
@override
220+
html.HttpRequestUpload get upload => throw UnimplementedError();
221+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright 2014 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+
void runTests() {
6+
// This is a web-specific test. Nothing to do for AOT engine.
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2014 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 '_network_image_web_test_io.dart'
6+
if (dart.library.html) '_network_image_test_web.dart';
7+
8+
void main() {
9+
runTests();
10+
}

0 commit comments

Comments
 (0)