mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Update flutter/http.dart to use dart:io. (#5940)
This commit is contained in:
parent
4f0eff31bc
commit
400585cb96
|
@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/http.dart' as http;
|
||||
import 'package:mojo/core.dart' as core;
|
||||
|
||||
// All of these sounds are marked as public domain at soundbible.
|
||||
const String chimes = "http://soundbible.com/grab.php?id=2030&type=wav";
|
||||
|
@ -42,7 +43,10 @@ class PianoKey {
|
|||
Future<Null> load(mojom.MediaServiceProxy mediaService) async {
|
||||
try {
|
||||
mediaService.createPlayer(player);
|
||||
player.prepare(await http.readDataPipe(soundUrl), (bool ignored) { });
|
||||
http.Response response = await http.get(soundUrl);
|
||||
core.MojoDataPipe pipe = new core.MojoDataPipe();
|
||||
core.DataPipeFiller.fillHandle(pipe.producer, response.bodyBytes.buffer.asByteData());
|
||||
player.prepare(pipe.consumer, (bool ignored) { });
|
||||
} catch (e) {
|
||||
print("Error: failed to load sound file $soundUrl");
|
||||
player.close();
|
||||
|
|
|
@ -3,16 +3,18 @@
|
|||
// found in the LICENSE file.
|
||||
|
||||
/// A [Future]-based library for making HTTP requests.
|
||||
///
|
||||
///
|
||||
/// To use, import `package:flutter/http.dart`.
|
||||
///
|
||||
/// This library is based on Dart's `http` package, but differs in that it is a
|
||||
/// `mojo`-based HTTP client and does not have a dependency on mirrors.
|
||||
/// This library is based on Dart's `http` package, but differs in that it does
|
||||
/// not have a dependency on mirrors.
|
||||
///
|
||||
/// This library depends only on core Dart libraries as well as the `mojo`,
|
||||
/// `mojo_services`, and `sky_services` packages.
|
||||
// TODO(chinmaygarde): The contents of `lib/src/http` will become redundant
|
||||
// once https://github.com/dart-lang/http/issues/1 is fixed (removes the use
|
||||
// of mirrors). Once that issue is addressed, we should get rid this directory
|
||||
// and use `dart-lang/http` directly.
|
||||
library http;
|
||||
|
||||
export 'src/http/http.dart';
|
||||
export 'src/http/mojo_client.dart';
|
||||
export 'src/http/response.dart';
|
||||
export 'src/http/mock_client.dart';
|
||||
|
|
7
packages/flutter/lib/src/http/README.md
Normal file
7
packages/flutter/lib/src/http/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
`dart-lang/http` without Mirrors
|
||||
================================
|
||||
|
||||
The contents of this will become redundant once
|
||||
https://github.com/dart-lang/http/issues/1 is fixed (removes the use
|
||||
of mirrors). Once that issue is addressed, we should get rid this directory
|
||||
and use `dart-lang/http` directly.
|
199
packages/flutter/lib/src/http/base_client.dart
Normal file
199
packages/flutter/lib/src/http/base_client.dart
Normal file
|
@ -0,0 +1,199 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'base_request.dart';
|
||||
import 'client.dart';
|
||||
import 'exception.dart';
|
||||
import 'request.dart';
|
||||
import 'response.dart';
|
||||
import 'streamed_response.dart';
|
||||
|
||||
/// The abstract base class for an HTTP client. This is a mixin-style class;
|
||||
/// subclasses only need to implement [send] and maybe [close], and then they
|
||||
/// get various convenience methods for free.
|
||||
abstract class BaseClient implements Client {
|
||||
/// Sends an HTTP HEAD request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
@override
|
||||
Future<Response> head(dynamic url, {Map<String, String> headers}) =>
|
||||
_sendUnstreamed("HEAD", url, headers);
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
@override
|
||||
Future<Response> get(dynamic url, {Map<String, String> headers}) =>
|
||||
_sendUnstreamed("GET", url, headers);
|
||||
|
||||
/// Sends an HTTP POST request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>]
|
||||
/// or a [Map<String, String>]. If it's a String, it's encoded using
|
||||
/// [encoding] and used as the body of the request. The content-type of the
|
||||
/// request will default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to UTF-8.
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
@override
|
||||
Future<Response> post(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding}) =>
|
||||
_sendUnstreamed("POST", url, headers, body, encoding);
|
||||
|
||||
/// Sends an HTTP PUT request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>]
|
||||
/// or a [Map<String, String>]. If it's a String, it's encoded using
|
||||
/// [encoding] and used as the body of the request. The content-type of the
|
||||
/// request will default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to UTF-8.
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
@override
|
||||
Future<Response> put(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding}) =>
|
||||
_sendUnstreamed("PUT", url, headers, body, encoding);
|
||||
|
||||
/// Sends an HTTP PATCH request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>]
|
||||
/// or a [Map<String, String>]. If it's a String, it's encoded using
|
||||
/// [encoding] and used as the body of the request. The content-type of the
|
||||
/// request will default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to UTF-8.
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
@override
|
||||
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding}) =>
|
||||
_sendUnstreamed("PATCH", url, headers, body, encoding);
|
||||
|
||||
/// Sends an HTTP DELETE request with the given headers to the given URL,
|
||||
/// which can be a [Uri] or a [String].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
@override
|
||||
Future<Response> delete(dynamic url, {Map<String, String> headers}) =>
|
||||
_sendUnstreamed("DELETE", url, headers);
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String], and returns a Future that completes to the
|
||||
/// body of the response as a String.
|
||||
///
|
||||
/// The Future will emit a [ClientException] if the response doesn't have a
|
||||
/// success status code.
|
||||
///
|
||||
/// For more fine-grained control over the request and response, use [send] or
|
||||
/// [get] instead.
|
||||
@override
|
||||
Future<String> read(dynamic url, {Map<String, String> headers}) {
|
||||
return get(url, headers: headers).then((Response response) {
|
||||
_checkResponseSuccess(url, response);
|
||||
return response.body;
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String], and returns a Future that completes to the
|
||||
/// body of the response as a list of bytes.
|
||||
///
|
||||
/// The Future will emit an [ClientException] if the response doesn't have a
|
||||
/// success status code.
|
||||
///
|
||||
/// For more fine-grained control over the request and response, use [send] or
|
||||
/// [get] instead.
|
||||
@override
|
||||
Future<Uint8List> readBytes(dynamic url, {Map<String, String> headers}) {
|
||||
return get(url, headers: headers).then((Response response) {
|
||||
_checkResponseSuccess(url, response);
|
||||
return response.bodyBytes;
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends an HTTP request and asynchronously returns the response.
|
||||
///
|
||||
/// Implementers should call [BaseRequest.finalize] to get the body of the
|
||||
/// request as a [ByteStream]. They shouldn't make any assumptions about the
|
||||
/// state of the stream; it could have data written to it asynchronously at a
|
||||
/// later point, or it could already be closed when it's returned. Any
|
||||
/// internal HTTP errors should be wrapped as [ClientException]s.
|
||||
@override
|
||||
Future<StreamedResponse> send(BaseRequest request);
|
||||
|
||||
/// Sends a non-streaming [Request] and returns a non-streaming [Response].
|
||||
Future<Response> _sendUnstreamed(String method, dynamic url,
|
||||
Map<String, String> headers, [dynamic body, Encoding encoding]) async {
|
||||
|
||||
if (url is String) url = Uri.parse(url);
|
||||
Request request = new Request(method, url);
|
||||
|
||||
if (headers != null) request.headers.addAll(headers);
|
||||
if (encoding != null) request.encoding = encoding;
|
||||
if (body != null) {
|
||||
if (body is String) {
|
||||
request.body = body;
|
||||
} else if (body is List) {
|
||||
request.bodyBytes = DelegatingList.typed(body);
|
||||
} else if (body is Map) {
|
||||
request.bodyFields = DelegatingMap.typed(body);
|
||||
} else {
|
||||
throw new ArgumentError('Invalid request body "$body".');
|
||||
}
|
||||
}
|
||||
|
||||
return Response.fromStream(await send(request));
|
||||
}
|
||||
|
||||
/// Throws an error if [response] is not successful.
|
||||
void _checkResponseSuccess(dynamic url, Response response) {
|
||||
if (response.statusCode < 400) return;
|
||||
String message = "Request to $url failed with status ${response.statusCode}";
|
||||
if (response.reasonPhrase != null) {
|
||||
message = "$message: ${response.reasonPhrase}";
|
||||
}
|
||||
if (url is String) url = Uri.parse(url);
|
||||
throw new ClientException("$message.", url);
|
||||
}
|
||||
|
||||
/// Closes the client and cleans up any resources associated with it. It's
|
||||
/// important to close each client when it's done being used; failing to do so
|
||||
/// can cause the Dart process to hang.
|
||||
@override
|
||||
void close() {}
|
||||
}
|
141
packages/flutter/lib/src/http/base_request.dart
Normal file
141
packages/flutter/lib/src/http/base_request.dart
Normal file
|
@ -0,0 +1,141 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:collection';
|
||||
|
||||
import 'byte_stream.dart';
|
||||
import 'client.dart';
|
||||
import 'streamed_response.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// The base class for HTTP requests.
|
||||
///
|
||||
/// Subclasses of [BaseRequest] can be constructed manually and passed to
|
||||
/// [BaseClient.send], which allows the user to provide fine-grained control
|
||||
/// over the request properties. However, usually it's easier to use convenience
|
||||
/// methods like [get] or [BaseClient.get].
|
||||
abstract class BaseRequest {
|
||||
/// The HTTP method of the request. Most commonly "GET" or "POST", less
|
||||
/// commonly "HEAD", "PUT", or "DELETE". Non-standard method names are also
|
||||
/// supported.
|
||||
final String method;
|
||||
|
||||
/// The URL to which the request will be sent.
|
||||
final Uri url;
|
||||
|
||||
/// Creates a new HTTP request.
|
||||
BaseRequest(this.method, this.url)
|
||||
: headers = new LinkedHashMap<String, String>(
|
||||
equals: (String key1, String key2) => key1.toLowerCase() == key2.toLowerCase(),
|
||||
hashCode: (String key) => key.toLowerCase().hashCode);
|
||||
|
||||
/// The size of the request body, in bytes.
|
||||
///
|
||||
/// This defaults to `null`, which indicates that the size of the request is
|
||||
/// not known in advance.
|
||||
int get contentLength => _contentLength;
|
||||
int _contentLength;
|
||||
|
||||
set contentLength(int value) {
|
||||
if (value != null && value < 0) {
|
||||
throw new ArgumentError("Invalid content length $value.");
|
||||
}
|
||||
_checkFinalized();
|
||||
_contentLength = value;
|
||||
}
|
||||
|
||||
/// Whether a persistent connection should be maintained with the server.
|
||||
/// Defaults to true.
|
||||
bool get persistentConnection => _persistentConnection;
|
||||
bool _persistentConnection = true;
|
||||
|
||||
set persistentConnection(bool value) {
|
||||
_checkFinalized();
|
||||
_persistentConnection = value;
|
||||
}
|
||||
|
||||
/// Whether the client should follow redirects while resolving this request.
|
||||
/// Defaults to true.
|
||||
bool get followRedirects => _followRedirects;
|
||||
bool _followRedirects = true;
|
||||
|
||||
set followRedirects(bool value) {
|
||||
_checkFinalized();
|
||||
_followRedirects = value;
|
||||
}
|
||||
|
||||
/// The maximum number of redirects to follow when [followRedirects] is true.
|
||||
/// If this number is exceeded the [BaseResponse] future will signal a
|
||||
/// [RedirectException]. Defaults to 5.
|
||||
int get maxRedirects => _maxRedirects;
|
||||
int _maxRedirects = 5;
|
||||
|
||||
set maxRedirects(int value) {
|
||||
_checkFinalized();
|
||||
_maxRedirects = value;
|
||||
}
|
||||
|
||||
// TODO(nweiz): automatically parse cookies from headers
|
||||
|
||||
// TODO(nweiz): make this a HttpHeaders object
|
||||
/// The headers for this request.
|
||||
final Map<String, String> headers;
|
||||
|
||||
/// Whether the request has been finalized.
|
||||
bool get finalized => _finalized;
|
||||
bool _finalized = false;
|
||||
|
||||
/// Finalizes the HTTP request in preparation for it being sent. This freezes
|
||||
/// all mutable fields and returns a single-subscription [ByteStream] that
|
||||
/// emits the body of the request.
|
||||
///
|
||||
/// The base implementation of this returns null rather than a [ByteStream];
|
||||
/// subclasses are responsible for creating the return value, which should be
|
||||
/// single-subscription to ensure that no data is dropped. They should also
|
||||
/// freeze any additional mutable fields they add that don't make sense to
|
||||
/// change after the request headers are sent.
|
||||
ByteStream finalize() {
|
||||
// TODO(nweiz): freeze headers
|
||||
if (finalized) throw new StateError("Can't finalize a finalized Request.");
|
||||
_finalized = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Sends this request.
|
||||
///
|
||||
/// This automatically initializes a new [Client] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [Client] for all of those
|
||||
/// requests.
|
||||
Future<StreamedResponse> send() async {
|
||||
Client client = new Client();
|
||||
|
||||
try {
|
||||
StreamedResponse response = await client.send(this);
|
||||
Stream<dynamic> stream = onDone(response.stream, client.close);
|
||||
return new StreamedResponse(
|
||||
new ByteStream(stream),
|
||||
response.statusCode,
|
||||
contentLength: response.contentLength,
|
||||
request: response.request,
|
||||
headers: response.headers,
|
||||
isRedirect: response.isRedirect,
|
||||
persistentConnection: response.persistentConnection,
|
||||
reasonPhrase: response.reasonPhrase);
|
||||
} catch (ex) {
|
||||
client.close();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Throws an error if this request has been finalized.
|
||||
void _checkFinalized() {
|
||||
if (!finalized) return;
|
||||
throw new StateError("Can't modify a finalized Request.");
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => "$method $url";
|
||||
}
|
53
packages/flutter/lib/src/http/base_response.dart
Normal file
53
packages/flutter/lib/src/http/base_response.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'base_request.dart';
|
||||
|
||||
/// The base class for HTTP responses.
|
||||
///
|
||||
/// Subclasses of [BaseResponse] are usually not constructed manually; instead,
|
||||
/// they're returned by [BaseClient.send] or other HTTP client methods.
|
||||
abstract class BaseResponse {
|
||||
/// The (frozen) request that triggered this response.
|
||||
final BaseRequest request;
|
||||
|
||||
/// The status code of the response.
|
||||
final int statusCode;
|
||||
|
||||
/// The reason phrase associated with the status code.
|
||||
final String reasonPhrase;
|
||||
|
||||
/// The size of the response body, in bytes.
|
||||
///
|
||||
/// If the size of the request is not known in advance, this is `null`.
|
||||
final int contentLength;
|
||||
|
||||
// TODO(nweiz): automatically parse cookies from headers
|
||||
|
||||
// TODO(nweiz): make this a HttpHeaders object.
|
||||
/// The headers for this response.
|
||||
final Map<String, String> headers;
|
||||
|
||||
/// Whether this response is a redirect.
|
||||
final bool isRedirect;
|
||||
|
||||
/// Whether the server requested that a persistent connection be maintained.
|
||||
final bool persistentConnection;
|
||||
|
||||
/// Creates a new HTTP response.
|
||||
BaseResponse(
|
||||
this.statusCode,
|
||||
{this.contentLength,
|
||||
this.request,
|
||||
this.headers: const <String, String>{},
|
||||
this.isRedirect: false,
|
||||
this.persistentConnection: true,
|
||||
this.reasonPhrase}) {
|
||||
if (statusCode < 100) {
|
||||
throw new ArgumentError("Invalid status code $statusCode.");
|
||||
} else if (contentLength != null && contentLength < 0) {
|
||||
throw new ArgumentError("Invalid content length $contentLength.");
|
||||
}
|
||||
}
|
||||
}
|
36
packages/flutter/lib/src/http/byte_stream.dart
Normal file
36
packages/flutter/lib/src/http/byte_stream.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// A stream of chunks of bytes representing a single piece of data.
|
||||
class ByteStream extends StreamView<List<int>> {
|
||||
ByteStream(Stream<List<int>> stream)
|
||||
: super(stream);
|
||||
|
||||
/// Returns a single-subscription byte stream that will emit the given bytes
|
||||
/// in a single chunk.
|
||||
factory ByteStream.fromBytes(List<int> bytes) =>
|
||||
new ByteStream(new Stream<dynamic>.fromIterable(<List<int>>[bytes]));
|
||||
|
||||
/// Collects the data of this stream in a [Uint8List].
|
||||
Future<Uint8List> toBytes() {
|
||||
Completer<Uint8List> completer = new Completer<Uint8List>();
|
||||
dynamic sink = new ByteConversionSink.withCallback((dynamic bytes) =>
|
||||
completer.complete(new Uint8List.fromList(bytes)));
|
||||
listen(sink.add, onError: completer.completeError, onDone: sink.close,
|
||||
cancelOnError: true);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Collect the data of this stream in a [String], decoded according to
|
||||
/// [encoding], which defaults to `UTF8`.
|
||||
Future<String> bytesToString([Encoding encoding=UTF8]) =>
|
||||
encoding.decodeStream(this);
|
||||
|
||||
Stream<String> toStringStream([Encoding encoding=UTF8]) =>
|
||||
encoding.decoder.bind(this);
|
||||
}
|
148
packages/flutter/lib/src/http/client.dart
Normal file
148
packages/flutter/lib/src/http/client.dart
Normal file
|
@ -0,0 +1,148 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'base_client.dart';
|
||||
import 'base_request.dart';
|
||||
import 'io_client.dart';
|
||||
import 'response.dart';
|
||||
import 'streamed_response.dart';
|
||||
|
||||
typedef Client ClientOverride();
|
||||
|
||||
/// The interface for HTTP clients that take care of maintaining persistent
|
||||
/// connections across multiple requests to the same server. If you only need to
|
||||
/// send a single request, it's usually easier to use [head], [get], [post],
|
||||
/// [put], [patch], or [delete] instead.
|
||||
///
|
||||
/// When creating an HTTP client class with additional functionality, you must
|
||||
/// extend [BaseClient] rather than [Client]. In most cases, you can wrap
|
||||
/// another instance of [Client] and add functionality on top of that. This
|
||||
/// allows all classes implementing [Client] to be mutually composable.
|
||||
abstract class Client {
|
||||
/// Creates a new client.
|
||||
///
|
||||
/// Currently this will create an [IOClient] if `dart:io` is available and
|
||||
/// throw an [UnsupportedError] otherwise. In the future, it will create a
|
||||
/// [BrowserClient] if `dart:html` is available.
|
||||
factory Client() {
|
||||
return clientOverride == null ? new IOClient() : clientOverride();
|
||||
}
|
||||
|
||||
static ClientOverride clientOverride;
|
||||
|
||||
/// Sends an HTTP HEAD request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
Future<Response> head(dynamic url, {Map<String, String> headers});
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
Future<Response> get(dynamic url, {Map<String, String> headers});
|
||||
|
||||
/// Sends an HTTP POST request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>]
|
||||
/// or a [Map<String, String>]. If it's a String, it's encoded using
|
||||
/// [encoding] and used as the body of the request. The content-type of the
|
||||
/// request will default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
Future<Response> post(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding});
|
||||
|
||||
/// Sends an HTTP PUT request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>]
|
||||
/// or a [Map<String, String>]. If it's a String, it's encoded using
|
||||
/// [encoding] and used as the body of the request. The content-type of the
|
||||
/// request will default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
Future<Response> put(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding});
|
||||
|
||||
/// Sends an HTTP PATCH request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>]
|
||||
/// or a [Map<String, String>]. If it's a String, it's encoded using
|
||||
/// [encoding] and used as the body of the request. The content-type of the
|
||||
/// request will default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding});
|
||||
|
||||
/// Sends an HTTP DELETE request with the given headers to the given URL,
|
||||
/// which can be a [Uri] or a [String].
|
||||
///
|
||||
/// For more fine-grained control over the request, use [send] instead.
|
||||
Future<Response> delete(dynamic url, {Map<String, String> headers});
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String], and returns a Future that completes to the
|
||||
/// body of the response as a String.
|
||||
///
|
||||
/// The Future will emit a [ClientException] if the response doesn't have a
|
||||
/// success status code.
|
||||
///
|
||||
/// For more fine-grained control over the request and response, use [send] or
|
||||
/// [get] instead.
|
||||
Future<String> read(dynamic url, {Map<String, String> headers});
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String], and returns a Future that completes to the
|
||||
/// body of the response as a list of bytes.
|
||||
///
|
||||
/// The Future will emit a [ClientException] if the response doesn't have a
|
||||
/// success status code.
|
||||
///
|
||||
/// For more fine-grained control over the request and response, use [send] or
|
||||
/// [get] instead.
|
||||
Future<Uint8List> readBytes(dynamic url, {Map<String, String> headers});
|
||||
|
||||
/// Sends an HTTP request and asynchronously returns the response.
|
||||
Future<StreamedResponse> send(BaseRequest request);
|
||||
|
||||
/// Closes the client and cleans up any resources associated with it. It's
|
||||
/// important to close each client when it's done being used; failing to do so
|
||||
/// can cause the Dart process to hang.
|
||||
void close();
|
||||
}
|
16
packages/flutter/lib/src/http/exception.dart
Normal file
16
packages/flutter/lib/src/http/exception.dart
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// An exception caused by an error in a pkg/http client.
|
||||
class ClientException implements Exception {
|
||||
final String message;
|
||||
|
||||
/// The URL of the HTTP request or response that failed.
|
||||
final Uri uri;
|
||||
|
||||
ClientException(this.message, [this.uri]);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
|
@ -2,42 +2,49 @@
|
|||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
/// A composable, [Future]-based library for making HTTP requests.
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:mojo/core.dart' as mojo;
|
||||
|
||||
import 'mojo_client.dart';
|
||||
import 'client.dart';
|
||||
import 'response.dart';
|
||||
|
||||
export 'base_client.dart';
|
||||
export 'base_request.dart';
|
||||
export 'base_response.dart';
|
||||
export 'byte_stream.dart';
|
||||
export 'client.dart';
|
||||
export 'exception.dart';
|
||||
export 'io_client.dart';
|
||||
export 'multipart_file.dart';
|
||||
export 'multipart_request.dart';
|
||||
export 'request.dart';
|
||||
export 'response.dart';
|
||||
export 'streamed_request.dart';
|
||||
export 'streamed_response.dart';
|
||||
|
||||
/// Sends an HTTP HEAD request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// This automatically initializes a new [Client] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<Response> head(dynamic url) {
|
||||
return _withClient/*<Response>*/((MojoClient client) => client.head(url));
|
||||
}
|
||||
/// the same server, you should use a single [Client] for all of those requests.
|
||||
///
|
||||
/// For more fine-grained control over the request, use [Request] instead.
|
||||
Future<Response> head(dynamic url, {Map<String, String> headers}) =>
|
||||
_withClient((Client client) => client.head(url, headers: headers));
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// This automatically initializes a new [Client] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<Response> get(dynamic url, { Map<String, String> headers }) {
|
||||
return _withClient/*<Response>*/((MojoClient client) => client.get(url, headers: headers));
|
||||
}
|
||||
/// the same server, you should use a single [Client] for all of those requests.
|
||||
///
|
||||
/// For more fine-grained control over the request, use [Request] instead.
|
||||
Future<Response> get(dynamic url, {Map<String, String> headers}) =>
|
||||
_withClient((Client client) => client.get(url, headers: headers));
|
||||
|
||||
/// Sends an HTTP POST request with the given headers and body to the given URL,
|
||||
/// which can be a [Uri] or a [String].
|
||||
|
@ -56,18 +63,12 @@ Future<Response> get(dynamic url, { Map<String, String> headers }) {
|
|||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<Response> post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
|
||||
return _withClient/*<Response>*/((MojoClient client) {
|
||||
return client.post(url, headers: headers, body: body, encoding: encoding);
|
||||
});
|
||||
}
|
||||
/// For more fine-grained control over the request, use [Request] or
|
||||
/// [StreamedRequest] instead.
|
||||
Future<Response> post(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding}) =>
|
||||
_withClient((Client client) => client.post(url,
|
||||
headers: headers, body: body, encoding: encoding));
|
||||
|
||||
/// Sends an HTTP PUT request with the given headers and body to the given URL,
|
||||
/// which can be a [Uri] or a [String].
|
||||
|
@ -86,18 +87,12 @@ Future<Response> post(dynamic url, { Map<String, String> headers, dynamic body,
|
|||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<Response> put(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
|
||||
return _withClient/*<Response>*/((MojoClient client) {
|
||||
return client.put(url, headers: headers, body: body, encoding: encoding);
|
||||
});
|
||||
}
|
||||
/// For more fine-grained control over the request, use [Request] or
|
||||
/// [StreamedRequest] instead.
|
||||
Future<Response> put(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding}) =>
|
||||
_withClient((Client client) => client.put(url,
|
||||
headers: headers, body: body, encoding: encoding));
|
||||
|
||||
/// Sends an HTTP PATCH request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
|
@ -116,78 +111,61 @@ Future<Response> put(dynamic url, { Map<String, String> headers, dynamic body, E
|
|||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<Response> patch(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
|
||||
return _withClient/*<Response>*/((MojoClient client) {
|
||||
return client.patch(url, headers: headers, body: body, encoding: encoding);
|
||||
});
|
||||
}
|
||||
/// For more fine-grained control over the request, use [Request] or
|
||||
/// [StreamedRequest] instead.
|
||||
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body,
|
||||
Encoding encoding}) =>
|
||||
_withClient((Client client) => client.patch(url,
|
||||
headers: headers, body: body, encoding: encoding));
|
||||
|
||||
/// Sends an HTTP DELETE request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// This automatically initializes a new [Client] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<Response> delete(dynamic url, { Map<String, String> headers }) {
|
||||
return _withClient/*<Response>*/((MojoClient client) => client.delete(url, headers: headers));
|
||||
}
|
||||
/// the same server, you should use a single [Client] for all of those requests.
|
||||
///
|
||||
/// For more fine-grained control over the request, use [Request] instead.
|
||||
Future<Response> delete(dynamic url, {Map<String, String> headers}) =>
|
||||
_withClient((Client client) => client.delete(url, headers: headers));
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String], and returns a Future that completes to the body of
|
||||
/// the response as a [String].
|
||||
///
|
||||
/// The Future will resolve with an error in the case of a network error or if
|
||||
/// the response doesn't have a success status code.
|
||||
/// The Future will emit a [ClientException] if the response doesn't have a
|
||||
/// success status code.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// This automatically initializes a new [Client] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<String> read(dynamic url, { Map<String, String> headers }) {
|
||||
return _withClient/*<String>*/((MojoClient client) => client.read(url, headers: headers));
|
||||
}
|
||||
/// the same server, you should use a single [Client] for all of those requests.
|
||||
///
|
||||
/// For more fine-grained control over the request and response, use [Request]
|
||||
/// instead.
|
||||
Future<String> read(dynamic url, {Map<String, String> headers}) =>
|
||||
_withClient((Client client) => client.read(url, headers: headers));
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String], and returns a Future that completes to the body of
|
||||
/// the response as a list of bytes.
|
||||
///
|
||||
/// The Future will resolve with an error in the case of a network error or if
|
||||
/// the response doesn't have a success status code.
|
||||
/// The Future will emit a [ClientException] if the response doesn't have a
|
||||
/// success status code.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// This automatically initializes a new [Client] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<Uint8List> readBytes(dynamic url, { Map<String, String> headers }) {
|
||||
return _withClient/*<Uint8List>*/((MojoClient client) => client.readBytes(url, headers: headers));
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String], and returns a Future that completes to a data pipe
|
||||
/// containing the response bytes.
|
||||
/// the same server, you should use a single [Client] for all of those requests.
|
||||
///
|
||||
/// The Future will resolve with an error in the case of a network error or if
|
||||
/// the response doesn't have a success status code.
|
||||
///
|
||||
/// This automatically initializes a new [MojoClient] and closes that client once
|
||||
/// the request is complete. If you're planning on making multiple requests to
|
||||
/// the same server, you should use a single [MojoClient] for all of those requests,
|
||||
/// so that the same underlying TCP connection can be re-used (via HTTP pipelining).
|
||||
Future<mojo.MojoDataPipeConsumer> readDataPipe(dynamic url, { Map<String, String> headers }) {
|
||||
return _withClient/*<mojo.MojoDataPipeConsumer>*/((MojoClient client) => client.readDataPipe(url, headers: headers));
|
||||
}
|
||||
/// For more fine-grained control over the request and response, use [Request]
|
||||
/// instead.
|
||||
Future<Uint8List> readBytes(dynamic url, {Map<String, String> headers}) =>
|
||||
_withClient((Client client) => client.readBytes(url, headers: headers));
|
||||
|
||||
Future<dynamic/*=T*/> _withClient/*<T>*/(Future<dynamic/*=T*/> fn(MojoClient client)) {
|
||||
return fn(new MojoClient());
|
||||
Future/*<T>*/ _withClient/*<T>*/(Future/*<T>*/ fn(Client client)) async {
|
||||
Client client = new Client();
|
||||
try {
|
||||
return await fn(client);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
|
26
packages/flutter/lib/src/http/io.dart
Normal file
26
packages/flutter/lib/src/http/io.dart
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:io' as io;
|
||||
|
||||
/// Whether `dart:io` is supported on this platform.
|
||||
bool get supported => true;
|
||||
|
||||
/// Asserts that the [name]d `dart:io` feature is supported on this platform.
|
||||
///
|
||||
/// If `dart:io` doesn't work on this platform, this throws an
|
||||
/// [UnsupportedError].
|
||||
void assertSupported(String name) {}
|
||||
|
||||
/// Creates a new `dart:io` HttpClient instance.
|
||||
io.HttpClient newHttpClient() => new io.HttpClient();
|
||||
|
||||
/// Creates a new `dart:io` File instance with the given [path].
|
||||
io.File newFile(String path) => new io.File(path);
|
||||
|
||||
/// Returns whether [error] is a `dart:io` HttpException.
|
||||
bool isHttpException(dynamic error) => error is io.HttpException;
|
||||
|
||||
/// Returns whether [client] is a `dart:io` HttpClient.
|
||||
bool isHttpClient(dynamic client) => client is io.HttpClient;
|
90
packages/flutter/lib/src/http/io_client.dart
Normal file
90
packages/flutter/lib/src/http/io_client.dart
Normal file
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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 'package:async/async.dart';
|
||||
|
||||
import 'base_client.dart';
|
||||
import 'base_request.dart';
|
||||
import 'exception.dart';
|
||||
import 'io.dart' as io;
|
||||
import 'streamed_response.dart';
|
||||
|
||||
/// A `dart:io`-based HTTP client.
|
||||
///
|
||||
/// This is the default client when running on the command line.
|
||||
class IOClient extends BaseClient {
|
||||
/// The underlying `dart:io` HTTP client.
|
||||
dynamic _inner;
|
||||
|
||||
/// Creates a new HTTP client.
|
||||
///
|
||||
/// [innerClient] must be a `dart:io` HTTP client. If it's not passed, a
|
||||
/// default one will be instantiated.
|
||||
IOClient([dynamic innerClient]) {
|
||||
io.assertSupported("IOClient");
|
||||
if (innerClient != null) {
|
||||
// TODO(nweiz): remove this assert when we can type [innerClient]
|
||||
// properly.
|
||||
assert(io.isHttpClient(innerClient));
|
||||
_inner = innerClient;
|
||||
} else {
|
||||
_inner = io.newHttpClient();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an HTTP request and asynchronously returns the response.
|
||||
@override
|
||||
Future<StreamedResponse> send(BaseRequest request) async {
|
||||
dynamic stream = request.finalize();
|
||||
|
||||
try {
|
||||
dynamic ioRequest = await _inner.openUrl(request.method, request.url);
|
||||
|
||||
ioRequest
|
||||
..followRedirects = request.followRedirects
|
||||
..maxRedirects = request.maxRedirects
|
||||
..contentLength = request.contentLength == null
|
||||
? -1
|
||||
: request.contentLength
|
||||
..persistentConnection = request.persistentConnection;
|
||||
request.headers.forEach((String name, String value) {
|
||||
ioRequest.headers.set(name, value);
|
||||
});
|
||||
|
||||
dynamic response = await stream.pipe(
|
||||
DelegatingStreamConsumer.typed(ioRequest));
|
||||
Map<String, dynamic> headers = <String, dynamic>{};
|
||||
response.headers.forEach((String key, dynamic values) {
|
||||
headers[key] = values.join(',');
|
||||
});
|
||||
|
||||
return new StreamedResponse(
|
||||
DelegatingStream.typed/*<List<int>>*/(response).handleError((dynamic error) =>
|
||||
throw new ClientException(error.message, error.uri),
|
||||
test: (dynamic error) => io.isHttpException(error)),
|
||||
response.statusCode,
|
||||
contentLength: response.contentLength == -1
|
||||
? null
|
||||
: response.contentLength,
|
||||
request: request,
|
||||
headers: headers,
|
||||
isRedirect: response.isRedirect,
|
||||
persistentConnection: response.persistentConnection,
|
||||
reasonPhrase: response.reasonPhrase);
|
||||
} catch (error) {
|
||||
if (!io.isHttpException(error)) rethrow;
|
||||
throw new ClientException(error.message, error.uri);
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the client. This terminates all active connections. If a client
|
||||
/// remains unclosed, the Dart process may not terminate.
|
||||
@override
|
||||
void close() {
|
||||
if (_inner != null) _inner.close(force: true);
|
||||
_inner = null;
|
||||
}
|
||||
}
|
88
packages/flutter/lib/src/http/mock_client.dart
Normal file
88
packages/flutter/lib/src/http/mock_client.dart
Normal file
|
@ -0,0 +1,88 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:typed_data';
|
||||
|
||||
import 'base_client.dart';
|
||||
import 'base_request.dart';
|
||||
import 'byte_stream.dart';
|
||||
import 'request.dart';
|
||||
import 'response.dart';
|
||||
import 'streamed_response.dart';
|
||||
|
||||
// TODO(nweiz): once Dart has some sort of Rack- or WSGI-like standard for
|
||||
// server APIs, MockClient should conform to it.
|
||||
|
||||
/// A mock HTTP client designed for use when testing code that uses
|
||||
/// [BaseClient]. This client allows you to define a handler callback for all
|
||||
/// requests that are made through it so that you can mock a server without
|
||||
/// having to send real HTTP requests.
|
||||
class MockClient extends BaseClient {
|
||||
/// The handler for receiving [StreamedRequest]s and sending
|
||||
/// [StreamedResponse]s.
|
||||
final MockClientStreamHandler _handler;
|
||||
|
||||
/// Creates a [MockClient] with a handler that receives [Request]s and sends
|
||||
/// [Response]s.
|
||||
MockClient(MockClientHandler fn)
|
||||
: this._((Request baseRequest, ByteStream bodyStream) {
|
||||
return bodyStream.toBytes().then((Uint8List bodyBytes) {
|
||||
Request request = new Request(baseRequest.method, baseRequest.url)
|
||||
..persistentConnection = baseRequest.persistentConnection
|
||||
..followRedirects = baseRequest.followRedirects
|
||||
..maxRedirects = baseRequest.maxRedirects
|
||||
..headers.addAll(baseRequest.headers)
|
||||
..bodyBytes = bodyBytes
|
||||
..finalize();
|
||||
|
||||
return fn(request);
|
||||
}).then((Response response) {
|
||||
return new StreamedResponse(
|
||||
new ByteStream.fromBytes(response.bodyBytes),
|
||||
response.statusCode,
|
||||
contentLength: response.contentLength,
|
||||
request: baseRequest,
|
||||
headers: response.headers,
|
||||
isRedirect: response.isRedirect,
|
||||
persistentConnection: response.persistentConnection,
|
||||
reasonPhrase: response.reasonPhrase);
|
||||
});
|
||||
});
|
||||
|
||||
MockClient._(this._handler);
|
||||
|
||||
/// Creates a [MockClient] with a handler that receives [StreamedRequest]s and
|
||||
/// sends [StreamedResponse]s.
|
||||
MockClient.streaming(MockClientStreamHandler fn)
|
||||
: this._((Request request, ByteStream bodyStream) {
|
||||
return fn(request, bodyStream).then((StreamedResponse response) {
|
||||
return new StreamedResponse(
|
||||
response.stream,
|
||||
response.statusCode,
|
||||
contentLength: response.contentLength,
|
||||
request: request,
|
||||
headers: response.headers,
|
||||
isRedirect: response.isRedirect,
|
||||
persistentConnection: response.persistentConnection,
|
||||
reasonPhrase: response.reasonPhrase);
|
||||
});
|
||||
});
|
||||
|
||||
/// Sends a request.
|
||||
@override
|
||||
Future<StreamedResponse> send(BaseRequest request) async {
|
||||
ByteStream bodyStream = request.finalize();
|
||||
return await _handler(request, bodyStream);
|
||||
}
|
||||
}
|
||||
|
||||
/// A handler function that receives [StreamedRequest]s and sends
|
||||
/// [StreamedResponse]s. Note that [request] will be finalized.
|
||||
typedef Future<StreamedResponse> MockClientStreamHandler(
|
||||
BaseRequest request, ByteStream bodyStream);
|
||||
|
||||
/// A handler function that receives [Request]s and sends [Response]s. Note that
|
||||
/// [request] will be finalized.
|
||||
typedef Future<Response> MockClientHandler(Request request);
|
|
@ -1,260 +0,0 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mojo/core.dart' as mojo;
|
||||
import 'package:mojo/mojo/http_header.mojom.dart' as mojom;
|
||||
import 'package:mojo/mojo/url_request.mojom.dart' as mojom;
|
||||
import 'package:mojo/mojo/url_response.mojom.dart' as mojom;
|
||||
import 'package:mojo_services/mojo/network_service.mojom.dart' as mojom;
|
||||
import 'package:mojo_services/mojo/url_loader.mojom.dart' as mojom;
|
||||
|
||||
import 'response.dart';
|
||||
|
||||
/// A `mojo`-based HTTP client.
|
||||
class MojoClient {
|
||||
|
||||
/// Sends an HTTP HEAD request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
Future<Response> head(dynamic url, { Map<String, String> headers }) {
|
||||
return _createResponse(_send("HEAD", url, headers));
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
Future<Response> get(dynamic url, { Map<String, String> headers }) {
|
||||
return _createResponse(_send("GET", url, headers));
|
||||
}
|
||||
|
||||
/// Sends an HTTP POST request with the given headers and body to the given URL,
|
||||
/// which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>] or
|
||||
/// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
|
||||
/// used as the body of the request. The content-type of the request will
|
||||
/// default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
Future<Response> post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
|
||||
return _createResponse(_send("POST", url, headers, body, encoding));
|
||||
}
|
||||
|
||||
/// Sends an HTTP PUT request with the given headers and body to the given URL,
|
||||
/// which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>] or
|
||||
/// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
|
||||
/// used as the body of the request. The content-type of the request will
|
||||
/// default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
Future<Response> put(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
|
||||
return _createResponse(_send("PUT", url, headers, body, encoding));
|
||||
}
|
||||
|
||||
/// Sends an HTTP PATCH request with the given headers and body to the given
|
||||
/// URL, which can be a [Uri] or a [String].
|
||||
///
|
||||
/// [body] sets the body of the request. It can be a [String], a [List<int>] or
|
||||
/// a [Map<String, String>]. If it's a String, it's encoded using [encoding] and
|
||||
/// used as the body of the request. The content-type of the request will
|
||||
/// default to "text/plain".
|
||||
///
|
||||
/// If [body] is a List, it's used as a list of bytes for the body of the
|
||||
/// request.
|
||||
///
|
||||
/// If [body] is a Map, it's encoded as form fields using [encoding]. The
|
||||
/// content-type of the request will be set to
|
||||
/// `"application/x-www-form-urlencoded"`; this cannot be overridden.
|
||||
///
|
||||
/// [encoding] defaults to [UTF8].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
Future<Response> patch(dynamic url, {Map<String, String> headers, dynamic body, Encoding encoding: UTF8 }) {
|
||||
return _createResponse(_send("PATCH", url, headers, body, encoding));
|
||||
}
|
||||
|
||||
/// Sends an HTTP DELETE request with the given headers to the given URL, which
|
||||
/// can be a [Uri] or a [String].
|
||||
///
|
||||
/// Network errors will be turned into [Response] object with a non-null
|
||||
/// [Response.error] field.
|
||||
Future<Response> delete(dynamic url, { Map<String, String> headers }) {
|
||||
return _createResponse(_send("DELETE", url, headers));
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String], and returns a Future that completes to the body of
|
||||
/// the response as a [String].
|
||||
///
|
||||
/// The Future will resolve with an error in the case of a network error or if
|
||||
/// the response doesn't have a success status code.
|
||||
Future<String> read(dynamic url, { Map<String, String> headers }) {
|
||||
return get(url, headers: headers).then((Response response) {
|
||||
_requireSuccess(url, response.statusCode, response.error);
|
||||
return response.body;
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String], and returns a Future that completes to the body of
|
||||
/// the response as a list of bytes.
|
||||
///
|
||||
/// The Future will resolve with an error in the case of a network error or if
|
||||
/// the response doesn't have a success status code.
|
||||
Future<Uint8List> readBytes(dynamic url, { Map<String, String> headers }) {
|
||||
return get(url, headers: headers).then((Response response) {
|
||||
_requireSuccess(url, response.statusCode, response.error);
|
||||
return response.bodyBytes;
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends an HTTP GET request with the given headers to the given URL, which can
|
||||
/// be a [Uri] or a [String], and returns a Future that completes to the body of
|
||||
/// the response as a [mojo.MojoDataPipeConsumer].
|
||||
///
|
||||
/// The Future will resolve with an error in the case of a network error or if
|
||||
/// the response doesn't have a success status code.
|
||||
Future<mojo.MojoDataPipeConsumer> readDataPipe(dynamic url, { Map<String, String> headers }) {
|
||||
return _send('GET', url, headers).then((mojom.UrlResponse response) {
|
||||
_requireSuccess(url, response.statusCode, response.statusLine);
|
||||
return response.body;
|
||||
});
|
||||
}
|
||||
|
||||
mojom.UrlRequest _prepareRequest(String method, dynamic url, Map<String, String> headers, [dynamic body, Encoding encoding = UTF8]) {
|
||||
List<mojom.HttpHeader> mojoHeaders = <mojom.HttpHeader>[];
|
||||
headers?.forEach((String name, String value) {
|
||||
mojom.HttpHeader header = new mojom.HttpHeader()
|
||||
..name = name
|
||||
..value = value;
|
||||
mojoHeaders.add(header);
|
||||
});
|
||||
mojom.UrlRequest request = new mojom.UrlRequest()
|
||||
..url = url.toString()
|
||||
..headers = mojoHeaders
|
||||
..method = method
|
||||
..autoFollowRedirects = true;
|
||||
if (body != null) {
|
||||
mojo.MojoDataPipe pipe = new mojo.MojoDataPipe();
|
||||
request.body = <mojo.MojoDataPipeConsumer>[pipe.consumer];
|
||||
Uint8List encodedBody = encoding.encode(body);
|
||||
ByteData data = new ByteData.view(encodedBody.buffer);
|
||||
mojo.DataPipeFiller.fillHandle(pipe.producer, data);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
Future<mojom.UrlResponse> _send(String method, dynamic url, Map<String, String> headers, [dynamic body, Encoding encoding = UTF8]) {
|
||||
Completer<mojom.UrlResponse> completer = new Completer<mojom.UrlResponse>();
|
||||
mojom.UrlLoaderProxy loader = new mojom.UrlLoaderProxy.unbound();
|
||||
networkService.createUrlLoader(loader);
|
||||
mojom.UrlRequest request = _prepareRequest(method, url, headers, body, encoding);
|
||||
loader.start(request, (mojom.UrlResponse response) async {
|
||||
loader.close();
|
||||
try {
|
||||
if (response.error != null)
|
||||
throw new Exception('Request to "$url" failed with error ${response.error.code}.\n${response.error.description}');
|
||||
if (!response.body.handle.isValid)
|
||||
throw new Exception('Response body does not have a valid handle, but no error was reported.\n${response.body}');
|
||||
completer.complete(response);
|
||||
} catch (e, stack) {
|
||||
FlutterError.reportError(new FlutterErrorDetails(
|
||||
exception: e,
|
||||
stack: stack,
|
||||
library: 'networking HTTP library',
|
||||
context: 'while interacting with the Mojo network library',
|
||||
silent: true
|
||||
));
|
||||
completer.completeError(e);
|
||||
}
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<Response> _createResponse(Future<mojom.UrlResponse> futureResponse) async {
|
||||
try {
|
||||
mojom.UrlResponse response = await futureResponse;
|
||||
try {
|
||||
ByteData data = await mojo.DataPipeDrainer.drainHandle(response.body);
|
||||
Uint8List bodyBytes = new Uint8List.view(data.buffer);
|
||||
Map<String, String> headers = <String, String>{};
|
||||
if (response.headers != null) {
|
||||
for (mojom.HttpHeader header in response.headers) {
|
||||
String headerName = header.name.toLowerCase();
|
||||
String existingValue = headers[headerName];
|
||||
headers[headerName] = existingValue != null ? '$existingValue, ${header.value}' : header.value;
|
||||
}
|
||||
}
|
||||
return new Response.bytes(bodyBytes, response.statusCode, headers: headers, error: response.statusLine);
|
||||
} catch (e, stack) {
|
||||
FlutterError.reportError(new FlutterErrorDetails(
|
||||
exception: e,
|
||||
stack: stack,
|
||||
library: 'networking HTTP library',
|
||||
context: 'while interacting with the Mojo network library',
|
||||
silent: true
|
||||
));
|
||||
rethrow;
|
||||
}
|
||||
} catch (e) {
|
||||
return new Response.bytes(null, 500, error: e);
|
||||
}
|
||||
}
|
||||
|
||||
void _requireSuccess(dynamic url, int statusCode, dynamic error) {
|
||||
if (error is Exception)
|
||||
throw error;
|
||||
if (statusCode >= 400) {
|
||||
String extra;
|
||||
if (error is String && error != '') {
|
||||
extra = '\nServer response: "$error"';
|
||||
} else if (error != null) {
|
||||
extra = '\n$error';
|
||||
} else {
|
||||
extra = '';
|
||||
}
|
||||
throw new Exception('Request to "$url" failed with status $statusCode.$extra');
|
||||
}
|
||||
}
|
||||
|
||||
static mojom.NetworkServiceProxy _initNetworkService() {
|
||||
return shell.connectToApplicationService('mojo:authenticated_network_service', mojom.NetworkService.connectToService);
|
||||
}
|
||||
|
||||
/// A handle to the [NetworkService] object used by [MojoClient].
|
||||
static final mojom.NetworkServiceProxy networkService = _initNetworkService();
|
||||
}
|
111
packages/flutter/lib/src/http/multipart_file.dart
Normal file
111
packages/flutter/lib/src/http/multipart_file.dart
Normal file
|
@ -0,0 +1,111 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
import 'byte_stream.dart';
|
||||
import 'io.dart' as io;
|
||||
import 'utils.dart';
|
||||
|
||||
/// A file to be uploaded as part of a [MultipartRequest]. This doesn't need to
|
||||
/// correspond to a physical file.
|
||||
class MultipartFile {
|
||||
/// The name of the form field for the file.
|
||||
final String field;
|
||||
|
||||
/// The size of the file in bytes. This must be known in advance, even if this
|
||||
/// file is created from a [ByteStream].
|
||||
final int length;
|
||||
|
||||
/// The basename of the file. May be null.
|
||||
final String filename;
|
||||
|
||||
/// The content-type of the file. Defaults to `application/octet-stream`.
|
||||
final MediaType contentType;
|
||||
|
||||
/// The stream that will emit the file's contents.
|
||||
final ByteStream _stream;
|
||||
|
||||
/// Creates a new [MultipartFile] from a chunked [Stream] of bytes. The length
|
||||
/// of the file in bytes must be known in advance. If it's not, read the data
|
||||
/// from the stream and use [MultipartFile.fromBytes] instead.
|
||||
///
|
||||
/// [contentType] currently defaults to `application/octet-stream`, but in the
|
||||
/// future may be inferred from [filename].
|
||||
MultipartFile(this.field, Stream<List<int>> stream, this.length,
|
||||
{this.filename, MediaType contentType})
|
||||
: this._stream = toByteStream(stream),
|
||||
this.contentType = contentType != null ? contentType :
|
||||
new MediaType("application", "octet-stream");
|
||||
|
||||
/// Creates a new [MultipartFile] from a byte array.
|
||||
///
|
||||
/// [contentType] currently defaults to `application/octet-stream`, but in the
|
||||
/// future may be inferred from [filename].
|
||||
factory MultipartFile.fromBytes(String field, List<int> value,
|
||||
{String filename, MediaType contentType}) {
|
||||
ByteStream stream = new ByteStream.fromBytes(value);
|
||||
return new MultipartFile(field, stream, value.length,
|
||||
filename: filename,
|
||||
contentType: contentType);
|
||||
}
|
||||
|
||||
/// Creates a new [MultipartFile] from a string.
|
||||
///
|
||||
/// The encoding to use when translating [value] into bytes is taken from
|
||||
/// [contentType] if it has a charset set. Otherwise, it defaults to UTF-8.
|
||||
/// [contentType] currently defaults to `text/plain; charset=utf-8`, but in
|
||||
/// the future may be inferred from [filename].
|
||||
factory MultipartFile.fromString(String field, String value,
|
||||
{String filename, MediaType contentType}) {
|
||||
contentType = contentType == null ? new MediaType("text", "plain")
|
||||
: contentType;
|
||||
Encoding encoding = encodingForCharset(contentType.parameters['charset'], UTF8);
|
||||
contentType = contentType.change(parameters: <String, String>{'charset': encoding.name});
|
||||
|
||||
return new MultipartFile.fromBytes(field, encoding.encode(value),
|
||||
filename: filename,
|
||||
contentType: contentType);
|
||||
}
|
||||
|
||||
/// Whether [finalize] has been called.
|
||||
bool get isFinalized => _isFinalized;
|
||||
bool _isFinalized = false;
|
||||
|
||||
// TODO(nweiz): Infer the content-type from the filename.
|
||||
/// Creates a new [MultipartFile] from a path to a file on disk.
|
||||
///
|
||||
/// [filename] defaults to the basename of [filePath]. [contentType] currently
|
||||
/// defaults to `application/octet-stream`, but in the future may be inferred
|
||||
/// from [filename].
|
||||
///
|
||||
/// This can only be used in an environment that supports "dart:io".
|
||||
static Future<MultipartFile> fromPath(String field, String filePath,
|
||||
{String filename, MediaType contentType}) async {
|
||||
io.assertSupported("MultipartFile.fromPath");
|
||||
if (filename == null) filename = path.basename(filePath);
|
||||
dynamic file = io.newFile(filePath);
|
||||
int length = await file.length();
|
||||
ByteStream stream = new ByteStream(DelegatingStream.typed(file.openRead()));
|
||||
return new MultipartFile(field, stream, length,
|
||||
filename: filename,
|
||||
contentType: contentType);
|
||||
}
|
||||
|
||||
// Finalizes the file in preparation for it being sent as part of a
|
||||
// [MultipartRequest]. This returns a [ByteStream] that should emit the body
|
||||
// of the file. The stream may be closed to indicate an empty file.
|
||||
ByteStream finalize() {
|
||||
if (isFinalized) {
|
||||
throw new StateError("Can't finalize a finalized MultipartFile.");
|
||||
}
|
||||
_isFinalized = true;
|
||||
return _stream;
|
||||
}
|
||||
}
|
177
packages/flutter/lib/src/http/multipart_request.dart
Normal file
177
packages/flutter/lib/src/http/multipart_request.dart
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'base_request.dart';
|
||||
import 'byte_stream.dart';
|
||||
import 'multipart_file.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
final RegExp _newlineRegExp = new RegExp(r"\r\n|\r|\n");
|
||||
|
||||
/// A `multipart/form-data` request. Such a request has both string [fields],
|
||||
/// which function as normal form fields, and (potentially streamed) binary
|
||||
/// [files].
|
||||
///
|
||||
/// This request automatically sets the Content-Type header to
|
||||
/// `multipart/form-data`. This value will override any value set by the user.
|
||||
///
|
||||
/// var uri = Uri.parse("http://pub.dartlang.org/packages/create");
|
||||
/// var request = new http.MultipartRequest("POST", url);
|
||||
/// request.fields['user'] = 'nweiz@google.com';
|
||||
/// request.files.add(new http.MultipartFile.fromFile(
|
||||
/// 'package',
|
||||
/// new File('build/package.tar.gz'),
|
||||
/// contentType: new MediaType('application', 'x-tar'));
|
||||
/// request.send().then((response) {
|
||||
/// if (response.statusCode == 200) print("Uploaded!");
|
||||
/// });
|
||||
class MultipartRequest extends BaseRequest {
|
||||
/// The total length of the multipart boundaries used when building the
|
||||
/// request body. According to http://tools.ietf.org/html/rfc1341.html, this
|
||||
/// can't be longer than 70.
|
||||
static const int _BOUNDARY_LENGTH = 70;
|
||||
|
||||
static final Random _random = new Random();
|
||||
|
||||
/// The form fields to send for this request.
|
||||
final Map<String, String> fields;
|
||||
|
||||
/// The private version of [files].
|
||||
final List<MultipartFile> _files;
|
||||
|
||||
/// Creates a new [MultipartRequest].
|
||||
MultipartRequest(String method, Uri url)
|
||||
: fields = <String, String>{},
|
||||
_files = <MultipartFile>[],
|
||||
super(method, url);
|
||||
|
||||
/// The list of files to upload for this request.
|
||||
List<MultipartFile> get files => _files;
|
||||
|
||||
/// The total length of the request body, in bytes. This is calculated from
|
||||
/// [fields] and [files] and cannot be set manually.
|
||||
@override
|
||||
int get contentLength {
|
||||
int length = 0;
|
||||
|
||||
fields.forEach((String name, String value) {
|
||||
length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
|
||||
UTF8.encode(_headerForField(name, value)).length +
|
||||
UTF8.encode(value).length + "\r\n".length;
|
||||
});
|
||||
|
||||
for (MultipartFile file in _files) {
|
||||
length += "--".length + _BOUNDARY_LENGTH + "\r\n".length +
|
||||
UTF8.encode(_headerForFile(file)).length +
|
||||
file.length + "\r\n".length;
|
||||
}
|
||||
|
||||
return length + "--".length + _BOUNDARY_LENGTH + "--\r\n".length;
|
||||
}
|
||||
|
||||
@override
|
||||
set contentLength(int value) {
|
||||
throw new UnsupportedError("Cannot set the contentLength property of "
|
||||
"multipart requests.");
|
||||
}
|
||||
|
||||
/// Freezes all mutable fields and returns a single-subscription [ByteStream]
|
||||
/// that will emit the request body.
|
||||
@override
|
||||
ByteStream finalize() {
|
||||
// TODO(nweiz): freeze fields and files
|
||||
String boundary = _boundaryString();
|
||||
headers['content-type'] = 'multipart/form-data; boundary="$boundary"';
|
||||
super.finalize();
|
||||
|
||||
StreamController<List<int>> controller = new StreamController<List<int>>(sync: true);
|
||||
|
||||
void writeAscii(String string) {
|
||||
controller.add(UTF8.encode(string));
|
||||
}
|
||||
|
||||
dynamic writeUtf8(String string) => controller.add(UTF8.encode(string));
|
||||
dynamic writeLine() => controller.add(<int>[13, 10]); // \r\n
|
||||
|
||||
fields.forEach((String name, String value) {
|
||||
writeAscii('--$boundary\r\n');
|
||||
writeAscii(_headerForField(name, value));
|
||||
writeUtf8(value);
|
||||
writeLine();
|
||||
});
|
||||
|
||||
Future.forEach(_files, (MultipartFile file) {
|
||||
writeAscii('--$boundary\r\n');
|
||||
writeAscii(_headerForFile(file));
|
||||
return writeStreamToSink(file.finalize(), controller)
|
||||
.then((_) => writeLine());
|
||||
}).then((_) {
|
||||
// TODO(nweiz): pass any errors propagated through this future on to
|
||||
// the stream. See issue 3657.
|
||||
writeAscii('--$boundary--\r\n');
|
||||
controller.close();
|
||||
});
|
||||
|
||||
return new ByteStream(controller.stream);
|
||||
}
|
||||
|
||||
/// All character codes that are valid in multipart boundaries. From
|
||||
/// http://tools.ietf.org/html/rfc2046#section-5.1.1.
|
||||
static const List<int> _BOUNDARY_CHARACTERS = const <int>[
|
||||
39, 40, 41, 43, 95, 44, 45, 46, 47, 58, 61, 63, 48, 49, 50, 51, 52, 53, 54,
|
||||
55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80,
|
||||
81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103,
|
||||
104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118,
|
||||
119, 120, 121, 122
|
||||
];
|
||||
|
||||
/// Returns the header string for a field. The return value is guaranteed to
|
||||
/// contain only ASCII characters.
|
||||
String _headerForField(String name, String value) {
|
||||
String header =
|
||||
'content-disposition: form-data; name="${_browserEncode(name)}"';
|
||||
if (!isPlainAscii(value)) {
|
||||
header = '$header\r\n'
|
||||
'content-type: text/plain; charset=utf-8\r\n'
|
||||
'content-transfer-encoding: binary';
|
||||
}
|
||||
return '$header\r\n\r\n';
|
||||
}
|
||||
|
||||
/// Returns the header string for a file. The return value is guaranteed to
|
||||
/// contain only ASCII characters.
|
||||
String _headerForFile(MultipartFile file) {
|
||||
String header = 'content-type: ${file.contentType}\r\n'
|
||||
'content-disposition: form-data; name="${_browserEncode(file.field)}"';
|
||||
|
||||
if (file.filename != null) {
|
||||
header = '$header; filename="${_browserEncode(file.filename)}"';
|
||||
}
|
||||
return '$header\r\n\r\n';
|
||||
}
|
||||
|
||||
/// Encode [value] in the same way browsers do.
|
||||
String _browserEncode(String value) {
|
||||
// http://tools.ietf.org/html/rfc2388 mandates some complex encodings for
|
||||
// field names and file names, but in practice user agents seem not to
|
||||
// follow this at all. Instead, they URL-encode `\r`, `\n`, and `\r\n` as
|
||||
// `\r\n`; URL-encode `"`; and do nothing else (even for `%` or non-ASCII
|
||||
// characters). We follow their behavior.
|
||||
return value.replaceAll(_newlineRegExp, "%0D%0A").replaceAll('"', "%22");
|
||||
}
|
||||
|
||||
/// Returns a randomly-generated multipart boundary string
|
||||
String _boundaryString() {
|
||||
String prefix = "dart-http-boundary-";
|
||||
List<int> list = new List<int>.generate(_BOUNDARY_LENGTH - prefix.length,
|
||||
(int index) =>
|
||||
_BOUNDARY_CHARACTERS[_random.nextInt(_BOUNDARY_CHARACTERS.length)],
|
||||
growable: false);
|
||||
return "$prefix${new String.fromCharCodes(list)}";
|
||||
}
|
||||
}
|
164
packages/flutter/lib/src/http/request.dart
Normal file
164
packages/flutter/lib/src/http/request.dart
Normal file
|
@ -0,0 +1,164 @@
|
|||
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import 'base_request.dart';
|
||||
import 'byte_stream.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// An HTTP request where the entire request body is known in advance.
|
||||
class Request extends BaseRequest {
|
||||
/// Creates a new HTTP request.
|
||||
Request(String method, Uri url)
|
||||
: _defaultEncoding = UTF8,
|
||||
_bodyBytes = new Uint8List(0),
|
||||
super(method, url);
|
||||
|
||||
/// The size of the request body, in bytes. This is calculated from
|
||||
/// [bodyBytes].
|
||||
///
|
||||
/// The content length cannot be set for [Request], since it's automatically
|
||||
/// calculated from [bodyBytes].
|
||||
@override
|
||||
int get contentLength => bodyBytes.length;
|
||||
|
||||
@override
|
||||
set contentLength(int value) {
|
||||
throw new UnsupportedError("Cannot set the contentLength property of "
|
||||
"non-streaming Request objects.");
|
||||
}
|
||||
|
||||
/// The default encoding to use when converting between [bodyBytes] and
|
||||
/// [body]. This is only used if [encoding] hasn't been manually set and if
|
||||
/// the content-type header has no encoding information.
|
||||
Encoding _defaultEncoding;
|
||||
|
||||
/// The encoding used for the request. This encoding is used when converting
|
||||
/// between [bodyBytes] and [body].
|
||||
///
|
||||
/// If the request has a `Content-Type` header and that header has a `charset`
|
||||
/// parameter, that parameter's value is used as the encoding. Otherwise, if
|
||||
/// [encoding] has been set manually, that encoding is used. If that hasn't
|
||||
/// been set either, this defaults to [UTF8].
|
||||
///
|
||||
/// If the `charset` parameter's value is not a known [Encoding], reading this
|
||||
/// will throw a [FormatException].
|
||||
///
|
||||
/// If the request has a `Content-Type` header, setting this will set the
|
||||
/// charset parameter on that header.
|
||||
Encoding get encoding {
|
||||
if (_contentType == null ||
|
||||
!_contentType.parameters.containsKey('charset')) {
|
||||
return _defaultEncoding;
|
||||
}
|
||||
return requiredEncodingForCharset(_contentType.parameters['charset']);
|
||||
}
|
||||
|
||||
set encoding(Encoding value) {
|
||||
_checkFinalized();
|
||||
_defaultEncoding = value;
|
||||
MediaType contentType = _contentType;
|
||||
if (contentType == null) return;
|
||||
_contentType = contentType.change(parameters: <String, String>{'charset': value.name});
|
||||
}
|
||||
|
||||
// TODO(nweiz): make this return a read-only view
|
||||
/// The bytes comprising the body of the request. This is converted to and
|
||||
/// from [body] using [encoding].
|
||||
///
|
||||
/// This list should only be set, not be modified in place.
|
||||
Uint8List get bodyBytes => _bodyBytes;
|
||||
Uint8List _bodyBytes;
|
||||
|
||||
set bodyBytes(List<int> value) {
|
||||
_checkFinalized();
|
||||
_bodyBytes = toUint8List(value);
|
||||
}
|
||||
|
||||
/// The body of the request as a string. This is converted to and from
|
||||
/// [bodyBytes] using [encoding].
|
||||
///
|
||||
/// When this is set, if the request does not yet have a `Content-Type`
|
||||
/// header, one will be added with the type `text/plain`. Then the `charset`
|
||||
/// parameter of the `Content-Type` header (whether new or pre-existing) will
|
||||
/// be set to [encoding] if it wasn't already set.
|
||||
String get body => encoding.decode(bodyBytes);
|
||||
|
||||
set body(String value) {
|
||||
bodyBytes = encoding.encode(value);
|
||||
MediaType contentType = _contentType;
|
||||
if (contentType == null) {
|
||||
_contentType = new MediaType("text", "plain", <String, String>{'charset': encoding.name});
|
||||
} else if (!contentType.parameters.containsKey('charset')) {
|
||||
_contentType = contentType.change(parameters: <String, String>{'charset': encoding.name});
|
||||
}
|
||||
}
|
||||
|
||||
/// The form-encoded fields in the body of the request as a map from field
|
||||
/// names to values. The form-encoded body is converted to and from
|
||||
/// [bodyBytes] using [encoding] (in the same way as [body]).
|
||||
///
|
||||
/// If the request doesn't have a `Content-Type` header of
|
||||
/// `application/x-www-form-urlencoded`, reading this will throw a
|
||||
/// [StateError].
|
||||
///
|
||||
/// If the request has a `Content-Type` header with a type other than
|
||||
/// `application/x-www-form-urlencoded`, setting this will throw a
|
||||
/// [StateError]. Otherwise, the content type will be set to
|
||||
/// `application/x-www-form-urlencoded`.
|
||||
///
|
||||
/// This map should only be set, not modified in place.
|
||||
Map<String, String> get bodyFields {
|
||||
MediaType contentType = _contentType;
|
||||
if (contentType == null ||
|
||||
contentType.mimeType != "application/x-www-form-urlencoded") {
|
||||
throw new StateError('Cannot access the body fields of a Request without '
|
||||
'content-type "application/x-www-form-urlencoded".');
|
||||
}
|
||||
|
||||
return Uri.splitQueryString(body, encoding: encoding);
|
||||
}
|
||||
|
||||
set bodyFields(Map<String, String> fields) {
|
||||
MediaType contentType = _contentType;
|
||||
if (contentType == null) {
|
||||
_contentType = new MediaType("application", "x-www-form-urlencoded");
|
||||
} else if (contentType.mimeType != "application/x-www-form-urlencoded") {
|
||||
throw new StateError('Cannot set the body fields of a Request with '
|
||||
'content-type "${contentType.mimeType}".');
|
||||
}
|
||||
|
||||
this.body = mapToQuery(fields, encoding: encoding);
|
||||
}
|
||||
|
||||
/// Freezes all mutable fields and returns a single-subscription [ByteStream]
|
||||
/// containing the request body.
|
||||
@override
|
||||
ByteStream finalize() {
|
||||
super.finalize();
|
||||
return new ByteStream.fromBytes(bodyBytes);
|
||||
}
|
||||
|
||||
/// The `Content-Type` header of the request (if it exists) as a
|
||||
/// [MediaType].
|
||||
MediaType get _contentType {
|
||||
String contentType = headers['content-type'];
|
||||
if (contentType == null) return null;
|
||||
return new MediaType.parse(contentType);
|
||||
}
|
||||
|
||||
set _contentType(MediaType value) {
|
||||
headers['content-type'] = value.toString();
|
||||
}
|
||||
|
||||
/// Throw an error if this request has been finalized.
|
||||
void _checkFinalized() {
|
||||
if (!finalized) return;
|
||||
throw new StateError("Can't modify a finalized Request.");
|
||||
}
|
||||
}
|
|
@ -2,103 +2,94 @@
|
|||
// for details. 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:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
import 'base_request.dart';
|
||||
import 'base_response.dart';
|
||||
import 'streamed_response.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// An HTTP response where the entire response body is known in advance.
|
||||
class Response {
|
||||
/// Creates a [Response] object with the given fields.
|
||||
///
|
||||
/// If [bodyBytes] is non-null, it is used to populate [body].
|
||||
Response.bytes(this.bodyBytes, this.statusCode, {
|
||||
this.headers: const <String, String>{},
|
||||
this.error
|
||||
});
|
||||
|
||||
/// The result of decoding [bodyBytes] using the character encoding declared
|
||||
/// in the headers.
|
||||
///
|
||||
/// Defaults to [LATIN1] (ISO 8859-1).
|
||||
///
|
||||
/// If [bodyBytes] is null, this will also be null.
|
||||
String get body => bodyBytes == null ? null : _encodingForHeaders(headers).decode(bodyBytes);
|
||||
|
||||
/// The raw byte stream.
|
||||
class Response extends BaseResponse {
|
||||
/// The bytes comprising the body of this response.
|
||||
final Uint8List bodyBytes;
|
||||
|
||||
/// The HTTP result code.
|
||||
/// Creates a new HTTP response with a string body.
|
||||
Response(
|
||||
String body,
|
||||
int statusCode,
|
||||
{BaseRequest request,
|
||||
Map<String, String> headers: const <String, String>{},
|
||||
bool isRedirect: false,
|
||||
bool persistentConnection: true,
|
||||
String reasonPhrase})
|
||||
: this.bytes(
|
||||
_encodingForHeaders(headers).encode(body),
|
||||
statusCode,
|
||||
request: request,
|
||||
headers: headers,
|
||||
isRedirect: isRedirect,
|
||||
persistentConnection: persistentConnection,
|
||||
reasonPhrase: reasonPhrase);
|
||||
|
||||
/// Create a new HTTP response with a byte array body.
|
||||
Response.bytes(
|
||||
List<int> bodyBytes,
|
||||
int statusCode,
|
||||
{BaseRequest request,
|
||||
Map<String, String> headers: const <String, String>{},
|
||||
bool isRedirect: false,
|
||||
bool persistentConnection: true,
|
||||
String reasonPhrase})
|
||||
: bodyBytes = toUint8List(bodyBytes),
|
||||
super(
|
||||
statusCode,
|
||||
contentLength: bodyBytes.length,
|
||||
request: request,
|
||||
headers: headers,
|
||||
isRedirect: isRedirect,
|
||||
persistentConnection: persistentConnection,
|
||||
reasonPhrase: reasonPhrase);
|
||||
|
||||
/// The body of the response as a string. This is converted from [bodyBytes]
|
||||
/// using the `charset` parameter of the `Content-Type` header field, if
|
||||
/// available. If it's unavailable or if the encoding name is unknown,
|
||||
/// [LATIN1] is used by default, as per [RFC 2616][].
|
||||
///
|
||||
/// The code 500 is used when no status code could be obtained from the host.
|
||||
final int statusCode;
|
||||
/// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
|
||||
String get body => _encodingForHeaders(headers).decode(bodyBytes);
|
||||
|
||||
/// Error information, if any. This may be populated if the [statusCode] is
|
||||
/// 4xx or 5xx. This may be a string (e.g. the status line from the server) or
|
||||
/// an [Exception], but in either case the object should have a useful
|
||||
/// [toString] implementation that returns a human-readable value.
|
||||
final dynamic error;
|
||||
|
||||
/// The headers for this response.
|
||||
final Map<String, String> headers;
|
||||
}
|
||||
|
||||
bool _isSpace(String c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
|
||||
}
|
||||
|
||||
int _skipSpaces(String string, int index) {
|
||||
while (index < string.length && _isSpace(string[index]))
|
||||
index += 1;
|
||||
return index;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#algorithm-for-extracting-a-character-encoding-from-a-meta-element
|
||||
String _getCharset(String contentType) {
|
||||
int index = 0;
|
||||
while (index < contentType.length) {
|
||||
index = contentType.indexOf(new RegExp(r'charset', caseSensitive: false), index);
|
||||
if (index == -1)
|
||||
return null;
|
||||
index += 7;
|
||||
index = _skipSpaces(contentType, index);
|
||||
if (index >= contentType.length)
|
||||
return null;
|
||||
if (contentType[index] != '=')
|
||||
continue;
|
||||
index += 1;
|
||||
index = _skipSpaces(contentType, index);
|
||||
if (index >= contentType.length)
|
||||
return null;
|
||||
String delimiter = contentType[index];
|
||||
if (delimiter == '"' || delimiter == '\'') {
|
||||
index += 1;
|
||||
if (index >= contentType.length)
|
||||
return null;
|
||||
int start = index;
|
||||
int end = contentType.indexOf(delimiter, start);
|
||||
if (end == -1)
|
||||
return null;
|
||||
return contentType.substring(start, end);
|
||||
}
|
||||
int start = index;
|
||||
while (index < contentType.length) {
|
||||
String c = contentType[index];
|
||||
if (c == ' ' || c == ';')
|
||||
break;
|
||||
index += 1;
|
||||
}
|
||||
return contentType.substring(start, index);
|
||||
/// Creates a new HTTP response by waiting for the full body to become
|
||||
/// available from a [StreamedResponse].
|
||||
static Future<Response> fromStream(StreamedResponse response) {
|
||||
return response.stream.toBytes().then((List<int> body) {
|
||||
return new Response.bytes(
|
||||
body,
|
||||
response.statusCode,
|
||||
request: response.request,
|
||||
headers: response.headers,
|
||||
isRedirect: response.isRedirect,
|
||||
persistentConnection: response.persistentConnection,
|
||||
reasonPhrase: response.reasonPhrase);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Encoding _encodingForHeaders(Map<String, String> headers) {
|
||||
if (headers == null)
|
||||
return LATIN1;
|
||||
/// Returns the encoding to use for a response with the given headers. This
|
||||
/// defaults to [LATIN1] if the headers don't specify a charset or
|
||||
/// if that charset is unknown.
|
||||
Encoding _encodingForHeaders(Map<String, String> headers) =>
|
||||
encodingForCharset(_contentTypeForHeaders(headers).parameters['charset']);
|
||||
|
||||
/// Returns the [MediaType] object for the given headers's content-type.
|
||||
///
|
||||
/// Defaults to `application/octet-stream`.
|
||||
MediaType _contentTypeForHeaders(Map<String, String> headers) {
|
||||
String contentType = headers['content-type'];
|
||||
if (contentType == null)
|
||||
return LATIN1;
|
||||
String charset = _getCharset(contentType);
|
||||
if (charset == null)
|
||||
return LATIN1;
|
||||
return Encoding.getByName(charset) ?? LATIN1;
|
||||
if (contentType != null) return new MediaType.parse(contentType);
|
||||
return new MediaType("application", "octet-stream");
|
||||
}
|
||||
|
|
42
packages/flutter/lib/src/http/streamed_request.dart
Normal file
42
packages/flutter/lib/src/http/streamed_request.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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 'byte_stream.dart';
|
||||
import 'base_request.dart';
|
||||
|
||||
/// An HTTP request where the request body is sent asynchronously after the
|
||||
/// connection has been established and the headers have been sent.
|
||||
///
|
||||
/// When the request is sent via [BaseClient.send], only the headers and
|
||||
/// whatever data has already been written to [StreamedRequest.stream] will be
|
||||
/// sent immediately. More data will be sent as soon as it's written to
|
||||
/// [StreamedRequest.sink], and when the sink is closed the request will end.
|
||||
class StreamedRequest extends BaseRequest {
|
||||
/// Creates a new streaming request.
|
||||
StreamedRequest(String method, Uri url)
|
||||
: _controller = new StreamController<List<int>>(sync: true),
|
||||
super(method, url);
|
||||
|
||||
/// The sink to which to write data that will be sent as the request body.
|
||||
/// This may be safely written to before the request is sent; the data will be
|
||||
/// buffered.
|
||||
///
|
||||
/// Closing this signals the end of the request.
|
||||
EventSink<List<int>> get sink => _controller.sink;
|
||||
|
||||
/// The controller for [sink], from which [BaseRequest] will read data for
|
||||
/// [finalize].
|
||||
final StreamController<List<int>> _controller;
|
||||
|
||||
/// Freezes all mutable fields other than [stream] and returns a
|
||||
/// single-subscription [ByteStream] that emits the data being written to
|
||||
/// [sink].
|
||||
@override
|
||||
ByteStream finalize() {
|
||||
super.finalize();
|
||||
return new ByteStream(_controller.stream);
|
||||
}
|
||||
}
|
39
packages/flutter/lib/src/http/streamed_response.dart
Normal file
39
packages/flutter/lib/src/http/streamed_response.dart
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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 'byte_stream.dart';
|
||||
import 'base_response.dart';
|
||||
import 'base_request.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
/// An HTTP response where the response body is received asynchronously after
|
||||
/// the headers have been received.
|
||||
class StreamedResponse extends BaseResponse {
|
||||
/// The stream from which the response body data can be read. This should
|
||||
/// always be a single-subscription stream.
|
||||
final ByteStream stream;
|
||||
|
||||
/// Creates a new streaming response. [stream] should be a single-subscription
|
||||
/// stream.
|
||||
StreamedResponse(
|
||||
Stream<List<int>> stream,
|
||||
int statusCode,
|
||||
{int contentLength,
|
||||
BaseRequest request,
|
||||
Map<String, String> headers: const <String, String> {},
|
||||
bool isRedirect: false,
|
||||
bool persistentConnection: true,
|
||||
String reasonPhrase})
|
||||
: this.stream = toByteStream(stream),
|
||||
super(
|
||||
statusCode,
|
||||
contentLength: contentLength,
|
||||
request: request,
|
||||
headers: headers,
|
||||
isRedirect: isRedirect,
|
||||
persistentConnection: persistentConnection,
|
||||
reasonPhrase: reasonPhrase);
|
||||
}
|
143
packages/flutter/lib/src/http/utils.dart
Normal file
143
packages/flutter/lib/src/http/utils.dart
Normal file
|
@ -0,0 +1,143 @@
|
|||
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. 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:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'byte_stream.dart';
|
||||
|
||||
/// Converts a [Map] from parameter names to values to a URL query string.
|
||||
///
|
||||
/// mapToQuery({"foo": "bar", "baz": "bang"});
|
||||
/// //=> "foo=bar&baz=bang"
|
||||
String mapToQuery(Map<String, String> map, {Encoding encoding}) {
|
||||
List<List<String>> pairs = <List<String>>[];
|
||||
map.forEach((String key, String value) =>
|
||||
pairs.add(<String>[Uri.encodeQueryComponent(key, encoding: encoding),
|
||||
Uri.encodeQueryComponent(value, encoding: encoding)]));
|
||||
return pairs.map((List<String> pair) => "${pair[0]}=${pair[1]}").join("&");
|
||||
}
|
||||
|
||||
/// Like [String.split], but only splits on the first occurrence of the pattern.
|
||||
/// This will always return an array of two elements or fewer.
|
||||
///
|
||||
/// split1("foo,bar,baz", ","); //=> ["foo", "bar,baz"]
|
||||
/// split1("foo", ","); //=> ["foo"]
|
||||
/// split1("", ","); //=> []
|
||||
List<String> split1(String toSplit, String pattern) {
|
||||
if (toSplit.isEmpty) return <String>[];
|
||||
|
||||
int index = toSplit.indexOf(pattern);
|
||||
if (index == -1) return <String>[toSplit];
|
||||
return <String>[
|
||||
toSplit.substring(0, index),
|
||||
toSplit.substring(index + pattern.length)
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns the [Encoding] that corresponds to [charset]. Returns [fallback] if
|
||||
/// [charset] is null or if no [Encoding] was found that corresponds to
|
||||
/// [charset].
|
||||
Encoding encodingForCharset(String charset, [Encoding fallback = LATIN1]) {
|
||||
if (charset == null) return fallback;
|
||||
Encoding encoding = Encoding.getByName(charset);
|
||||
return encoding == null ? fallback : encoding;
|
||||
}
|
||||
|
||||
|
||||
/// Returns the [Encoding] that corresponds to [charset]. Throws a
|
||||
/// [FormatException] if no [Encoding] was found that corresponds to [charset].
|
||||
/// [charset] may not be null.
|
||||
Encoding requiredEncodingForCharset(String charset) {
|
||||
Encoding encoding = Encoding.getByName(charset);
|
||||
if (encoding != null) return encoding;
|
||||
throw new FormatException('Unsupported encoding "$charset".');
|
||||
}
|
||||
|
||||
/// A regular expression that matches strings that are composed entirely of
|
||||
/// ASCII-compatible characters.
|
||||
final RegExp _kAsciiOnly = new RegExp(r"^[\x00-\x7F]+$");
|
||||
|
||||
/// Returns whether [string] is composed entirely of ASCII-compatible
|
||||
/// characters.
|
||||
bool isPlainAscii(String string) => _kAsciiOnly.hasMatch(string);
|
||||
|
||||
/// Converts [input] into a [Uint8List].
|
||||
///
|
||||
/// If [input] is a [TypedData], this just returns a view on [input].
|
||||
Uint8List toUint8List(dynamic input) {
|
||||
if (input is Uint8List)
|
||||
return input;
|
||||
if (input is TypedData)
|
||||
return new Uint8List.view(input.buffer);
|
||||
return new Uint8List.fromList(input);
|
||||
}
|
||||
|
||||
/// If [stream] is already a [ByteStream], returns it. Otherwise, wraps it in a
|
||||
/// [ByteStream].
|
||||
ByteStream toByteStream(Stream<List<int>> stream) {
|
||||
if (stream is ByteStream) return stream;
|
||||
return new ByteStream(stream);
|
||||
}
|
||||
|
||||
/// Calls [onDone] once [stream] (a single-subscription [Stream]) is finished.
|
||||
/// The return value, also a single-subscription [Stream] should be used in
|
||||
/// place of [stream] after calling this method.
|
||||
Stream/*<T>*/ onDone/*<T>*/(Stream/*<T>*/ stream, void onDone()) =>
|
||||
stream.transform(new StreamTransformer.fromHandlers(handleDone: (EventSink<dynamic> sink) { // ignore: always_specify_types
|
||||
sink.close();
|
||||
onDone();
|
||||
}));
|
||||
|
||||
// TODO(nweiz): remove this when issue 7786 is fixed.
|
||||
/// Pipes all data and errors from [stream] into [sink]. When [stream] is done,
|
||||
/// [sink] is closed and the returned [Future] is completed.
|
||||
Future<dynamic> store(Stream<dynamic> stream, EventSink<dynamic> sink) {
|
||||
Completer<dynamic> completer = new Completer<dynamic>();
|
||||
stream.listen(sink.add,
|
||||
onError: sink.addError,
|
||||
onDone: () {
|
||||
sink.close();
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Pipes all data and errors from [stream] into [sink]. Completes [Future] once
|
||||
/// [stream] is done. Unlike [store], [sink] remains open after [stream] is
|
||||
/// done.
|
||||
Future<dynamic> writeStreamToSink(Stream<dynamic> stream, EventSink<dynamic> sink) {
|
||||
Completer<dynamic> completer = new Completer<dynamic>();
|
||||
stream.listen(sink.add,
|
||||
onError: sink.addError,
|
||||
onDone: () => completer.complete());
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// A pair of values.
|
||||
class Pair<E, F> {
|
||||
E first;
|
||||
F last;
|
||||
|
||||
Pair(this.first, this.last);
|
||||
|
||||
@override
|
||||
String toString() => '($first, $last)';
|
||||
|
||||
@override
|
||||
bool operator==(dynamic other) {
|
||||
if (other is! Pair) return false;
|
||||
return other.first == first && other.last == last;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => first.hashCode ^ last.hashCode;
|
||||
}
|
||||
|
||||
/// Configures [future] so that its result (success or exception) is passed on
|
||||
/// to [completer].
|
||||
void chainToCompleter(Future<dynamic> future, Completer<dynamic> completer) {
|
||||
future.then(completer.complete, onError: completer.completeError);
|
||||
}
|
|
@ -13,8 +13,6 @@ import 'package:flutter/http.dart' as http;
|
|||
import 'package:mojo/core.dart' as core;
|
||||
import 'package:mojo_services/mojo/asset_bundle/asset_bundle.mojom.dart' as mojom;
|
||||
|
||||
import 'shell.dart';
|
||||
|
||||
/// A collection of resources used by the application.
|
||||
///
|
||||
/// Asset bundles contain resources, such as images and strings, that can be
|
||||
|
@ -86,12 +84,18 @@ class NetworkAssetBundle extends AssetBundle {
|
|||
|
||||
@override
|
||||
Future<core.MojoDataPipeConsumer> load(String key) async {
|
||||
return await http.readDataPipe(_urlFromKey(key));
|
||||
http.Response response = await http.get(_urlFromKey(key));
|
||||
if (response.statusCode == 200)
|
||||
return null;
|
||||
core.MojoDataPipe pipe = new core.MojoDataPipe();
|
||||
core.DataPipeFiller.fillHandle(pipe.producer, response.bodyBytes.buffer.asByteData());
|
||||
return pipe.consumer;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> loadString(String key, { bool cache: true }) async {
|
||||
return (await http.get(_urlFromKey(key))).body;
|
||||
http.Response response = await http.get(_urlFromKey(key));
|
||||
return response.statusCode == 200 ? response.body : null;
|
||||
}
|
||||
|
||||
/// Retrieve a string from the asset bundle, parse it with the given function,
|
||||
|
@ -191,22 +195,6 @@ class MojoAssetBundle extends CachingAssetBundle {
|
|||
/// Creates an [AssetBundle] interface around the given [mojom.AssetBundleProxy] Mojo service.
|
||||
MojoAssetBundle(this._bundle);
|
||||
|
||||
/// Retrieves the asset bundle located at the given URL, unpacks it, and provides it contents.
|
||||
factory MojoAssetBundle.fromNetwork(String relativeUrl) {
|
||||
final mojom.AssetBundleProxy bundle = new mojom.AssetBundleProxy.unbound();
|
||||
_fetchAndUnpackBundleAsychronously(relativeUrl, bundle);
|
||||
return new MojoAssetBundle(bundle);
|
||||
}
|
||||
|
||||
static Future<Null> _fetchAndUnpackBundleAsychronously(String relativeUrl, mojom.AssetBundleProxy bundle) async {
|
||||
final core.MojoDataPipeConsumer bundleData = await http.readDataPipe(Uri.base.resolve(relativeUrl));
|
||||
final mojom.AssetUnpackerProxy unpacker = shell.connectToApplicationService(
|
||||
'mojo:asset_bundle', mojom.AssetUnpacker.connectToService
|
||||
);
|
||||
unpacker.unpackZipStream(bundleData, bundle);
|
||||
unpacker.close();
|
||||
}
|
||||
|
||||
mojom.AssetBundleProxy _bundle;
|
||||
|
||||
@override
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui' show Size, Locale, hashValues;
|
||||
import 'dart:ui' as ui show Image;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/http.dart' as http;
|
||||
|
@ -278,7 +279,7 @@ abstract class DataPipeImageProvider<T> extends ImageProvider<T> {
|
|||
// TODO(ianh): Find some way to honour cache headers to the extent that when the
|
||||
// last reference to an image is released, we proactively evict the image from
|
||||
// our cache if the headers describe the image as having expired at that point.
|
||||
class NetworkImage extends DataPipeImageProvider<NetworkImage> {
|
||||
class NetworkImage extends ImageProvider<NetworkImage> {
|
||||
/// Creates an object that fetches the image at the given URL.
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
|
@ -296,15 +297,36 @@ class NetworkImage extends DataPipeImageProvider<NetworkImage> {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<mojo.MojoDataPipeConsumer> loadDataPipe(NetworkImage key) async {
|
||||
assert(key == this);
|
||||
return http.readDataPipe(Uri.base.resolve(key.url));
|
||||
ImageStreamCompleter load(NetworkImage key) {
|
||||
return new OneFrameImageStreamCompleter(
|
||||
_loadAsync(key),
|
||||
informationCollector: (StringBuffer information) {
|
||||
information.writeln('Image provider: $this');
|
||||
information.write('Image key: $key');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double getScale(NetworkImage key) {
|
||||
Future<ImageInfo> _loadAsync(NetworkImage key) async {
|
||||
assert(key == this);
|
||||
return key.scale;
|
||||
|
||||
final Uri resolved = Uri.base.resolve(key.url);
|
||||
final http.Response response = await http.get(resolved);
|
||||
if (response == null || response.statusCode != 200)
|
||||
return null;
|
||||
|
||||
Uint8List bytes = response.bodyBytes;
|
||||
if (bytes.lengthInBytes == 0)
|
||||
return null;
|
||||
|
||||
final ui.Image image = await decodeImageFromList(bytes);
|
||||
if (image == null)
|
||||
return null;
|
||||
|
||||
return new ImageInfo(
|
||||
image: image,
|
||||
scale: key.scale,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -10,6 +10,11 @@ dependencies:
|
|||
meta: ^1.0.3
|
||||
vector_math: '>=2.0.3 <3.0.0'
|
||||
|
||||
# async and http_parser can be removed when we move to using dart-lang/http
|
||||
# directly.
|
||||
async: "^1.10.0"
|
||||
http_parser: ">=0.0.1 <4.0.0"
|
||||
|
||||
sky_engine:
|
||||
path: ../../bin/cache/pkg/sky_engine
|
||||
sky_services:
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'dart:ui' as ui;
|
|||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/http.dart' as http;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -109,6 +110,13 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
|||
@override
|
||||
void initInstances() {
|
||||
timeDilation = 1.0; // just in case the developer has artificially changed it for development
|
||||
http.Client.clientOverride = () {
|
||||
return new http.MockClient((http.Request request){
|
||||
return new Future<http.Response>.value(
|
||||
new http.Response("Mocked: Unavailable.", 404, request: request)
|
||||
);
|
||||
});
|
||||
};
|
||||
super.initInstances();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue