Unverified Commit 31ca4db5 authored by Jonas Martinez's avatar Jonas Martinez Committed by GitHub

Flutter web add support for NetworkImage headers (#85954)

parent 0e2f51df
......@@ -2,8 +2,9 @@
// 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:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
......@@ -11,6 +12,22 @@ import 'package:flutter/foundation.dart';
import 'image_provider.dart' as image_provider;
import 'image_stream.dart';
/// Creates a type for an overridable factory function for testing purposes.
typedef HttpRequestFactory = html.HttpRequest Function();
/// Default HTTP client.
html.HttpRequest _httpClient() {
return html.HttpRequest();
}
/// Creates an overridable factory function.
HttpRequestFactory httpRequestFactory = _httpClient;
/// Restores to the default HTTP request factory.
void debugRestoreHttpRequestFactory() {
httpRequestFactory = _httpClient;
}
/// The dart:html implementation of [image_provider.NetworkImage].
///
/// NetworkImage on the web does not support decoding to a specified size.
......@@ -78,18 +95,67 @@ class NetworkImage
NetworkImage key,
image_provider.DecoderCallback decode,
StreamController<ImageChunkEvent> chunkEvents,
) {
) async {
assert(key == this);
final Uri resolved = Uri.base.resolve(key.url);
// This API only exists in the web engine implementation and is not
// contained in the analyzer summary for Flutter.
return ui.webOnlyInstantiateImageCodecFromUrl(// ignore: undefined_function, avoid_dynamic_calls
resolved,
chunkCallback: (int bytes, int total) {
chunkEvents.add(ImageChunkEvent(cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
},
) as Future<ui.Codec>;
// We use a different method when headers are set because the
// `ui.webOnlyInstantiateImageCodecFromUrl` method is not capable of handling headers.
if (key.headers?.isNotEmpty ?? false) {
final Completer<html.HttpRequest> completer =
Completer<html.HttpRequest>();
final html.HttpRequest request = httpRequestFactory();
request.open('GET', key.url, async: true);
request.responseType = 'arraybuffer';
key.headers!.forEach((String header, String value) {
request.setRequestHeader(header, value);
});
request.onLoad.listen((html.ProgressEvent e) {
final int? status = request.status;
final bool accepted = status! >= 200 && status < 300;
final bool fileUri = status == 0; // file:// URIs have status of 0.
final bool notModified = status == 304;
final bool unknownRedirect = status > 307 && status < 400;
final bool success =
accepted || fileUri || notModified || unknownRedirect;
if (success) {
completer.complete(request);
} else {
completer.completeError(e);
throw image_provider.NetworkImageLoadException(
statusCode: request.status ?? 400, uri: resolved);
}
});
request.onError.listen(completer.completeError);
request.send();
await completer.future;
final Uint8List bytes = (request.response as ByteBuffer).asUint8List();
if (bytes.lengthInBytes == 0)
throw image_provider.NetworkImageLoadException(
statusCode: request.status!, uri: resolved);
return decode(bytes);
} else {
// This API only exists in the web engine implementation and is not
// contained in the analyzer summary for Flutter.
// ignore: undefined_function, avoid_dynamic_calls
return ui.webOnlyInstantiateImageCodecFromUrl(
resolved,
chunkCallback: (int bytes, int total) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: bytes, expectedTotalBytes: total));
},
) as Future<ui.Codec>;
}
}
@override
......
// Copyright 2014 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:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/painting/_network_image_web.dart';
import 'package:flutter_test/flutter_test.dart';
import '../image_data.dart';
void runTests() {
tearDown(() {
debugRestoreHttpRequestFactory();
});
testWidgets('loads an image from the network with headers',
(WidgetTester tester) async {
final TestHttpRequest testHttpRequest = TestHttpRequest()
..status = 200
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
html.ProgressEvent('test error'),
])
..response = (Uint8List.fromList(kTransparentImage)).buffer;
httpRequestFactory = () {
return testHttpRequest;
};
const Map<String, String> headers = <String, String>{
'flutter': 'flutter',
'second': 'second'
};
final Image image = Image.network(
'https://www.example.com/images/frame.png',
headers: headers,
);
await tester.pumpWidget(image);
assert(mapEquals(testHttpRequest.responseHeaders, headers), true);
});
testWidgets('loads an image from the network with unsuccessful HTTP code',
(WidgetTester tester) async {
final TestHttpRequest testHttpRequest = TestHttpRequest()
..status = 404
..onError = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
html.ProgressEvent('test error'),
]);
httpRequestFactory = () {
return testHttpRequest;
};
const Map<String, String> headers = <String, String>{
'flutter': 'flutter',
'second': 'second'
};
final Image image = Image.network(
'https://www.example.com/images/frame2.png',
headers: headers,
);
await tester.pumpWidget(image);
expect((tester.takeException() as html.ProgressEvent).type, 'test error');
});
testWidgets('loads an image from the network with empty response',
(WidgetTester tester) async {
final TestHttpRequest testHttpRequest = TestHttpRequest()
..status = 200
..onLoad = Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[
html.ProgressEvent('test error'),
])
..response = (Uint8List.fromList(<int>[])).buffer;
httpRequestFactory = () {
return testHttpRequest;
};
const Map<String, String> headers = <String, String>{
'flutter': 'flutter',
'second': 'second'
};
final Image image = Image.network(
'https://www.example.com/images/frame3.png',
headers: headers,
);
await tester.pumpWidget(image);
expect(tester.takeException().toString(),
'HTTP request failed, statusCode: 200, https://www.example.com/images/frame3.png');
});
}
// ignore: avoid_implementing_value_types
class TestHttpRequest implements html.HttpRequest {
@override
String responseType = 'invalid';
@override
int? timeout = 10;
@override
bool? withCredentials = false;
@override
void abort() {
throw UnimplementedError();
}
@override
void addEventListener(String type, html.EventListener? listener,
[bool? useCapture]) {
throw UnimplementedError();
}
@override
bool dispatchEvent(html.Event event) {
throw UnimplementedError();
}
@override
String getAllResponseHeaders() {
throw UnimplementedError();
}
@override
String getResponseHeader(String name) {
throw UnimplementedError();
}
@override
html.Events get on => throw UnimplementedError();
@override
Stream<html.ProgressEvent> get onAbort => throw UnimplementedError();
@override
Stream<html.ProgressEvent> onError =
Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
@override
Stream<html.ProgressEvent> onLoad =
Stream<html.ProgressEvent>.fromIterable(<html.ProgressEvent>[]);
@override
Stream<html.ProgressEvent> get onLoadEnd => throw UnimplementedError();
@override
Stream<html.ProgressEvent> get onLoadStart => throw UnimplementedError();
@override
Stream<html.ProgressEvent> get onProgress => throw UnimplementedError();
@override
Stream<html.Event> get onReadyStateChange => throw UnimplementedError();
@override
Stream<html.ProgressEvent> get onTimeout => throw UnimplementedError();
@override
void open(String method, String url,
{bool? async, String? user, String? password}) {}
@override
void overrideMimeType(String mime) {
throw UnimplementedError();
}
@override
int get readyState => throw UnimplementedError();
@override
void removeEventListener(String type, html.EventListener? listener,
[bool? useCapture]) {
throw UnimplementedError();
}
@override
dynamic response;
Map<String, String> headers = <String, String>{};
@override
Map<String, String> get responseHeaders => headers;
@override
String get responseText => throw UnimplementedError();
@override
String get responseUrl => throw UnimplementedError();
@override
html.Document get responseXml => throw UnimplementedError();
@override
void send([dynamic bodyOrData]) {}
@override
void setRequestHeader(String name, String value) {
headers[name] = value;
}
@override
int status = -1;
@override
String get statusText => throw UnimplementedError();
@override
html.HttpRequestUpload get upload => throw UnimplementedError();
}
// Copyright 2014 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.
void runTests() {
// This is a web-specific test. Nothing to do for AOT engine.
}
// Copyright 2014 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 '_network_image_web_test_io.dart'
if (dart.library.html) '_network_image_test_web.dart';
void main() {
runTests();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment