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

Commit 02a379d

Browse files
authored
[web] consolidate network code into httpFetch (#39657)
* consolidate network code into httpFetch * make HTTP test cross-browser friendly; fix copypasta
1 parent fa1b2dd commit 02a379d

File tree

15 files changed

+912
-300
lines changed

15 files changed

+912
-300
lines changed

lib/web_ui/dev/test_platform.dart

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ class BrowserPlatform extends PlatformPlugin {
9090
// This handler goes last, after all more specific handlers failed to handle the request.
9191
.add(_createAbsolutePackageUrlHandler())
9292
.add(_screenshotHandler)
93+
94+
// Generates and serves a test payload of given length, split into chunks
95+
// of given size. Reponds to requests to /long_test_payload.
96+
.add(_testPayloadGenerator)
97+
98+
// If none of the handlers above handled the request, return 404.
9399
.add(_fileNotFoundCatcher);
94100

95101
server.mount(cascade.handler);
@@ -320,6 +326,46 @@ class BrowserPlatform extends PlatformPlugin {
320326
};
321327
}
322328

329+
Future<shelf.Response> _testPayloadGenerator(shelf.Request request) async {
330+
if (!request.requestedUri.path.endsWith('/long_test_payload')) {
331+
return shelf.Response.notFound(
332+
'This request is not handled by the test payload generator');
333+
}
334+
335+
final int payloadLength = int.parse(request.requestedUri.queryParameters['length']!);
336+
final int chunkLength = int.parse(request.requestedUri.queryParameters['chunk']!);
337+
338+
final StreamController<List<int>> controller = StreamController<List<int>>();
339+
340+
Future<void> fillPayload() async {
341+
int remainingByteCount = payloadLength;
342+
int byteCounter = 0;
343+
while (remainingByteCount > 0) {
344+
final int currentChunkLength = min(chunkLength, remainingByteCount);
345+
final List<int> chunk = List<int>.generate(
346+
currentChunkLength,
347+
(int i) => (byteCounter + i) & 0xFF,
348+
);
349+
byteCounter = (byteCounter + currentChunkLength) & 0xFF;
350+
remainingByteCount -= currentChunkLength;
351+
controller.add(chunk);
352+
await Future<void>.delayed(const Duration(milliseconds: 100));
353+
}
354+
await controller.close();
355+
}
356+
357+
// Kick off payload filling function but don't block on it. The stream should
358+
// be returned immediately, and the client should receive data in chunks.
359+
unawaited(fillPayload());
360+
return shelf.Response.ok(
361+
controller.stream,
362+
headers: <String, String>{
363+
'Content-Type': 'application/octet-stream',
364+
'Content-Length': '$payloadLength',
365+
},
366+
);
367+
}
368+
323369
Future<shelf.Response> _screenshotHandler(shelf.Request request) async {
324370
if (!request.requestedUri.path.endsWith('/screenshot')) {
325371
return shelf.Response.notFound(

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

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -63,52 +63,23 @@ class AssetManager {
6363
return Uri.encodeFull('${_baseUrl ?? ''}$assetsDir/$asset');
6464
}
6565

66+
/// Loads an asset and returns the server response.
67+
Future<HttpFetchResponse> loadAsset(String asset) {
68+
return httpFetch(getAssetUrl(asset));
69+
}
70+
6671
/// Loads an asset using an [DomXMLHttpRequest] and returns data as [ByteData].
6772
Future<ByteData> load(String asset) async {
6873
final String url = getAssetUrl(asset);
69-
try {
70-
final DomXMLHttpRequest request =
71-
await domHttpRequest(url, responseType: 'arraybuffer');
72-
73-
final ByteBuffer response = request.response as ByteBuffer;
74-
return response.asByteData();
75-
} catch (e) {
76-
if (!domInstanceOfString(e, 'ProgressEvent')){
77-
rethrow;
78-
}
79-
final DomProgressEvent p = e as DomProgressEvent;
80-
final DomEventTarget? target = p.target;
81-
if (domInstanceOfString(target,'XMLHttpRequest')) {
82-
final DomXMLHttpRequest request = target! as DomXMLHttpRequest;
83-
if (request.status == 404 && asset == 'AssetManifest.json') {
84-
printWarning('Asset manifest does not exist at `$url` – ignoring.');
85-
return Uint8List.fromList(utf8.encode('{}')).buffer.asByteData();
86-
}
87-
throw AssetManagerException(url, request.status!.toInt());
88-
}
89-
90-
final String? constructorName = target == null ? 'null' :
91-
domGetConstructorName(target);
92-
printWarning('Caught ProgressEvent with unknown target: '
93-
'$constructorName');
94-
rethrow;
95-
}
96-
}
97-
}
74+
final HttpFetchResponse response = await httpFetch(url);
9875

99-
/// Thrown to indicate http failure during asset loading.
100-
class AssetManagerException implements Exception {
101-
/// Initializes exception with request url and http status.
102-
AssetManagerException(this.url, this.httpStatus);
103-
104-
/// Http request url for asset.
105-
final String url;
106-
107-
/// Http status of response.
108-
final int httpStatus;
76+
if (response.status == 404 && asset == 'AssetManifest.json') {
77+
printWarning('Asset manifest does not exist at `$url` - ignoring.');
78+
return Uint8List.fromList(utf8.encode('{}')).buffer.asByteData();
79+
}
10980

110-
@override
111-
String toString() => 'Failed to load asset at "$url" ($httpStatus)';
81+
return (await response.payload.asByteBuffer()).asByteData();
82+
}
11283
}
11384

11485
/// An asset manager that gives fake empty responses for assets.
@@ -141,6 +112,33 @@ class WebOnlyMockAssetManager implements AssetManager {
141112
@override
142113
String getAssetUrl(String asset) => asset;
143114

115+
@override
116+
Future<HttpFetchResponse> loadAsset(String asset) async {
117+
if (asset == getAssetUrl('AssetManifest.json')) {
118+
return MockHttpFetchResponse(
119+
url: asset,
120+
status: 200,
121+
payload: MockHttpFetchPayload(
122+
byteBuffer: _toByteData(utf8.encode(defaultAssetManifest)).buffer,
123+
)
124+
);
125+
}
126+
if (asset == getAssetUrl('FontManifest.json')) {
127+
return MockHttpFetchResponse(
128+
url: asset,
129+
status: 200,
130+
payload: MockHttpFetchPayload(
131+
byteBuffer: _toByteData(utf8.encode(defaultFontManifest)).buffer,
132+
)
133+
);
134+
}
135+
136+
return MockHttpFetchResponse(
137+
url: asset,
138+
status: 404,
139+
);
140+
}
141+
144142
@override
145143
Future<ByteData> load(String asset) {
146144
if (asset == getAssetUrl('AssetManifest.json')) {
@@ -151,7 +149,7 @@ class WebOnlyMockAssetManager implements AssetManager {
151149
return Future<ByteData>.value(
152150
_toByteData(utf8.encode(defaultFontManifest)));
153151
}
154-
throw AssetManagerException(asset, 404);
152+
throw HttpFetchNoPayloadError(asset, status: 404);
155153
}
156154

157155
ByteData _toByteData(List<int> bytes) {

lib/web_ui/lib/src/engine/canvaskit/font_fallbacks.dart

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -488,37 +488,33 @@ class NotoDownloader {
488488
/// Downloads the [url] and returns it as a [ByteBuffer].
489489
///
490490
/// Override this for testing.
491-
Future<ByteBuffer> downloadAsBytes(String url, {String? debugDescription}) {
491+
Future<ByteBuffer> downloadAsBytes(String url, {String? debugDescription}) async {
492492
if (assertionsEnabled) {
493493
_debugActiveDownloadCount += 1;
494494
}
495-
final Future<ByteBuffer> result = httpFetch(url).then(
496-
(DomResponse fetchResult) => fetchResult
497-
.arrayBuffer()
498-
.then<ByteBuffer>((dynamic x) => x as ByteBuffer));
495+
final Future<ByteBuffer> data = httpFetchByteBuffer(url);
499496
if (assertionsEnabled) {
500-
result.whenComplete(() {
497+
unawaited(data.whenComplete(() {
501498
_debugActiveDownloadCount -= 1;
502-
});
499+
}));
503500
}
504-
return result;
501+
return data;
505502
}
506503

507504
/// Downloads the [url] and returns is as a [String].
508505
///
509506
/// Override this for testing.
510-
Future<String> downloadAsString(String url, {String? debugDescription}) {
507+
Future<String> downloadAsString(String url, {String? debugDescription}) async {
511508
if (assertionsEnabled) {
512509
_debugActiveDownloadCount += 1;
513510
}
514-
final Future<String> result = httpFetch(url).then((DomResponse response) =>
515-
response.text().then<String>((dynamic x) => x as String));
511+
final Future<String> data = httpFetchText(url);
516512
if (assertionsEnabled) {
517-
result.whenComplete(() {
513+
unawaited(data.whenComplete(() {
518514
_debugActiveDownloadCount -= 1;
519-
});
515+
}));
520516
}
521-
return result;
517+
return data;
522518
}
523519
}
524520

lib/web_ui/lib/src/engine/canvaskit/fonts.dart

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,21 +93,15 @@ class SkiaFontCollection implements FontCollection {
9393
/// Loads fonts from `FontManifest.json`.
9494
@override
9595
Future<void> downloadAssetFonts(AssetManager assetManager) async {
96-
ByteData byteData;
96+
final HttpFetchResponse response = await assetManager.loadAsset('FontManifest.json');
9797

98-
try {
99-
byteData = await assetManager.load('FontManifest.json');
100-
} on AssetManagerException catch (e) {
101-
if (e.httpStatus == 404) {
102-
printWarning('Font manifest does not exist at `${e.url}` – ignoring.');
103-
return;
104-
} else {
105-
rethrow;
106-
}
98+
if (!response.hasPayload) {
99+
printWarning('Font manifest does not exist at `${response.url}` - ignoring.');
100+
return;
107101
}
108102

109-
final List<dynamic>? fontManifest =
110-
json.decode(utf8.decode(byteData.buffer.asUint8List())) as List<dynamic>?;
103+
final Uint8List data = await response.asUint8List();
104+
final List<dynamic>? fontManifest = json.decode(utf8.decode(data)) as List<dynamic>?;
111105
if (fontManifest == null) {
112106
throw AssertionError(
113107
'There was a problem trying to load FontManifest.json');
@@ -206,10 +200,11 @@ class SkiaFontCollection implements FontCollection {
206200
String family
207201
) {
208202
Future<UnregisteredFont?> downloadFont() async {
209-
ByteBuffer buffer;
203+
// Try to get the font leniently. Do not crash the app when failing to
204+
// fetch the font in the spirit of "gradual degradation of functionality".
210205
try {
211-
buffer = await httpFetch(url).then(_getArrayBuffer);
212-
return UnregisteredFont(buffer, url, family);
206+
final ByteBuffer data = await httpFetchByteBuffer(url);
207+
return UnregisteredFont(data, url, family);
213208
} catch (e) {
214209
printWarning('Failed to load font $family at $url');
215210
printWarning(e.toString());
@@ -230,12 +225,6 @@ class SkiaFontCollection implements FontCollection {
230225
return actualFamily;
231226
}
232227

233-
Future<ByteBuffer> _getArrayBuffer(DomResponse fetchResult) {
234-
return fetchResult
235-
.arrayBuffer()
236-
.then<ByteBuffer>((dynamic x) => x as ByteBuffer);
237-
}
238-
239228
TypefaceFontProvider? fontProvider;
240229

241230
@override

lib/web_ui/lib/src/engine/canvaskit/image.dart

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,6 @@ class ImageCodecException implements Exception {
167167

168168
const String _kNetworkImageMessage = 'Failed to load network image.';
169169

170-
typedef HttpRequestFactory = DomXMLHttpRequest Function();
171-
HttpRequestFactory httpRequestFactory = () => createDomXMLHttpRequest();
172-
void debugRestoreHttpRequestFactory() {
173-
httpRequestFactory = () => createDomXMLHttpRequest();
174-
}
175-
176170
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after
177171
/// requesting from URI.
178172
Future<ui.Codec> skiaInstantiateWebImageCodec(
@@ -186,49 +180,48 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
186180
}
187181

188182
/// Sends a request to fetch image data.
189-
Future<Uint8List> fetchImage(
190-
String url, WebOnlyImageCodecChunkCallback? chunkCallback) {
191-
final Completer<Uint8List> completer = Completer<Uint8List>();
192-
193-
final DomXMLHttpRequest request = httpRequestFactory();
194-
request.open('GET', url, true);
195-
request.responseType = 'arraybuffer';
196-
if (chunkCallback != null) {
197-
request.addEventListener('progress', allowInterop((DomEvent event) {
198-
event = event as DomProgressEvent;
199-
chunkCallback.call(event.loaded!.toInt(), event.total!.toInt());
200-
}));
201-
}
202-
203-
request.addEventListener('error', allowInterop((DomEvent event) {
204-
completer.completeError(ImageCodecException('$_kNetworkImageMessage\n'
183+
Future<Uint8List> fetchImage(String url, WebOnlyImageCodecChunkCallback? chunkCallback) async {
184+
try {
185+
final HttpFetchResponse response = await httpFetch(url);
186+
final int? contentLength = response.contentLength;
187+
188+
if (!response.hasPayload) {
189+
throw ImageCodecException(
190+
'$_kNetworkImageMessage\n'
205191
'Image URL: $url\n'
206-
'Trying to load an image from another domain? Find answers at:\n'
207-
'https://flutter.dev/docs/development/platform-integration/web-images'));
208-
}));
209-
210-
request.addEventListener('load', allowInterop((DomEvent event) {
211-
final int status = request.status!.toInt();
212-
final bool accepted = status >= 200 && status < 300;
213-
final bool fileUri = status == 0; // file:// URIs have status of 0.
214-
final bool notModified = status == 304;
215-
final bool unknownRedirect = status > 307 && status < 400;
216-
final bool success = accepted || fileUri || notModified || unknownRedirect;
217-
218-
if (!success) {
219-
completer.completeError(
220-
ImageCodecException('$_kNetworkImageMessage\n'
221-
'Image URL: $url\n'
222-
'Server response code: $status'),
192+
'Server response code: ${response.status}',
223193
);
224-
return;
225194
}
226195

227-
completer.complete(Uint8List.view(request.response as ByteBuffer));
228-
}));
196+
if (chunkCallback != null && contentLength != null) {
197+
return readChunked(response.payload, contentLength, chunkCallback);
198+
} else {
199+
return await response.asUint8List();
200+
}
201+
} on HttpFetchError catch (_) {
202+
throw ImageCodecException(
203+
'$_kNetworkImageMessage\n'
204+
'Image URL: $url\n'
205+
'Trying to load an image from another domain? Find answers at:\n'
206+
'https://flutter.dev/docs/development/platform-integration/web-images',
207+
);
208+
}
209+
}
229210

230-
request.send();
231-
return completer.future;
211+
/// Reads the [payload] in chunks using the browser's Streams API
212+
///
213+
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
214+
Future<Uint8List> readChunked(HttpFetchPayload payload, int contentLength, WebOnlyImageCodecChunkCallback chunkCallback) async {
215+
final Uint8List result = Uint8List(contentLength);
216+
int position = 0;
217+
int cumulativeBytesLoaded = 0;
218+
await payload.read<Uint8List>((Uint8List chunk) {
219+
cumulativeBytesLoaded += chunk.lengthInBytes;
220+
chunkCallback(cumulativeBytesLoaded, contentLength);
221+
result.setAll(position, chunk);
222+
position += chunk.lengthInBytes;
223+
});
224+
return result;
232225
}
233226

234227
/// A [ui.Image] backed by an `SkImage` from Skia.

0 commit comments

Comments
 (0)