From c303aa84e6d803227badbe6c4ef308bfb80dee70 Mon Sep 17 00:00:00 2001 From: Benjamin Dopplinger Date: Wed, 28 Sep 2016 04:48:56 +1000 Subject: [PATCH 01/41] Update URL for reporting bugs Also remove stale [docs] link. Closes #3 --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 506bbadefa..20efbb1390 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,4 @@ main() async { ## Filing issues -Please file issues for the http package at [http://dartbug.com/new][bugs]. - -[bugs]: http://dartbug.com/new -[docs]: https://api.dartlang.org/docs/channels/dev/latest/http.html +Please file issues for the http package at . From 899f4569f9806bb431ae4f19ad4732b81851c598 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 14 Dec 2016 14:28:20 -0800 Subject: [PATCH 02/41] Remove an outdated bug link in the README. (#48) Closes #46 --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 20efbb1390..8d852914d4 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,3 @@ main() async { print('Response body: ${response.body}'); } ``` - -## Filing issues - -Please file issues for the http package at . From efc13deacec5b7c747a0a84c44a482d4fd35d843 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Fri, 7 Oct 2016 11:35:29 -0700 Subject: [PATCH 03/41] fix analyzer --- test/io/utils.dart | 2 +- test/mock_client_test.dart | 1 - test/multipart_test.dart | 4 ++-- test/response_test.dart | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/test/io/utils.dart b/test/io/utils.dart index 4594b6e5a8..a97f04fe9c 100644 --- a/test/io/utils.dart +++ b/test/io/utils.dart @@ -96,7 +96,7 @@ Future startServer() { // want to test them here. if (name == 'cookie' || name == 'host') return; - content['headers'][name] = values; + (content['headers'] as Map)[name] = values; }); var body = JSON.encode(content); diff --git a/test/mock_client_test.dart b/test/mock_client_test.dart index a19adcad24..48e635fee3 100644 --- a/test/mock_client_test.dart +++ b/test/mock_client_test.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:http/src/utils.dart'; import 'package:http/testing.dart'; import 'package:unittest/unittest.dart'; diff --git a/test/multipart_test.dart b/test/multipart_test.dart index 8f908f938b..64586db2d8 100644 --- a/test/multipart_test.dart +++ b/test/multipart_test.dart @@ -183,7 +183,7 @@ void main() { test('with a stream file', () { var request = new http.MultipartRequest('POST', dummyUrl); - var controller = new StreamController(sync: true); + var controller = new StreamController>(sync: true); request.files.add(new http.MultipartFile('file', controller.stream, 5)); expect(request, bodyMatches(''' @@ -201,7 +201,7 @@ void main() { test('with an empty stream file', () { var request = new http.MultipartRequest('POST', dummyUrl); - var controller = new StreamController(sync: true); + var controller = new StreamController>(sync: true); request.files.add(new http.MultipartFile('file', controller.stream, 0)); expect(request, bodyMatches(''' diff --git a/test/response_test.dart b/test/response_test.dart index 08c4708c38..0b33f908df 100644 --- a/test/response_test.dart +++ b/test/response_test.dart @@ -48,7 +48,7 @@ void main() { group('.fromStream()', () { test('sets body', () { - var controller = new StreamController(sync: true); + var controller = new StreamController>(sync: true); var streamResponse = new http.StreamedResponse( controller.stream, 200, contentLength: 13); var future = http.Response.fromStream(streamResponse) @@ -61,7 +61,7 @@ void main() { }); test('sets bodyBytes', () { - var controller = new StreamController(sync: true); + var controller = new StreamController>(sync: true); var streamResponse = new http.StreamedResponse( controller.stream, 200, contentLength: 5); var future = http.Response.fromStream(streamResponse) From c23bef785d0cc975414af174b57d5ec7fe34e5fd Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 11 Jan 2017 21:33:26 -0800 Subject: [PATCH 04/41] Replicate Body from shelf into http --- lib/src/body.dart | 96 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 lib/src/body.dart diff --git a/lib/src/body.dart b/lib/src/body.dart new file mode 100644 index 0000000000..e3589d0326 --- /dev/null +++ b/lib/src/body.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2017, 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:collection/collection.dart'; + +/// The body of a request or response. +/// +/// This tracks whether the body has been read. It's separate from [Message] +/// because the message may be changed with [Message.change], but each instance +/// should share a notion of whether the body was read. +class Body { + /// The contents of the message body. + /// + /// This will be `null` after [read] is called. + Stream> _stream; + + /// The encoding used to encode the stream returned by [read], or `null` if no + /// encoding was used. + final Encoding encoding; + + /// The length of the stream returned by [read], or `null` if that can't be + /// determined efficiently. + final int contentLength; + + Body._(this._stream, this.encoding, this.contentLength); + + /// Converts [body] to a byte stream and wraps it in a [Body]. + /// + /// [body] may be either a [Body], a [String], a [List], a + /// [Stream>], or `null`. If it's a [String], [encoding] will be + /// used to convert it to a [Stream>]. + factory Body(body, [Encoding encoding]) { + if (body is Body) return body; + + Stream> stream; + int contentLength; + if (body == null) { + contentLength = 0; + stream = new Stream.fromIterable([]); + } else if (body is String) { + if (encoding == null) { + var encoded = UTF8.encode(body); + // If the text is plain ASCII, don't modify the encoding. This means + // that an encoding of "text/plain" will stay put. + if (!_isPlainAscii(encoded, body.length)) encoding = UTF8; + contentLength = encoded.length; + stream = new Stream.fromIterable([encoded]); + } else { + var encoded = encoding.encode(body); + contentLength = encoded.length; + stream = new Stream.fromIterable([encoded]); + } + } else if (body is List) { + contentLength = body.length; + stream = new Stream.fromIterable([DelegatingList.typed(body)]); + } else if (body is Stream) { + stream = DelegatingStream.typed(body); + } else { + throw new ArgumentError('Response body "$body" must be a String or a ' + 'Stream.'); + } + + return new Body._(stream, encoding, contentLength); + } + + /// Returns whether [bytes] is plain ASCII. + /// + /// [codeUnits] is the number of code units in the original string. + static bool _isPlainAscii(List bytes, int codeUnits) { + // Most non-ASCII code units will produce multiple bytes and make the text + // longer. + if (bytes.length != codeUnits) return false; + + // Non-ASCII code units between U+0080 and U+009F produce 8-bit characters + // with the high bit set. + return bytes.every((byte) => byte & 0x80 == 0); + } + + /// Returns a [Stream] representing the body. + /// + /// Can only be called once. + Stream> read() { + if (_stream == null) { + throw new StateError("The 'read' method can only be called once on a " + "http.Request/http.Response object."); + } + var stream = _stream; + _stream = null; + return stream; + } +} From 0dac77f0bf37871daa754b961b21d2ff1f279b34 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 11 Jan 2017 21:33:45 -0800 Subject: [PATCH 05/41] Replicate UnmodifiableMap implementation from shelf --- lib/src/http_unmodifiable_map.dart | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 lib/src/http_unmodifiable_map.dart diff --git a/lib/src/http_unmodifiable_map.dart b/lib/src/http_unmodifiable_map.dart new file mode 100644 index 0000000000..5769dcc6c8 --- /dev/null +++ b/lib/src/http_unmodifiable_map.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2017, 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:collection'; + +import 'package:collection/collection.dart'; +import 'package:http_parser/http_parser.dart'; + +/// A simple wrapper over [UnmodifiableMapView] which avoids re-wrapping itself. +class HttpUnmodifiableMap extends UnmodifiableMapView { + /// `true` if the key values are already lowercase. + final bool _ignoreKeyCase; + + /// If [source] is a [HttpUnmodifiableMap] with matching [ignoreKeyCase], + /// then [source] is returned. + /// + /// If [source] is `null` it is treated like an empty map. + /// + /// If [ignoreKeyCase] is `true`, the keys will have case-insensitive access. + /// + /// [source] is copied to a new [Map] to ensure changes to the parameter value + /// after constructions are not reflected. + factory HttpUnmodifiableMap(Map source, + {bool ignoreKeyCase: false}) { + if (source is HttpUnmodifiableMap && + // !ignoreKeyCase: no transformation of the input is required + // source._ignoreKeyCase: the input cannot be transformed any more + (!ignoreKeyCase || source._ignoreKeyCase)) { + return source; + } + + if (source == null || source.isEmpty) { + return const _EmptyHttpUnmodifiableMap(); + } + + if (ignoreKeyCase) { + source = new CaseInsensitiveMap.from(source); + } else { + source = new Map.from(source); + } + + return new HttpUnmodifiableMap._(source, ignoreKeyCase); + } + + /// Returns an empty [HttpUnmodifiableMap]. + const factory HttpUnmodifiableMap.empty() = _EmptyHttpUnmodifiableMap; + + HttpUnmodifiableMap._(Map source, this._ignoreKeyCase) + : super(source); +} + +/// A const implementation of an empty [HttpUnmodifiableMap]. +class _EmptyHttpUnmodifiableMap extends MapView + implements HttpUnmodifiableMap { + bool get _ignoreKeyCase => true; + const _EmptyHttpUnmodifiableMap() : super(const {}); +} From f975718799202f4a28fda7e906ca3ee19f233bfd Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 17 Jan 2017 13:03:33 -0800 Subject: [PATCH 06/41] Updating .gitignore to latest from GitHub (#53) --- .gitignore | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 7dbf0350d6..8b7331fd82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,27 @@ -# Don’t commit the following directories created by pub. +# See https://www.dartlang.org/tools/private-files.html + +# Files and directories created by pub .buildlog +.packages +.project .pub/ build/ -packages -.packages +**/packages/ -# Or the files created by dart2js. +# Files created by dart2js +# (Most Dart developers will use pub build to compile Dart, use/modify these +# rules if you intend to use dart2js directly +# Convention is to use extension '.dart.js' for Dart compiled to Javascript to +# differentiate from explicit Javascript files) *.dart.js -*.js_ +*.part.js *.js.deps *.js.map +*.info.json + +# Directory created by dartdoc +doc/api/ -# Include when developing application packages. -pubspec.lock \ No newline at end of file +# Don't commit pubspec lock file +# (Library packages only! Remove pattern if developing an application package) +pubspec.lock From 3ae6d4bf01c26aabecfa753f7185e610b955fdea Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 14 Feb 2017 13:34:30 -0800 Subject: [PATCH 07/41] Emulate shelf messaging (#54) --- lib/src/message.dart | 198 +++++++++++++++++++++++++++++ lib/src/request.dart | 289 +++++++++++++++++++++--------------------- lib/src/response.dart | 145 ++++++++++----------- lib/src/utils.dart | 31 +++++ 4 files changed, 440 insertions(+), 223 deletions(-) create mode 100644 lib/src/message.dart diff --git a/lib/src/message.dart b/lib/src/message.dart new file mode 100644 index 0000000000..96608dda66 --- /dev/null +++ b/lib/src/message.dart @@ -0,0 +1,198 @@ +// Copyright (c) 2017, 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:collection/collection.dart'; +import 'package:http_parser/http_parser.dart'; + +import 'body.dart'; +import 'http_unmodifiable_map.dart'; +import 'utils.dart'; + +/// Retrieves the [Body] contained in the [message]. +/// +/// This is meant for internal use by `http` so the message body is accessible +/// for subclasses of [Message] but hidden elsewhere. +Body getBody(Message message) => message._body; + +/// Represents logic shared between [Request] and [Response]. +abstract class Message { + /// The HTTP headers. + /// + /// This is immutable. A copy of this with new headers can be created using + /// [change]. + final Map headers; + + /// Extra context that can be used by middleware and handlers. + /// + /// For requests, this is used to pass data to inner middleware and handlers; + /// for responses, it's used to pass data to outer middleware and handlers. + /// + /// Context properties that are used by a particular package should begin with + /// that package's name followed by a period. For example, if there was a + /// package `foo` which contained a middleware `bar` and it wanted to take + /// a context property, its property would be `"foo.bar"`. + /// + /// This is immutable. A copy of this with new context values can be created + /// using [change]. + final Map context; + + /// The streaming body of the message. + /// + /// This can be read via [read] or [readAsString]. + final Body _body; + + /// Creates a new [Message]. + /// + /// [body] is the message body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. If it's a [String], + /// [encoding] is used to encode it to a [Stream>]. It defaults to + /// UTF-8. + /// + /// If [headers] is `null`, it's treated as empty. + /// + /// If [encoding] is passed, the "encoding" field of the Content-Type header + /// in [headers] will be set appropriately. If there is no existing + /// Content-Type header, it will be set to "application/octet-stream". + Message(body, + {Encoding encoding, + Map headers, + Map context}) + : this._(new Body(body, encoding), headers, context); + + Message._(Body body, Map headers, Map context) + : _body = body, + headers = new HttpUnmodifiableMap(_adjustHeaders(headers, body), + ignoreKeyCase: true), + context = + new HttpUnmodifiableMap(context, ignoreKeyCase: false); + + /// If `true`, the stream returned by [read] won't emit any bytes. + /// + /// This may have false negatives, but it won't have false positives. + bool get isEmpty => _body.contentLength == 0; + + /// The contents of the content-length field in [headers]. + /// + /// If not set, `null`. + int get contentLength { + if (_contentLengthCache != null) return _contentLengthCache; + if (!headers.containsKey('content-length')) return null; + _contentLengthCache = int.parse(headers['content-length']); + return _contentLengthCache; + } + int _contentLengthCache; + + /// The MIME type declared in [headers]. + /// + /// This is parsed from the Content-Type header in [headers]. It contains only + /// the MIME type, without any Content-Type parameters. + /// + /// If [headers] doesn't have a Content-Type header, this will be `null`. + String get mimeType { + var contentType = _contentType; + if (contentType == null) return null; + return contentType.mimeType; + } + + /// The encoding of the body returned by [read]. + /// + /// This is parsed from the "charset" parameter of the Content-Type header in + /// [headers]. + /// + /// If [headers] doesn't have a Content-Type header or it specifies an + /// encoding that [dart:convert] doesn't support, this will be `null`. + Encoding get encoding { + var contentType = _contentType; + if (contentType == null) return null; + if (!contentType.parameters.containsKey('charset')) return null; + return Encoding.getByName(contentType.parameters['charset']); + } + + /// The parsed version of the Content-Type header in [headers]. + /// + /// This is cached for efficient access. + MediaType get _contentType { + if (_contentTypeCache != null) return _contentTypeCache; + if (!headers.containsKey('content-type')) return null; + _contentTypeCache = new MediaType.parse(headers['content-type']); + return _contentTypeCache; + } + MediaType _contentTypeCache; + + /// Returns the message body as byte chunks. + /// + /// Throws a [StateError] if [read] or [readAsString] has already been called. + Stream> read() => _body.read(); + + /// Returns the message body as a string. + /// + /// If [encoding] is passed, that's used to decode the body. Otherwise the + /// encoding is taken from the Content-Type header. If that doesn't exist or + /// doesn't have a "charset" parameter, UTF-8 is used. + /// + /// Throws a [StateError] if [read] or [readAsString] has already been called. + Future readAsString([Encoding encoding]) { + encoding ??= this.encoding ?? UTF8; + return encoding.decodeStream(read()); + } + + /// Creates a copy of this by copying existing values and applying specified + /// changes. + Message change( + {Map headers, Map context, body}); +} + +/// Adds information about encoding to [headers]. +/// +/// Returns a new map without modifying [headers]. +Map _adjustHeaders(Map headers, Body body) { + var sameEncoding = _sameEncoding(headers, body); + if (sameEncoding) { + if (body.contentLength == null || + getHeader(headers, 'content-length') == body.contentLength.toString()) { + return headers ?? const HttpUnmodifiableMap.empty(); + } else if (body.contentLength == 0 && + (headers == null || headers.isEmpty)) { + return const HttpUnmodifiableMap.empty(); + } + } + + var newHeaders = headers == null + ? new CaseInsensitiveMap() + : new CaseInsensitiveMap.from(headers); + + if (!sameEncoding) { + if (newHeaders['content-type'] == null) { + newHeaders['content-type'] = + 'application/octet-stream; charset=${body.encoding.name}'; + } else { + var contentType = new MediaType.parse(newHeaders['content-type']) + .change(parameters: {'charset': body.encoding.name}); + newHeaders['content-type'] = contentType.toString(); + } + } + + if (body.contentLength != null) { + var coding = newHeaders['transfer-encoding']; + if (coding == null || equalsIgnoreAsciiCase(coding, 'identity')) { + newHeaders['content-length'] = body.contentLength.toString(); + } + } + + return newHeaders; +} + +/// Returns whether [headers] declares the same encoding as [body]. +bool _sameEncoding(Map headers, Body body) { + if (body.encoding == null) return true; + + var contentType = getHeader(headers, 'content-type'); + if (contentType == null) return false; + + var charset = new MediaType.parse(contentType).parameters['charset']; + return Encoding.getByName(charset) == body.encoding; +} diff --git a/lib/src/request.dart b/lib/src/request.dart index 67b664c08e..1f2be464df 100644 --- a/lib/src/request.dart +++ b/lib/src/request.dart @@ -3,159 +3,154 @@ // 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 'message.dart'; import 'utils.dart'; -/// An HTTP request where the entire request body is known in advance. -class Request extends BaseRequest { - /// 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]. - int get contentLength => bodyBytes.length; - - 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; - var contentType = _contentType; - if (contentType == null) return; - _contentType = contentType.change(parameters: {'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]. +/// Represents an HTTP request to be sent to a server. +class Request extends Message { + /// The HTTP method of the request. /// - /// This list should only be set, not be modified in place. - Uint8List get bodyBytes => _bodyBytes; - Uint8List _bodyBytes; - - set bodyBytes(List 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); - var contentType = _contentType; - if (contentType == null) { - _contentType = new MediaType("text", "plain", {'charset': encoding.name}); - } else if (!contentType.parameters.containsKey('charset')) { - _contentType = contentType.change(parameters: {'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 get bodyFields { - var 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 fields) { - var 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); - } - - /// Creates a new HTTP request. - Request(String method, Uri url) - : _defaultEncoding = UTF8, - _bodyBytes = new Uint8List(0), - super(method, url); - - /// Freezes all mutable fields and returns a single-subscription [ByteStream] - /// containing the request body. - ByteStream finalize() { - super.finalize(); - return new ByteStream.fromBytes(bodyBytes); - } + /// Most commonly "GET" or "POST", less commonly "HEAD", "PUT", or "DELETE". + /// Non-standard method names are also supported. + final String method; - /// The `Content-Type` header of the request (if it exists) as a - /// [MediaType]. - MediaType get _contentType { - var contentType = headers['content-type']; - if (contentType == null) return null; - return new MediaType.parse(contentType); - } + /// The URL to which the request will be sent. + final Uri url; - set _contentType(MediaType value) { - headers['content-type'] = value.toString(); - } + /// Creates a new [Request] for [url] using [method]. + /// + /// [body] is the request body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. If it's a [String], + /// [encoding] is used to encode it to a [Stream>]. It defaults to + /// UTF-8. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + Request(this.method, this.url, + {body, + Encoding encoding, + Map headers, + Map context}) + : super(body, encoding: encoding, headers: headers, context: context); + + /// Creates a new HEAD [Request] to [url]. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + Request.head(Uri url, + {Map headers, Map context}) + : this('HEAD', url, headers: headers, context: context); - /// Throw an error if this request has been finalized. - void _checkFinalized() { - if (!finalized) return; - throw new StateError("Can't modify a finalized Request."); + /// Creates a new GET [Request] to [url]. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + Request.get(Uri url, + {Map headers, Map context}) + : this('GET', url, headers: headers, context: context); + + /// Creates a new POST [Request] to [url]. + /// [body] is the request body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. If it's a [String], + /// [encoding] is used to encode it to a [Stream>]. It defaults to + /// UTF-8. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + Request.post(Uri url, + body, + {Encoding encoding, + Map headers, + Map context}) + : this('POST', url, + body: body, encoding: encoding, headers: headers, context: context); + + /// Creates a new PUT [Request] to [url]. + /// + /// [body] is the request body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. If it's a [String], + /// [encoding] is used to encode it to a [Stream>]. It defaults to + /// UTF-8. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + Request.put(Uri url, + body, + {Encoding encoding, + Map headers, + Map context}) + : this('PUT', url, + body: body, encoding: encoding, headers: headers, context: context); + + /// Creates a new PATCH [Request] to [url]. + /// + /// [body] is the request body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. If it's a [String], + /// [encoding] is used to encode it to a [Stream>]. It defaults to + /// UTF-8. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + Request.patch(Uri url, + body, + {Encoding encoding, + Map headers, + Map context}) + : this('PATCH', url, + body: body, encoding: encoding, headers: headers, context: context); + + /// Creates a new DELETE [Request] to [url]. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + Request.delete(Uri url, + {Map headers, Map context}) + : this('DELETE', url, headers: headers, context: context); + + /// Creates a new [Request] by copying existing values and applying specified + /// changes. + /// + /// New key-value pairs in [context] and [headers] will be added to the copied + /// [Request]. If [context] or [headers] includes a key that already exists, + /// the key-value pair will replace the corresponding entry in the copied + /// [Request]. All other context and header values from the [Request] will be + /// included in the copied [Request] unchanged. + /// + /// [body] is the request body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. + Request change( + {Map headers, + Map context, + body}) { + var updatedHeaders = updateMap(this.headers, headers); + var updatedContext = updateMap(this.context, context); + + return new Request(this.method, this.url, + body: body ?? getBody(this), + encoding: this.encoding, + headers: updatedHeaders, + context: updatedContext); } } diff --git a/lib/src/response.dart b/lib/src/response.dart index 9fa06ee782..c94cb0b46c 100644 --- a/lib/src/response.dart +++ b/lib/src/response.dart @@ -2,94 +2,87 @@ // 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 'message.dart'; import 'utils.dart'; /// An HTTP response where the entire response body is known in advance. -class Response extends BaseResponse { - /// The bytes comprising the body of this response. - final Uint8List bodyBytes; +class Response extends Message { + /// The status code of the response. + final int statusCode; - /// 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][]. + /// Creates a new HTTP response with the given [statusCode]. /// - /// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html - String get body => _encodingForHeaders(headers).decode(bodyBytes); - - /// Creates a new HTTP response with a string body. - Response( - String body, - int statusCode, - {BaseRequest request, - Map headers: const {}, - 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); + /// [body] is the request body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. If it's a [String], + /// [encoding] is used to encode it to a [Stream>]. It defaults to + /// UTF-8. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between outer middleware + /// and handlers. + Response(this.statusCode, + {body, + Encoding encoding, + Map headers, + Map context}) + : super(body, encoding: encoding, headers: headers, context: context); - /// Create a new HTTP response with a byte array body. - Response.bytes( - List bodyBytes, - int statusCode, - {BaseRequest request, - Map headers: const {}, - 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); + /// Creates a new [Response] by copying existing values and applying specified + /// changes. + /// + /// New key-value pairs in [context] and [headers] will be added to the copied + /// [Response]. + /// + /// If [context] or [headers] includes a key that already exists, the + /// key-value pair will replace the corresponding entry in the copied + /// [Response]. + /// + /// All other context and header values from the [Response] will be included + /// in the copied [Response] unchanged. + /// + /// [body] is the request body. It may be either a [String], a [List], a + /// [Stream>], or `null` to indicate no body. + Response change( + {Map headers, + Map context, + body}) { + var updatedHeaders = updateMap(this.headers, headers); + var updatedContext = updateMap(this.context, context); - /// Creates a new HTTP response by waiting for the full body to become - /// available from a [StreamedResponse]. - static Future fromStream(StreamedResponse response) { - return response.stream.toBytes().then((body) { - return new Response.bytes( - body, - response.statusCode, - request: response.request, - headers: response.headers, - isRedirect: response.isRedirect, - persistentConnection: response.persistentConnection, - reasonPhrase: response.reasonPhrase); - }); + return new Response(this.statusCode, + body: body ?? getBody(this), + headers: updatedHeaders, + context: updatedContext); } -} -/// 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 headers) => - encodingForCharset(_contentTypeForHeaders(headers).parameters['charset']); + /// The date and time after which the response's data should be considered + /// stale. + /// + /// This is parsed from the Expires header in [headers]. If [headers] doesn't + /// have an Expires header, this will be `null`. + DateTime get expires { + if (_expiresCache != null) return _expiresCache; + if (!headers.containsKey('expires')) return null; + _expiresCache = parseHttpDate(headers['expires']); + return _expiresCache; + } + DateTime _expiresCache; -/// Returns the [MediaType] object for the given headers's content-type. -/// -/// Defaults to `application/octet-stream`. -MediaType _contentTypeForHeaders(Map headers) { - var contentType = headers['content-type']; - if (contentType != null) return new MediaType.parse(contentType); - return new MediaType("application", "octet-stream"); + /// The date and time the source of the response's data was last modified. + /// + /// This is parsed from the Last-Modified header in [headers]. If [headers] + /// doesn't have a Last-Modified header, this will be `null`. + DateTime get lastModified { + if (_lastModifiedCache != null) return _lastModifiedCache; + if (!headers.containsKey('last-modified')) return null; + _lastModifiedCache = parseHttpDate(headers['last-modified']); + return _lastModifiedCache; + } + DateTime _lastModifiedCache; } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 86a6690fb5..39f1aefac4 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -6,7 +6,24 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; + import 'byte_stream.dart'; +import 'http_unmodifiable_map.dart'; + +/// Returns a [Map] with the values from [original] and the values from +/// [updates]. +/// +/// For keys that are the same between [original] and [updates], the value in +/// [updates] is used. +/// +/// If [updates] is `null` or empty, [original] is returned unchanged. +Map/**/ updateMap/**/( + Map/**/ original, Map/**/ updates) { + if (updates == null || updates.isEmpty) return original; + + return new Map.from(original)..addAll(updates); +} /// Converts a [Map] from parameter names to values to a URL query string. /// @@ -139,3 +156,17 @@ class Pair { void chainToCompleter(Future future, Completer completer) { future.then(completer.complete, onError: completer.completeError); } + +/// Returns the header with the given [name] in [headers]. +/// +/// This works even if [headers] is `null`, or if it's not yet a +/// case-insensitive map. +String getHeader(Map headers, String name) { + if (headers == null) return null; + if (headers is HttpUnmodifiableMap) return headers[name]; + + for (var key in headers.keys) { + if (equalsIgnoreAsciiCase(key, name)) return headers[key]; + } + return null; +} From 5940bb271a21fea2c26276b6344f1ac4ffbbd779 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 16 Feb 2017 15:42:06 -0800 Subject: [PATCH 08/41] Stop using dart:mirrors. (#55) --- CHANGELOG.md | 4 +++ lib/browser_client.dart | 4 +-- lib/src/client.dart | 6 +--- lib/src/io.dart | 55 ------------------------------------- lib/src/io_client.dart | 24 ++++------------ lib/src/multipart_file.dart | 8 +++--- pubspec.yaml | 4 +-- 7 files changed, 17 insertions(+), 88 deletions(-) delete mode 100644 lib/src/io.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index bb02985362..3e4ebd8ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.11.3+10 + +* Stop using `dart:mirrors`. + ## 0.11.3+9 * Remove an extra newline in multipart chunks. diff --git a/lib/browser_client.dart b/lib/browser_client.dart index 883b2b184a..309b3ac7e1 100644 --- a/lib/browser_client.dart +++ b/lib/browser_client.dart @@ -15,9 +15,7 @@ import 'src/exception.dart'; import 'src/streamed_response.dart'; // TODO(nweiz): Move this under src/, re-export from lib/http.dart, and use this -// automatically from [new Client] once we can create an HttpRequest using -// mirrors on dart2js (issue 18541) and dart2js doesn't crash on pkg/collection -// (issue 18535). +// automatically from [new Client] once sdk#24581 is fixed. /// A `dart:html`-based HTTP client that runs in the browser and is backed by /// XMLHttpRequests. diff --git a/lib/src/client.dart b/lib/src/client.dart index 70d317e34d..cf1ff784a0 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -8,7 +8,6 @@ import 'dart:typed_data'; import 'base_client.dart'; import 'base_request.dart'; -import 'io.dart' as io; import 'io_client.dart'; import 'response.dart'; import 'streamed_response.dart'; @@ -28,10 +27,7 @@ abstract class 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() { - io.assertSupported("IOClient"); - return new IOClient(); - } + factory Client() => new IOClient(); /// Sends an HTTP HEAD request with the given headers to the given URL, which /// can be a [Uri] or a [String]. diff --git a/lib/src/io.dart b/lib/src/io.dart deleted file mode 100644 index 7c41f99290..0000000000 --- a/lib/src/io.dart +++ /dev/null @@ -1,55 +0,0 @@ -// 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. - -@MirrorsUsed(targets: const ['dart.io.HttpClient', 'dart.io.HttpException', - 'dart.io.File']) -import 'dart:mirrors'; - -/// Whether `dart:io` is supported on this platform. -bool get supported => _library != null; - -/// The `dart:io` library mirror, or `null` if it couldn't be loaded. -final _library = _getLibrary(); - -/// The `dart:io` HttpClient class mirror. -final ClassMirror _httpClient = - _library.declarations[const Symbol('HttpClient')]; - -/// The `dart:io` HttpException class mirror. -final ClassMirror _httpException = - _library.declarations[const Symbol('HttpException')]; - -/// The `dart:io` File class mirror. -final ClassMirror _file = _library.declarations[const Symbol('File')]; - -/// 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) { - if (supported) return; - throw new UnsupportedError("$name isn't supported on this platform."); -} - -/// Creates a new `dart:io` HttpClient instance. -newHttpClient() => _httpClient.newInstance(const Symbol(''), []).reflectee; - -/// Creates a new `dart:io` File instance with the given [path]. -newFile(String path) => _file.newInstance(const Symbol(''), [path]).reflectee; - -/// Returns whether [error] is a `dart:io` HttpException. -bool isHttpException(error) => reflect(error).type.isSubtypeOf(_httpException); - -/// Returns whether [client] is a `dart:io` HttpClient. -bool isHttpClient(client) => reflect(client).type.isSubtypeOf(_httpClient); - -/// Tries to load `dart:io` and returns `null` if it fails. -LibraryMirror _getLibrary() { - try { - return currentMirrorSystem().findLibrary(const Symbol('dart.io')); - } catch (_) { - // TODO(nweiz): narrow the catch clause when issue 18532 is fixed. - return null; - } -} diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart index 7387a30edb..03950ad6fa 100644 --- a/lib/src/io_client.dart +++ b/lib/src/io_client.dart @@ -3,13 +3,13 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:io'; 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. @@ -17,23 +17,10 @@ import 'streamed_response.dart'; /// This is the default client when running on the command line. class IOClient extends BaseClient { /// The underlying `dart:io` HTTP client. - var _inner; + HttpClient _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([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(); - } - } + IOClient([HttpClient inner]) : _inner = inner ?? new HttpClient(); /// Sends an HTTP request and asynchronously returns the response. Future send(BaseRequest request) async { @@ -63,7 +50,7 @@ class IOClient extends BaseClient { return new StreamedResponse( DelegatingStream.typed/*>*/(response).handleError((error) => throw new ClientException(error.message, error.uri), - test: (error) => io.isHttpException(error)), + test: (error) => error is HttpException), response.statusCode, contentLength: response.contentLength == -1 ? null @@ -73,8 +60,7 @@ class IOClient extends BaseClient { isRedirect: response.isRedirect, persistentConnection: response.persistentConnection, reasonPhrase: response.reasonPhrase); - } catch (error) { - if (!io.isHttpException(error)) rethrow; + } on HttpException catch (error) { throw new ClientException(error.message, error.uri); } } diff --git a/lib/src/multipart_file.dart b/lib/src/multipart_file.dart index 3597c6ef59..da4bface78 100644 --- a/lib/src/multipart_file.dart +++ b/lib/src/multipart_file.dart @@ -4,13 +4,13 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; 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 @@ -85,12 +85,12 @@ class MultipartFile { /// 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". + /// Throws an [UnsupportedError] if `dart:io` isn't supported in this + /// environment. static Future fromPath(String field, String filePath, {String filename, MediaType contentType}) async { - io.assertSupported("MultipartFile.fromPath"); if (filename == null) filename = path.basename(filePath); - var file = io.newFile(filePath); + var file = new File(filePath); var length = await file.length(); var stream = new ByteStream(DelegatingStream.typed(file.openRead())); return new MultipartFile(field, stream, length, diff --git a/pubspec.yaml b/pubspec.yaml index 4aa69c1b06..4ae55d8068 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: http -version: 0.11.3+9 +version: 0.11.3+10 author: "Dart Team " homepage: https://github.com/dart-lang/http description: A composable, Future-based API for making HTTP requests. @@ -12,4 +12,4 @@ dependencies: dev_dependencies: unittest: ">=0.9.0 <0.12.0" environment: - sdk: ">=1.9.0 <2.0.0" + sdk: ">=1.22.0 <2.0.0" From 2d2738dbabc38c3b9347589591471e687e3167c5 Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Mon, 6 Mar 2017 12:36:13 -0800 Subject: [PATCH 09/41] Correct the pubspec.yaml, we require 1.23.0 (#58) Closes https://github.com/dart-lang/http/issues/57. --- pubspec.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 4ae55d8068..8f419adbb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: http -version: 0.11.3+10 +version: 0.11.3+11 author: "Dart Team " homepage: https://github.com/dart-lang/http description: A composable, Future-based API for making HTTP requests. @@ -12,4 +12,4 @@ dependencies: dev_dependencies: unittest: ">=0.9.0 <0.12.0" environment: - sdk: ">=1.22.0 <2.0.0" + sdk: ">=1.23.0-dev.0.0 <2.0.0" From e9dbebda39ea48f1f4dc90526854a88ca686c5d7 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 6 Mar 2017 14:55:44 -0800 Subject: [PATCH 10/41] Update Client interface (#56) --- lib/http.dart | 24 +++--- lib/src/base_client.dart | 71 ++++++----------- lib/src/base_request.dart | 140 --------------------------------- lib/src/base_response.dart | 53 ------------- lib/src/client.dart | 11 ++- lib/src/request.dart | 56 +++++++------ lib/src/streamed_request.dart | 41 ---------- lib/src/streamed_response.dart | 39 --------- lib/src/utils.dart | 13 +++ pubspec.yaml | 4 +- 10 files changed, 87 insertions(+), 365 deletions(-) delete mode 100644 lib/src/base_request.dart delete mode 100644 lib/src/base_response.dart delete mode 100644 lib/src/streamed_request.dart delete mode 100644 lib/src/streamed_response.dart diff --git a/lib/http.dart b/lib/http.dart index 9ccd77a6f8..f59c19e732 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -11,8 +11,6 @@ import 'src/client.dart'; import 'src/response.dart'; export 'src/base_client.dart'; -export 'src/base_request.dart'; -export 'src/base_response.dart'; export 'src/byte_stream.dart'; export 'src/client.dart'; export 'src/exception.dart'; @@ -21,8 +19,6 @@ export 'src/multipart_file.dart'; export 'src/multipart_request.dart'; export 'src/request.dart'; export 'src/response.dart'; -export 'src/streamed_request.dart'; -export 'src/streamed_response.dart'; /// Sends an HTTP HEAD request with the given headers to the given URL, which /// can be a [Uri] or a [String]. @@ -65,10 +61,10 @@ Future get(url, {Map headers}) => /// /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. -Future post(url, {Map headers, body, +Future post(url, body, {Map headers, Encoding encoding}) => - _withClient((client) => client.post(url, - headers: headers, body: body, encoding: encoding)); + _withClient((client) => client.post(url, body, + headers: headers, 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]. @@ -89,10 +85,10 @@ Future post(url, {Map headers, body, /// /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. -Future put(url, {Map headers, body, +Future put(url, body, {Map headers, Encoding encoding}) => - _withClient((client) => client.put(url, - headers: headers, body: body, encoding: encoding)); + _withClient((client) => client.put(url, body, + headers: headers, 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]. @@ -113,10 +109,10 @@ Future put(url, {Map headers, body, /// /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. -Future patch(url, {Map headers, body, +Future patch(url, body, {Map headers, Encoding encoding}) => - _withClient((client) => client.patch(url, - headers: headers, body: body, encoding: encoding)); + _withClient((client) => client.patch(url, body, + headers: headers, encoding: encoding)); /// Sends an HTTP DELETE request with the given headers to the given URL, which /// can be a [Uri] or a [String]. @@ -161,7 +157,7 @@ Future read(url, {Map headers}) => Future readBytes(url, {Map headers}) => _withClient((client) => client.readBytes(url, headers: headers)); -Future/**/ _withClient/**/(Future/**/ fn(Client client)) async { +Future _withClient(Future fn(Client client)) async { var client = new Client(); try { return await fn(client); diff --git a/lib/src/base_client.dart b/lib/src/base_client.dart index 7b3fbfa41e..29dd1107e3 100644 --- a/lib/src/base_client.dart +++ b/lib/src/base_client.dart @@ -6,14 +6,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; -import 'package:collection/collection.dart'; +import 'package:async/async.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 @@ -24,14 +22,14 @@ abstract class BaseClient implements Client { /// /// For more fine-grained control over the request, use [send] instead. Future head(url, {Map headers}) => - _sendUnstreamed("HEAD", url, headers); + send(new Request.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]. /// /// For more fine-grained control over the request, use [send] instead. Future get(url, {Map headers}) => - _sendUnstreamed("GET", url, headers); + send(new Request.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]. @@ -51,9 +49,10 @@ abstract class BaseClient implements Client { /// [encoding] defaults to UTF-8. /// /// For more fine-grained control over the request, use [send] instead. - Future post(url, {Map headers, body, + Future post(url, body, {Map headers, Encoding encoding}) => - _sendUnstreamed("POST", url, headers, body, encoding); + send(new Request.post(url, body, headers: headers, + 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]. @@ -73,9 +72,10 @@ abstract class BaseClient implements Client { /// [encoding] defaults to UTF-8. /// /// For more fine-grained control over the request, use [send] instead. - Future put(url, {Map headers, body, + Future put(url, body, {Map headers, Encoding encoding}) => - _sendUnstreamed("PUT", url, headers, body, encoding); + send(new Request.put(url, body, headers: headers, + 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]. @@ -95,16 +95,17 @@ abstract class BaseClient implements Client { /// [encoding] defaults to UTF-8. /// /// For more fine-grained control over the request, use [send] instead. - Future patch(url, {Map headers, body, + Future patch(url, body, {Map headers, Encoding encoding}) => - _sendUnstreamed("PATCH", url, headers, body, encoding); + send(new Request.patch(url, body, headers: headers, + 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 delete(url, {Map headers}) => - _sendUnstreamed("DELETE", url, headers); + send(new Request.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 @@ -115,11 +116,11 @@ abstract class BaseClient implements Client { /// /// For more fine-grained control over the request and response, use [send] or /// [get] instead. - Future read(url, {Map headers}) { - return get(url, headers: headers).then((response) { - _checkResponseSuccess(url, response); - return response.body; - }); + Future read(url, {Map headers}) async { + var response = await get(url, headers: headers); + _checkResponseSuccess(url, response); + + return await response.readAsString(); } /// Sends an HTTP GET request with the given headers to the given URL, which @@ -131,11 +132,11 @@ abstract class BaseClient implements Client { /// /// For more fine-grained control over the request and response, use [send] or /// [get] instead. - Future readBytes(url, {Map headers}) { - return get(url, headers: headers).then((response) { - _checkResponseSuccess(url, response); - return response.bodyBytes; - }); + Future readBytes(url, {Map headers}) async { + var response = await get(url, headers: headers); + _checkResponseSuccess(url, response); + + return await collectBytes(response.read()); } /// Sends an HTTP request and asynchronously returns the response. @@ -145,31 +146,7 @@ abstract class BaseClient implements Client { /// 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. - Future send(BaseRequest request); - - /// Sends a non-streaming [Request] and returns a non-streaming [Response]. - Future _sendUnstreamed(String method, url, - Map headers, [body, Encoding encoding]) async { - - if (url is String) url = Uri.parse(url); - var 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)); - } + Future send(Request request); /// Throws an error if [response] is not successful. void _checkResponseSuccess(url, Response response) { diff --git a/lib/src/base_request.dart b/lib/src/base_request.dart deleted file mode 100644 index b11ef05ae5..0000000000 --- a/lib/src/base_request.dart +++ /dev/null @@ -1,140 +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: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; - - /// 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 headers; - - /// Whether the request has been finalized. - bool get finalized => _finalized; - bool _finalized = false; - - /// Creates a new HTTP request. - BaseRequest(this.method, this.url) - : headers = new LinkedHashMap( - equals: (key1, key2) => key1.toLowerCase() == key2.toLowerCase(), - hashCode: (key) => key.toLowerCase().hashCode); - - /// 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 send() async { - var client = new Client(); - - try { - var response = await client.send(this); - var 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 (_) { - 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."); - } - - String toString() => "$method $url"; -} diff --git a/lib/src/base_response.dart b/lib/src/base_response.dart deleted file mode 100644 index 26427f806f..0000000000 --- a/lib/src/base_response.dart +++ /dev/null @@ -1,53 +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 '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 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 {}, - 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."); - } - } -} diff --git a/lib/src/client.dart b/lib/src/client.dart index cf1ff784a0..fd4144326f 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -7,10 +7,9 @@ import 'dart:convert'; import 'dart:typed_data'; import 'base_client.dart'; -import 'base_request.dart'; import 'io_client.dart'; +import 'request.dart'; import 'response.dart'; -import 'streamed_response.dart'; /// The interface for HTTP clients that take care of maintaining persistent /// connections across multiple requests to the same server. If you only need to @@ -59,7 +58,7 @@ abstract class Client { /// [encoding] defaults to [UTF8]. /// /// For more fine-grained control over the request, use [send] instead. - Future post(url, {Map headers, body, + Future post(url, body, {Map headers, Encoding encoding}); /// Sends an HTTP PUT request with the given headers and body to the given @@ -80,7 +79,7 @@ abstract class Client { /// [encoding] defaults to [UTF8]. /// /// For more fine-grained control over the request, use [send] instead. - Future put(url, {Map headers, body, + Future put(url, body, {Map headers, Encoding encoding}); /// Sends an HTTP PATCH request with the given headers and body to the given @@ -101,7 +100,7 @@ abstract class Client { /// [encoding] defaults to [UTF8]. /// /// For more fine-grained control over the request, use [send] instead. - Future patch(url, {Map headers, body, + Future patch(url, body, {Map headers, Encoding encoding}); /// Sends an HTTP DELETE request with the given headers to the given URL, @@ -133,7 +132,7 @@ abstract class Client { Future readBytes(url, {Map headers}); /// Sends an HTTP request and asynchronously returns the response. - Future send(BaseRequest request); + Future send(Request 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 diff --git a/lib/src/request.dart b/lib/src/request.dart index 1f2be464df..f49c5b9f16 100644 --- a/lib/src/request.dart +++ b/lib/src/request.dart @@ -18,7 +18,8 @@ class Request extends Message { /// The URL to which the request will be sent. final Uri url; - /// Creates a new [Request] for [url] using [method]. + /// Creates a new [Request] for [url], which can be a [Uri] or a [String], + /// using [method]. /// /// [body] is the request body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. If it's a [String], @@ -30,36 +31,37 @@ class Request extends Message { /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request(this.method, this.url, + Request(String method, url, {body, Encoding encoding, Map headers, Map context}) - : super(body, encoding: encoding, headers: headers, context: context); + : this._(method, getUrl(url), body, encoding, headers, context); - /// Creates a new HEAD [Request] to [url]. + /// Creates a new HEAD [Request] to [url], which can be a [Uri] or a [String]. /// /// [headers] are the HTTP headers for the request. If [headers] is `null`, /// it is treated as empty. /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.head(Uri url, + Request.head(url, {Map headers, Map context}) : this('HEAD', url, headers: headers, context: context); - /// Creates a new GET [Request] to [url]. + /// Creates a new GET [Request] to [url], which can be a [Uri] or a [String]. /// /// [headers] are the HTTP headers for the request. If [headers] is `null`, /// it is treated as empty. /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.get(Uri url, + Request.get(url, {Map headers, Map context}) : this('GET', url, headers: headers, context: context); - /// Creates a new POST [Request] to [url]. + /// Creates a new POST [Request] to [url], which can be a [Uri] or a [String]. + /// /// [body] is the request body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. If it's a [String], /// [encoding] is used to encode it to a [Stream>]. It defaults to @@ -70,15 +72,14 @@ class Request extends Message { /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.post(Uri url, - body, + Request.post(url, body, {Encoding encoding, Map headers, Map context}) : this('POST', url, body: body, encoding: encoding, headers: headers, context: context); - /// Creates a new PUT [Request] to [url]. + /// Creates a new PUT [Request] to [url], which can be a [Uri] or a [String]. /// /// [body] is the request body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. If it's a [String], @@ -90,15 +91,15 @@ class Request extends Message { /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.put(Uri url, - body, + Request.put(url, body, {Encoding encoding, Map headers, Map context}) : this('PUT', url, body: body, encoding: encoding, headers: headers, context: context); - /// Creates a new PATCH [Request] to [url]. + /// Creates a new PATCH [Request] to [url], which can be a [Uri] or a + /// [String]. /// /// [body] is the request body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. If it's a [String], @@ -110,25 +111,32 @@ class Request extends Message { /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.patch(Uri url, - body, + Request.patch(url, body, {Encoding encoding, Map headers, Map context}) : this('PATCH', url, body: body, encoding: encoding, headers: headers, context: context); - /// Creates a new DELETE [Request] to [url]. + /// Creates a new DELETE [Request] to [url], which can be a [Uri] or a + /// [String]. /// /// [headers] are the HTTP headers for the request. If [headers] is `null`, /// it is treated as empty. /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.delete(Uri url, + Request.delete(url, {Map headers, Map context}) : this('DELETE', url, headers: headers, context: context); + Request._(this.method, this.url, + body, + Encoding encoding, + Map headers, + Map context) + : super(body, encoding: encoding, headers: headers, context: context); + /// Creates a new [Request] by copying existing values and applying specified /// changes. /// @@ -147,10 +155,12 @@ class Request extends Message { var updatedHeaders = updateMap(this.headers, headers); var updatedContext = updateMap(this.context, context); - return new Request(this.method, this.url, - body: body ?? getBody(this), - encoding: this.encoding, - headers: updatedHeaders, - context: updatedContext); + return new Request._( + this.method, + this.url, + body ?? getBody(this), + this.encoding, + updatedHeaders, + updatedContext); } } diff --git a/lib/src/streamed_request.dart b/lib/src/streamed_request.dart deleted file mode 100644 index 6a020bd77a..0000000000 --- a/lib/src/streamed_request.dart +++ /dev/null @@ -1,41 +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 '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 { - /// 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> get sink => _controller.sink; - - /// The controller for [sink], from which [BaseRequest] will read data for - /// [finalize]. - final StreamController> _controller; - - /// Creates a new streaming request. - StreamedRequest(String method, Uri url) - : _controller = new StreamController>(sync: true), - super(method, url); - - /// Freezes all mutable fields other than [stream] and returns a - /// single-subscription [ByteStream] that emits the data being written to - /// [sink]. - ByteStream finalize() { - super.finalize(); - return new ByteStream(_controller.stream); - } -} diff --git a/lib/src/streamed_response.dart b/lib/src/streamed_response.dart deleted file mode 100644 index 69d8356c1d..0000000000 --- a/lib/src/streamed_response.dart +++ /dev/null @@ -1,39 +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 '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> stream, - int statusCode, - {int contentLength, - BaseRequest request, - Map headers: const {}, - 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); -} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 39f1aefac4..b7e7347a3d 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -170,3 +170,16 @@ String getHeader(Map headers, String name) { } return null; } + +/// Returns a [Uri] from the [url], which can be a [Uri] or a [String]. +/// +/// If the [url] is not a [Uri] or [String] an [ArgumentError] is thrown. +Uri getUrl(url) { + if (url is Uri) { + return url; + } else if (url is String) { + return Uri.parse(url); + } else { + throw new ArgumentError.value(url, 'url', 'Not a Uri or String'); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 8f419adbb6..f059565133 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,10 +1,10 @@ name: http -version: 0.11.3+11 +version: 0.12.0-dev author: "Dart Team " homepage: https://github.com/dart-lang/http description: A composable, Future-based API for making HTTP requests. dependencies: - async: "^1.10.0" + async: "^1.13.0" collection: "^1.5.0" http_parser: ">=0.0.1 <4.0.0" path: ">=0.9.0 <2.0.0" From 128c0bc37c96b2b41f3a313727214c834fc5e494 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 23 May 2017 14:50:01 -0700 Subject: [PATCH 11/41] Add back missing Request and Response fields (#78) --- lib/src/response.dart | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/lib/src/response.dart b/lib/src/response.dart index c94cb0b46c..7a7cfe2a07 100644 --- a/lib/src/response.dart +++ b/lib/src/response.dart @@ -11,10 +11,20 @@ import 'utils.dart'; /// An HTTP response where the entire response body is known in advance. class Response extends Message { + /// The location of the requested resource. + /// + /// The value takes into account any redirects that occurred during the + /// request. + final Uri finalUrl; + /// The status code of the response. final int statusCode; - /// Creates a new HTTP response with the given [statusCode]. + /// The reason phrase associated with the status code. + final String reasonPhrase; + + /// Creates a new HTTP response for a resource at the [finalUrl], which can + /// be a [Uri] or a [String], with the given [statusCode]. /// /// [body] is the request body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. If it's a [String], @@ -26,11 +36,20 @@ class Response extends Message { /// /// Extra [context] can be used to pass information between outer middleware /// and handlers. - Response(this.statusCode, - {body, + Response(finalUrl, int statusCode, + {String reasonPhrase, + body, Encoding encoding, Map headers, Map context}) + : this._(getUrl(finalUrl), statusCode, reasonPhrase ?? '', + body, encoding, headers, context); + + Response._(this.finalUrl, this.statusCode, this.reasonPhrase, + body, + Encoding encoding, + Map headers, + Map context) : super(body, encoding: encoding, headers: headers, context: context); /// Creates a new [Response] by copying existing values and applying specified @@ -55,10 +74,14 @@ class Response extends Message { var updatedHeaders = updateMap(this.headers, headers); var updatedContext = updateMap(this.context, context); - return new Response(this.statusCode, - body: body ?? getBody(this), - headers: updatedHeaders, - context: updatedContext); + return new Response._( + this.finalUrl, + this.statusCode, + this.reasonPhrase, + body ?? getBody(this), + this.encoding, + updatedHeaders, + updatedContext); } /// The date and time after which the response's data should be considered From 11fca7bd8ea3de820d819668c64df1d8de7fe38a Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 24 May 2017 15:55:39 -0700 Subject: [PATCH 12/41] Add a dart:io client (#82) --- lib/src/io_client.dart | 56 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart index 03950ad6fa..99d52c4e1e 100644 --- a/lib/src/io_client.dart +++ b/lib/src/io_client.dart @@ -8,9 +8,9 @@ import 'dart:io'; import 'package:async/async.dart'; import 'base_client.dart'; -import 'base_request.dart'; import 'exception.dart'; -import 'streamed_response.dart'; +import 'request.dart'; +import 'response.dart'; /// A `dart:io`-based HTTP client. /// @@ -23,45 +23,42 @@ class IOClient extends BaseClient { IOClient([HttpClient inner]) : _inner = inner ?? new HttpClient(); /// Sends an HTTP request and asynchronously returns the response. - Future send(BaseRequest request) async { - var stream = request.finalize(); - + Future send(Request request) async { try { var ioRequest = await _inner.openUrl(request.method, request.url); + var context = request.context; ioRequest - ..followRedirects = request.followRedirects - ..maxRedirects = request.maxRedirects + ..followRedirects = context['io.followRedirects'] ?? true + ..maxRedirects = context['io.maxRedirects'] ?? 5 ..contentLength = request.contentLength == null ? -1 : request.contentLength - ..persistentConnection = request.persistentConnection; + ..persistentConnection = context['io.persistentConnection'] ?? true; request.headers.forEach((name, value) { ioRequest.headers.set(name, value); }); - var response = await stream.pipe( - DelegatingStreamConsumer.typed(ioRequest)); + request.read().pipe(DelegatingStreamConsumer.typed>(ioRequest)); + var response = await ioRequest.done; + var headers = {}; response.headers.forEach((key, values) { headers[key] = values.join(','); }); - return new StreamedResponse( - DelegatingStream.typed/*>*/(response).handleError((error) => - throw new ClientException(error.message, error.uri), - test: (error) => error is HttpException), + return new Response( + _responseUrl(request, response), response.statusCode, - contentLength: response.contentLength == -1 - ? null - : response.contentLength, - request: request, - headers: headers, - isRedirect: response.isRedirect, - persistentConnection: response.persistentConnection, - reasonPhrase: response.reasonPhrase); + reasonPhrase: response.reasonPhrase, + body: DelegatingStream.typed>(response).handleError( + (error) => throw new ClientException(error.message, error.uri), + test: (error) => error is HttpException), + headers: headers); } on HttpException catch (error) { throw new ClientException(error.message, error.uri); + } on SocketException catch (error) { + throw new ClientException(error.message, request.url); } } @@ -71,4 +68,19 @@ class IOClient extends BaseClient { if (_inner != null) _inner.close(force: true); _inner = null; } + + /// Determines the finalUrl retrieved by evaluating any redirects received in + /// the [response] based on the initial [request]. + Uri _responseUrl(Request request, HttpClientResponse response) { + var finalUrl = request.url; + + for (var redirect in response.redirects) { + var location = redirect.location; + + // Redirects can either be absolute or relative + finalUrl = location.isAbsolute ? location : finalUrl.resolveUri(location); + } + + return finalUrl; + } } From 2168f61244022a03f5cc8961d368f28bd1abfdbb Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 24 May 2017 17:21:50 -0700 Subject: [PATCH 13/41] Add a dart:html client (#83) --- lib/browser_client.dart | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/lib/browser_client.dart b/lib/browser_client.dart index 309b3ac7e1..ff3c80ce6a 100644 --- a/lib/browser_client.dart +++ b/lib/browser_client.dart @@ -6,13 +6,13 @@ import 'dart:async'; import 'dart:html'; import 'dart:typed_data'; +import 'package:async/async.dart'; import 'package:stack_trace/stack_trace.dart'; import 'src/base_client.dart'; -import 'src/base_request.dart'; -import 'src/byte_stream.dart'; import 'src/exception.dart'; -import 'src/streamed_response.dart'; +import 'src/request.dart'; +import 'src/response.dart'; // TODO(nweiz): Move this under src/, re-export from lib/http.dart, and use this // automatically from [new Client] once sdk#24581 is fixed. @@ -34,23 +34,17 @@ class BrowserClient extends BaseClient { /// Creates a new HTTP client. BrowserClient(); - /// Whether to send credentials such as cookies or authorization headers for - /// cross-site requests. - /// - /// Defaults to `false`. - bool withCredentials = false; - /// Sends an HTTP request and asynchronously returns the response. - Future send(BaseRequest request) async { - var bytes = await request.finalize().toBytes(); + Future send(Request request) async { + var bytes = await collectBytes(request.read()); var xhr = new HttpRequest(); _xhrs.add(xhr); _openHttpRequest(xhr, request.method, request.url.toString(), asynch: true); xhr.responseType = 'blob'; - xhr.withCredentials = withCredentials; + xhr.withCredentials = request.context['html.withCredentials'] ?? false; request.headers.forEach(xhr.setRequestHeader); - var completer = new Completer(); + var completer = new Completer(); xhr.onLoad.first.then((_) { // TODO(nweiz): Set the response type to "arraybuffer" when issue 18542 // is fixed. @@ -59,13 +53,12 @@ class BrowserClient extends BaseClient { reader.onLoad.first.then((_) { var body = reader.result as Uint8List; - completer.complete(new StreamedResponse( - new ByteStream.fromBytes(body), + completer.complete(new Response( + xhr.responseUrl, xhr.status, - contentLength: body.length, - request: request, - headers: xhr.responseHeaders, - reasonPhrase: xhr.statusText)); + reasonPhrase: xhr.statusText, + body: new Stream.fromIterable([body]), + headers: xhr.responseHeaders)); }); reader.onError.first.then((error) { From 5d6ce288412fd7581b2b904f0b81467502417e1a Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 24 May 2017 17:22:09 -0700 Subject: [PATCH 14/41] Remove ByteStream (#84) --- lib/http.dart | 1 - lib/src/byte_stream.dart | 36 ------------------------------------ lib/src/utils.dart | 8 -------- 3 files changed, 45 deletions(-) delete mode 100644 lib/src/byte_stream.dart diff --git a/lib/http.dart b/lib/http.dart index f59c19e732..d098a47e27 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -11,7 +11,6 @@ import 'src/client.dart'; import 'src/response.dart'; export 'src/base_client.dart'; -export 'src/byte_stream.dart'; export 'src/client.dart'; export 'src/exception.dart'; export 'src/io_client.dart'; diff --git a/lib/src/byte_stream.dart b/lib/src/byte_stream.dart deleted file mode 100644 index a9d47b075f..0000000000 --- a/lib/src/byte_stream.dart +++ /dev/null @@ -1,36 +0,0 @@ -// 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> { - ByteStream(Stream> stream) - : super(stream); - - /// Returns a single-subscription byte stream that will emit the given bytes - /// in a single chunk. - factory ByteStream.fromBytes(List bytes) => - new ByteStream(new Stream.fromIterable([bytes])); - - /// Collects the data of this stream in a [Uint8List]. - Future toBytes() { - var completer = new Completer(); - var sink = new ByteConversionSink.withCallback((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 bytesToString([Encoding encoding=UTF8]) => - encoding.decodeStream(this); - - Stream toStringStream([Encoding encoding=UTF8]) => - encoding.decoder.bind(this); -} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index b7e7347a3d..c19d354b9c 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -8,7 +8,6 @@ import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'byte_stream.dart'; import 'http_unmodifiable_map.dart'; /// Returns a [Map] with the values from [original] and the values from @@ -93,13 +92,6 @@ Uint8List toUint8List(List input) { return new Uint8List.fromList(input); } -/// If [stream] is already a [ByteStream], returns it. Otherwise, wraps it in a -/// [ByteStream]. -ByteStream toByteStream(Stream> 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. From 5466bac597f7212d96e76de9cebd49d9a3c5b850 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 22 Jun 2017 13:55:24 -0700 Subject: [PATCH 15/41] Adding middleware (#85) --- lib/src/client.dart | 11 +++++++ lib/src/handler.dart | 15 +++++++++ lib/src/handler_client.dart | 31 +++++++++++++++++ lib/src/middleware.dart | 66 +++++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 +- 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 lib/src/handler.dart create mode 100644 lib/src/handler_client.dart create mode 100644 lib/src/middleware.dart diff --git a/lib/src/client.dart b/lib/src/client.dart index fd4144326f..82b4dc4bed 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -7,6 +7,8 @@ import 'dart:convert'; import 'dart:typed_data'; import 'base_client.dart'; +import 'handler.dart'; +import 'handler_client.dart'; import 'io_client.dart'; import 'request.dart'; import 'response.dart'; @@ -28,6 +30,15 @@ abstract class Client { /// [BrowserClient] if `dart:html` is available. factory Client() => new IOClient(); + /// Creates a new [Client] from a [handler] callback. + /// + /// The [handler] is a function that receives a [Request] and returns a + /// [Future]. It will be called when [Client.send] is invoked. + /// + /// When [Client.close] is called the [onClose] function will be called. + factory Client.handler(Handler handler, {void onClose()}) + => new HandlerClient(handler, onClose ?? () {}); + /// Sends an HTTP HEAD request with the given headers to the given URL, which /// can be a [Uri] or a [String]. /// diff --git a/lib/src/handler.dart b/lib/src/handler.dart new file mode 100644 index 0000000000..f1d3a83f3b --- /dev/null +++ b/lib/src/handler.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2017, 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 'request.dart'; +import 'response.dart'; + +/// The signature of a function which handles a [Request] and returns a +/// [Future]. +/// +/// A [Handler] may call an underlying HTTP implementation, or it may wrap +/// another [Handler] or a [Client]. +typedef Future Handler(Request request); diff --git a/lib/src/handler_client.dart b/lib/src/handler_client.dart new file mode 100644 index 0000000000..475a3067e5 --- /dev/null +++ b/lib/src/handler_client.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2017, 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 'base_client.dart'; +import 'handler.dart'; +import 'request.dart'; +import 'response.dart'; + +/// A [Handler]-based HTTP client. +/// +/// The [HandlerClient] allows composition of a [Client] within a larger +/// application. +class HandlerClient extends BaseClient { + final Handler _handler; + final void Function() _close; + + /// Creates a new client using the [_handler] and [onClose] functions. + HandlerClient(this._handler, void onClose()) + : _close = onClose; + + /// Sends an HTTP request and asynchronously returns the response. + Future send(Request request) => _handler(request); + + /// Closes the client and cleans up any resources associated with it. + void close() { + _close(); + } +} diff --git a/lib/src/middleware.dart b/lib/src/middleware.dart new file mode 100644 index 0000000000..5635a5285c --- /dev/null +++ b/lib/src/middleware.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2017, 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 'client.dart'; +import 'handler_client.dart'; +import 'request.dart'; +import 'response.dart'; + +/// A function which creates a new [Client] by wrapping a [Client]. +/// +/// You can extend the functions of a [Client] by wrapping it in [Middleware] +/// that can intercept and process a HTTP request before it it sent to a +/// client, a response after it is received by a client, or both. +/// +/// Because [Middleware] consumes a [Client] and returns a new [Client], +/// multiple [Middleware] instances can be composed together to offer rich +/// functionality. +/// +/// Common uses for middleware include caching, logging, and authentication. +/// +/// A simple [Middleware] can be created using [createMiddleware]. +typedef Client Middleware(Client inner); + +/// Creates a [Middleware] using the provided functions. +/// +/// If provided, [requestHandler] receives a [Request]. It replies to the +/// request by returning a [Future]. The modified [Request] is then +/// sent to the inner [Client]. +/// +/// If provided, [responseHandler] is called with the [Response] generated +/// by the inner [Client]. It replies to the response by returning a +/// [Future requestHandler(Request request), + Future responseHandler(Response response), + void onClose(), + void errorHandler(error, [StackTrace stackTrace]) +}) { + requestHandler ??= (request) async => request; + responseHandler ??= (response) async => response; + + return (inner) { + return new HandlerClient( + (request) => + requestHandler(request) + .then((req) => inner.send(req)) + .then((res) => responseHandler(res), onError: errorHandler), + onClose == null + ? inner.close + : () { + onClose(); + inner.close(); + }, + ); + }; +} diff --git a/pubspec.yaml b/pubspec.yaml index f059565133..10e970bec8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,4 +12,4 @@ dependencies: dev_dependencies: unittest: ">=0.9.0 <0.12.0" environment: - sdk: ">=1.23.0-dev.0.0 <2.0.0" + sdk: ">=1.24.0-dev.0.0 <2.0.0" From 866628b68972863a4da689fece684df4d63edfd0 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 23 Jun 2017 13:22:07 -0700 Subject: [PATCH 16/41] Add Pipeline (#87) --- lib/src/pipeline.dart | 50 ++++++++++++++++++ test/pipeline_test.dart | 112 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 lib/src/pipeline.dart create mode 100644 test/pipeline_test.dart diff --git a/lib/src/pipeline.dart b/lib/src/pipeline.dart new file mode 100644 index 0000000000..d7fe1e4d70 --- /dev/null +++ b/lib/src/pipeline.dart @@ -0,0 +1,50 @@ +k// Copyright (c) 2017, 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 'client.dart'; +import 'handler.dart'; +import 'middleware.dart'; + +/// A helper that makes it easy to compose a set of [Middleware] and a +/// [Client]. +/// +/// var client = const Pipeline() +/// .addMiddleware(loggingMiddleware) +/// .addMiddleware(basicAuthMiddleware) +/// .addClient(new Client()); +class Pipeline { + /// The outer pipeline. + final Pipeline _parent; + + /// The [Middleware] that is invoked at this stage. + final Middleware _middleware; + + const Pipeline() + : _parent = null, + _middleware = null; + + Pipeline._(this._parent, this._middleware); + + /// Returns a new [Pipeline] with [middleware] added to the existing set of + /// [Middleware]. + /// + /// [middleware] will be the last [Middleware] to process a request and + /// the first to process a response. + Pipeline addMiddleware(Middleware middleware) => + new Pipeline._(this, middleware); + + /// Returns a new [Client] with [client] as the final processor of a + /// [Request] if all of the middleware in the pipeline have passed the request + /// through. + Client addClient(Client client) => + _middleware == null ? client : _parent.addClient(_middleware(client)); + + /// Returns a new [Client] with [handler] as the final processor of a + /// [Request] if all of the middleware in the pipeline have passed the request + /// through. + Client addHandler(Handler handler) => addClient(new Client.handler(handler)); + + /// Exposes this pipeline of [Middleware] as a single middleware instance. + Middleware get middleware => addClient; +} diff --git a/test/pipeline_test.dart b/test/pipeline_test.dart new file mode 100644 index 0000000000..cbde3a62e3 --- /dev/null +++ b/test/pipeline_test.dart @@ -0,0 +1,112 @@ +// Copyright (c) 2017, 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 'package:test/test.dart'; + +import 'package:http/http.dart'; + +void main() { + test('compose middleware with Pipeline', () async { + var accessLocation = 0; + + var middlewareA = createMiddleware(requestHandler: (request) async { + expect(accessLocation, 0); + accessLocation = 1; + return request; + }, responseHandler: (response) async { + expect(accessLocation, 4); + accessLocation = 5; + return response; + }); + + var middlewareB = createMiddleware(requestHandler: (request) async { + expect(accessLocation, 1); + accessLocation = 2; + return request; + }, responseHandler: (response) async { + expect(accessLocation, 3); + accessLocation = 4; + return response; + }); + + var client = const Pipeline() + .addMiddleware(middlewareA) + .addMiddleware(middlewareB) + .addClient(new Client.handler((request) async { + expect(accessLocation, 2); + accessLocation = 3; + return new Response(Uri.parse('dart:http'), 200); + })); + + var response = await client.get(Uri.parse('dart:http')); + + expect(response, isNotNull); + expect(accessLocation, 5); + }); + + test('Pipeline can be used as middleware', () async { + int accessLocation = 0; + + var middlewareA = createMiddleware(requestHandler: (request) async { + expect(accessLocation, 0); + accessLocation = 1; + return request; + }, responseHandler: (response) async { + expect(accessLocation, 4); + accessLocation = 5; + return response; + }); + + var middlewareB = createMiddleware(requestHandler: (request) async { + expect(accessLocation, 1); + accessLocation = 2; + return request; + }, responseHandler: (response) async { + expect(accessLocation, 3); + accessLocation = 4; + return response; + }); + + var innerPipeline = + const Pipeline().addMiddleware(middlewareA).addMiddleware(middlewareB); + + var client = const Pipeline() + .addMiddleware(innerPipeline.middleware) + .addClient(new Client.handler((request) async { + expect(accessLocation, 2); + accessLocation = 3; + return new Response(Uri.parse('dart:http'), 200); + })); + + var response = await client.get(Uri.parse('dart:http')); + + expect(response, isNotNull); + expect(accessLocation, 5); + }); + + test('Pipeline calls close on all middleware', () { + int accessLocation = 0; + + var middlewareA = createMiddleware(onClose: () { + expect(accessLocation, 0); + accessLocation = 1; + }); + + var middlewareB = createMiddleware(onClose: () { + expect(accessLocation, 1); + accessLocation = 2; + }); + + var client = const Pipeline() + .addMiddleware(middlewareA) + .addMiddleware(middlewareB) + .addClient(new Client.handler((request) async => null, onClose: () { + expect(accessLocation, 2); + accessLocation = 3; + })); + + client.close(); + expect(accessLocation, 3); + }); +} From dc9262ec9ad7265f865ddf237d66c4ee4dcdee50 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 23 Jun 2017 13:35:27 -0700 Subject: [PATCH 17/41] Add tests for Message (#90) --- lib/http.dart | 3 +- pubspec.yaml | 5 +- test/message_change_test.dart | 100 ++++++++++ test/message_test.dart | 343 ++++++++++++++++++++++++++++++++++ 4 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 test/message_change_test.dart create mode 100644 test/message_test.dart diff --git a/lib/http.dart b/lib/http.dart index d098a47e27..6b80d7f1eb 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -14,8 +14,7 @@ export 'src/base_client.dart'; export 'src/client.dart'; export 'src/exception.dart'; export 'src/io_client.dart'; -export 'src/multipart_file.dart'; -export 'src/multipart_request.dart'; +export 'src/middleware.dart'; export 'src/request.dart'; export 'src/response.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 10e970bec8..1f678acf01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,9 @@ dependencies: path: ">=0.9.0 <2.0.0" stack_trace: ">=0.9.1 <2.0.0" dev_dependencies: - unittest: ">=0.9.0 <0.12.0" + test: "^0.12.18" +# Override dependency on package_resolver to enable test +dependency_overrides: + package_resolver: '^1.0.0' environment: sdk: ">=1.24.0-dev.0.0 <2.0.0" diff --git a/test/message_change_test.dart b/test/message_change_test.dart new file mode 100644 index 0000000000..8c5942d191 --- /dev/null +++ b/test/message_change_test.dart @@ -0,0 +1,100 @@ +// Copyright (c) 2017, 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:test/test.dart'; + +import 'package:http/http.dart'; +import 'package:http/src/message.dart'; + +final _uri = Uri.parse('http://localhost/'); + +void main() { + group('Request', () { + _testChange(({body, headers, context}) { + return new Request('GET', _uri, + body: body, headers: headers, context: context); + }); + }); + + group('Response', () { + _testChange(({body, headers, context}) { + return new Response(_uri, 200, + body: body, headers: headers, context: context); + }); + }); +} + +/// Shared test method used by [Request] and [Response] tests to validate +/// the behavior of `change` with different `headers` and `context` values. +void _testChange( + Message factory( + {body, Map headers, Map context})) { + group('body', () { + test('with String', () async { + var request = factory(body: 'Hello, world'); + var copy = request.change(body: 'Goodbye, world'); + + var newBody = await copy.readAsString(); + + expect(newBody, equals('Goodbye, world')); + }); + + test('with Stream', () async { + var request = factory(body: 'Hello, world'); + var copy = request.change( + body: new Stream.fromIterable(['Goodbye, world']) + .transform(UTF8.encoder)); + + var newBody = await copy.readAsString(); + + expect(newBody, equals('Goodbye, world')); + }); + }); + + test('with empty headers returns identical instance', () { + var request = factory(headers: {'header1': 'header value 1'}); + var copy = request.change(headers: {}); + + expect(copy.headers, same(request.headers)); + }); + + test('with empty context returns identical instance', () { + var request = factory(context: {'context1': 'context value 1'}); + var copy = request.change(context: {}); + + expect(copy.context, same(request.context)); + }); + + test('new header values are added', () { + var request = factory(headers: {'test': 'test value'}); + var copy = request.change(headers: {'test2': 'test2 value'}); + + expect(copy.headers, + {'test': 'test value', 'test2': 'test2 value', 'content-length': '0'}); + }); + + test('existing header values are overwritten', () { + var request = factory(headers: {'test': 'test value'}); + var copy = request.change(headers: {'test': 'new test value'}); + + expect(copy.headers, {'test': 'new test value', 'content-length': '0'}); + }); + + test('new context values are added', () { + var request = factory(context: {'test': 'test value'}); + var copy = request.change(context: {'test2': 'test2 value'}); + + expect(copy.context, {'test': 'test value', 'test2': 'test2 value'}); + }); + + test('existing context values are overwritten', () { + var request = factory(context: {'test': 'test value'}); + var copy = request.change(context: {'test': 'new test value'}); + + expect(copy.context, {'test': 'new test value'}); + }); +} diff --git a/test/message_test.dart b/test/message_test.dart new file mode 100644 index 0000000000..c8d6474176 --- /dev/null +++ b/test/message_test.dart @@ -0,0 +1,343 @@ +// Copyright (c) 2017, 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:test/test.dart'; + +import 'package:http/src/message.dart'; + +// "hello," +const HELLO_BYTES = const [104, 101, 108, 108, 111, 44]; + +// " world" +const WORLD_BYTES = const [32, 119, 111, 114, 108, 100]; + +class _TestMessage extends Message { + _TestMessage(Map headers, Map context, body, + Encoding encoding) + : super(body, headers: headers, context: context, encoding: encoding); + + Message change( + {Map headers, Map context, body}) { + throw new UnimplementedError(); + } +} + +Message _createMessage( + {Map headers, + Map context, + body, + Encoding encoding}) { + return new _TestMessage(headers, context, body, encoding); +} + +void main() { + group('headers', () { + test('message headers are case insensitive', () { + var message = _createMessage(headers: {'foo': 'bar'}); + + expect(message.headers, containsPair('foo', 'bar')); + expect(message.headers, containsPair('Foo', 'bar')); + expect(message.headers, containsPair('FOO', 'bar')); + }); + + test('null header value becomes default', () { + var message = _createMessage(); + expect(message.headers.containsKey('content-length'), isFalse); + expect(message.headers, same(_createMessage().headers)); + expect(() => message.headers['h1'] = 'value1', throwsUnsupportedError); + }); + + test('headers are immutable', () { + var message = _createMessage(headers: {'h1': 'value1'}); + expect(() => message.headers['h1'] = 'value1', throwsUnsupportedError); + expect(() => message.headers['h1'] = 'value2', throwsUnsupportedError); + expect(() => message.headers['h2'] = 'value2', throwsUnsupportedError); + }); + }); + + group('context', () { + test('is accessible', () { + var message = _createMessage(context: {'foo': 'bar'}); + expect(message.context, containsPair('foo', 'bar')); + }); + + test('null context value becomes empty and immutable', () { + var message = _createMessage(); + expect(message.context, isEmpty); + expect(() => message.context['key'] = 'value', throwsUnsupportedError); + }); + + test('is immutable', () { + var message = _createMessage(context: {'key': 'value'}); + expect(() => message.context['key'] = 'value', throwsUnsupportedError); + expect(() => message.context['key2'] = 'value', throwsUnsupportedError); + }); + }); + + group("readAsString", () { + test("supports a null body", () { + var request = _createMessage(); + expect(request.readAsString(), completion(equals(""))); + }); + + test("supports a Stream> body", () { + var controller = new StreamController(); + var request = _createMessage(body: controller.stream); + expect(request.readAsString(), completion(equals("hello, world"))); + + controller.add(HELLO_BYTES); + return new Future(() { + controller + ..add(WORLD_BYTES) + ..close(); + }); + }); + + test("defaults to UTF-8", () { + var request = _createMessage( + body: new Stream.fromIterable([ + [195, 168] + ])); + expect(request.readAsString(), completion(equals("è"))); + }); + + test("the content-type header overrides the default", () { + var request = _createMessage( + headers: {'content-type': 'text/plain; charset=iso-8859-1'}, + body: new Stream.fromIterable([ + [195, 168] + ])); + expect(request.readAsString(), completion(equals("è"))); + }); + + test("an explicit encoding overrides the content-type header", () { + var request = _createMessage( + headers: {'content-type': 'text/plain; charset=iso-8859-1'}, + body: new Stream.fromIterable([ + [195, 168] + ])); + expect(request.readAsString(LATIN1), completion(equals("è"))); + }); + }); + + group("read", () { + test("supports a null body", () { + var request = _createMessage(); + expect(request.read().toList(), completion(isEmpty)); + }); + + test("supports a Stream> body", () { + var controller = new StreamController(); + var request = _createMessage(body: controller.stream); + expect(request.read().toList(), + completion(equals([HELLO_BYTES, WORLD_BYTES]))); + + controller.add(HELLO_BYTES); + return new Future(() { + controller + ..add(WORLD_BYTES) + ..close(); + }); + }); + + test("supports a List body", () { + var request = _createMessage(body: HELLO_BYTES); + expect(request.read().toList(), completion(equals([HELLO_BYTES]))); + }); + + test("throws when calling read()/readAsString() multiple times", () { + var request; + + request = _createMessage(); + expect(request.read().toList(), completion(isEmpty)); + expect(() => request.read(), throwsStateError); + + request = _createMessage(); + expect(request.readAsString(), completion(isEmpty)); + expect(() => request.readAsString(), throwsStateError); + + request = _createMessage(); + expect(request.readAsString(), completion(isEmpty)); + expect(() => request.read(), throwsStateError); + + request = _createMessage(); + expect(request.read().toList(), completion(isEmpty)); + expect(() => request.readAsString(), throwsStateError); + }); + }); + + group("content-length", () { + test("is null with a default body and without a content-length header", () { + var request = _createMessage(); + expect(request.contentLength, isNull); + }); + + test("comes from a byte body", () { + var request = _createMessage(body: [1, 2, 3]); + expect(request.contentLength, 3); + expect(request.isEmpty, isFalse); + }); + + test("comes from a string body", () { + var request = _createMessage(body: 'foobar'); + expect(request.contentLength, 6); + expect(request.isEmpty, isFalse); + }); + + test("is set based on byte length for a string body", () { + var request = _createMessage(body: 'fööbär'); + expect(request.contentLength, 9); + expect(request.isEmpty, isFalse); + + request = _createMessage(body: 'fööbär', encoding: LATIN1); + expect(request.contentLength, 6); + expect(request.isEmpty, isFalse); + }); + + test("is null for a stream body", () { + var request = _createMessage(body: new Stream.empty()); + expect(request.contentLength, isNull); + }); + + test("uses the content-length header for a stream body", () { + var request = _createMessage( + body: new Stream.empty(), headers: {'content-length': '42'}); + expect(request.contentLength, 42); + expect(request.isEmpty, isFalse); + }); + + test("real body length takes precedence over content-length header", () { + var request = + _createMessage(body: [1, 2, 3], headers: {'content-length': '42'}); + expect(request.contentLength, 3); + expect(request.isEmpty, isFalse); + }); + + test("is null for a chunked transfer encoding", () { + var request = _createMessage( + body: "1\r\na0\r\n\r\n", headers: {'transfer-encoding': 'chunked'}); + expect(request.contentLength, isNull); + }); + + test("is null for a non-identity transfer encoding", () { + var request = _createMessage( + body: "1\r\na0\r\n\r\n", headers: {'transfer-encoding': 'custom'}); + expect(request.contentLength, isNull); + }); + + test("is set for identity transfer encoding", () { + var request = _createMessage( + body: "1\r\na0\r\n\r\n", headers: {'transfer-encoding': 'identity'}); + expect(request.contentLength, equals(9)); + expect(request.isEmpty, isFalse); + }); + }); + + group("mimeType", () { + test("is null without a content-type header", () { + expect(_createMessage().mimeType, isNull); + }); + + test("comes from the content-type header", () { + expect(_createMessage(headers: {'content-type': 'text/plain'}).mimeType, + equals('text/plain')); + }); + + test("doesn't include parameters", () { + expect( + _createMessage( + headers: {'content-type': 'text/plain; foo=bar; bar=baz'}) + .mimeType, + equals('text/plain')); + }); + }); + + group("encoding", () { + test("is null without a content-type header", () { + expect(_createMessage().encoding, isNull); + }); + + test("is null without a charset parameter", () { + expect(_createMessage(headers: {'content-type': 'text/plain'}).encoding, + isNull); + }); + + test("is null with an unrecognized charset parameter", () { + expect( + _createMessage( + headers: {'content-type': 'text/plain; charset=fblthp'}).encoding, + isNull); + }); + + test("comes from the content-type charset parameter", () { + expect( + _createMessage( + headers: {'content-type': 'text/plain; charset=iso-8859-1'}) + .encoding, + equals(LATIN1)); + }); + + test("comes from the content-type charset parameter with a different case", + () { + expect( + _createMessage( + headers: {'Content-Type': 'text/plain; charset=iso-8859-1'}) + .encoding, + equals(LATIN1)); + }); + + test("defaults to encoding a String as UTF-8", () { + expect( + _createMessage(body: "è").read().toList(), + completion(equals([ + [195, 168] + ]))); + }); + + test("uses the explicit encoding if available", () { + expect( + _createMessage(body: "è", encoding: LATIN1).read().toList(), + completion(equals([ + [232] + ]))); + }); + + test("adds an explicit encoding to the content-type", () { + var request = _createMessage( + body: "è", encoding: LATIN1, headers: {'content-type': 'text/plain'}); + expect(request.headers, + containsPair('content-type', 'text/plain; charset=iso-8859-1')); + }); + + test("adds an explicit encoding to the content-type with a different case", + () { + var request = _createMessage( + body: "è", encoding: LATIN1, headers: {'Content-Type': 'text/plain'}); + expect(request.headers, + containsPair('Content-Type', 'text/plain; charset=iso-8859-1')); + }); + + test( + "sets an absent content-type to application/octet-stream in order to " + "set the charset", () { + var request = _createMessage(body: "è", encoding: LATIN1); + expect( + request.headers, + containsPair( + 'content-type', 'application/octet-stream; charset=iso-8859-1')); + }); + + test("overwrites an existing charset if given an explicit encoding", () { + var request = _createMessage( + body: "è", + encoding: LATIN1, + headers: {'content-type': 'text/plain; charset=whatever'}); + expect(request.headers, + containsPair('content-type', 'text/plain; charset=iso-8859-1')); + }); + }); +} From 00a9780146e4473a5f1922bebea48b729ce0380a Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 23 Jun 2017 13:55:59 -0700 Subject: [PATCH 18/41] Remove old unittest tests --- test/html/client_test.dart | 35 --- test/html/streamed_request_test.dart | 36 --- test/html/utils.dart | 11 - test/io/client_test.dart | 98 ------ test/io/http_test.dart | 436 --------------------------- test/io/multipart_test.dart | 41 --- test/io/request_test.dart | 59 ---- test/io/streamed_request_test.dart | 56 ---- test/io/utils.dart | 149 --------- test/mock_client_test.dart | 61 ---- test/multipart_test.dart | 234 -------------- test/request_test.dart | 354 ---------------------- test/response_test.dart | 75 ----- test/streamed_request_test.dart | 28 -- 14 files changed, 1673 deletions(-) delete mode 100644 test/html/client_test.dart delete mode 100644 test/html/streamed_request_test.dart delete mode 100644 test/html/utils.dart delete mode 100644 test/io/client_test.dart delete mode 100644 test/io/http_test.dart delete mode 100644 test/io/multipart_test.dart delete mode 100644 test/io/request_test.dart delete mode 100644 test/io/streamed_request_test.dart delete mode 100644 test/io/utils.dart delete mode 100644 test/mock_client_test.dart delete mode 100644 test/multipart_test.dart delete mode 100644 test/request_test.dart delete mode 100644 test/response_test.dart delete mode 100644 test/streamed_request_test.dart diff --git a/test/html/client_test.dart b/test/html/client_test.dart deleted file mode 100644 index ab806546e5..0000000000 --- a/test/html/client_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -// 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 'package:http/http.dart' as http; -import 'package:http/browser_client.dart'; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - test('#send a StreamedRequest', () { - var client = new BrowserClient(); - var request = new http.StreamedRequest("POST", echoUrl); - - expect(client.send(request).then((response) { - return response.stream.bytesToString(); - }).whenComplete(client.close), completion(equals('{"hello": "world"}'))); - - request.sink.add('{"hello": "world"}'.codeUnits); - request.sink.close(); - }); - - test('#send with an invalid URL', () { - var client = new BrowserClient(); - var url = Uri.parse('http://http.invalid'); - var request = new http.StreamedRequest("POST", url); - - expect(client.send(request), - throwsClientException("XMLHttpRequest error.")); - - request.sink.add('{"hello": "world"}'.codeUnits); - request.sink.close(); - }); -} diff --git a/test/html/streamed_request_test.dart b/test/html/streamed_request_test.dart deleted file mode 100644 index 6496f4b023..0000000000 --- a/test/html/streamed_request_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -// 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 'package:http/http.dart' as http; -import 'package:http/browser_client.dart'; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - group('contentLength', () { - test("works when it's set", () { - var request = new http.StreamedRequest('POST', echoUrl); - request.contentLength = 10; - request.sink.add([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - request.sink.close(); - - return new BrowserClient().send(request).then((response) { - expect(response.stream.toBytes(), - completion(equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))); - }); - }); - - test("works when it's not set", () { - var request = new http.StreamedRequest('POST', echoUrl); - request.sink.add([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - request.sink.close(); - - return new BrowserClient().send(request).then((response) { - expect(response.stream.toBytes(), - completion(equals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]))); - }); - }); - }); -} diff --git a/test/html/utils.dart b/test/html/utils.dart deleted file mode 100644 index 5d67765610..0000000000 --- a/test/html/utils.dart +++ /dev/null @@ -1,11 +0,0 @@ -// 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:html'; - -export '../utils.dart'; - -/// The test server's echo URL. -Uri get echoUrl => Uri.parse( - '${window.location.protocol}//${window.location.host}/echo'); diff --git a/test/io/client_test.dart b/test/io/client_test.dart deleted file mode 100644 index acfa584a3c..0000000000 --- a/test/io/client_test.dart +++ /dev/null @@ -1,98 +0,0 @@ -// 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'; - -import 'package:http/http.dart' as http; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - tearDown(stopServer); - - test('#send a StreamedRequest', () { - expect(startServer().then((_) { - var client = new http.Client(); - var request = new http.StreamedRequest("POST", serverUrl); - request.headers[HttpHeaders.CONTENT_TYPE] = - 'application/json; charset=utf-8'; - request.headers[HttpHeaders.USER_AGENT] = 'Dart'; - - expect(client.send(request).then((response) { - expect(response.request, equals(request)); - expect(response.statusCode, equals(200)); - expect(response.headers['single'], equals('value')); - // dart:io internally normalizes outgoing headers so that they never - // have multiple headers with the same name, so there's no way to test - // whether we handle that case correctly. - - return response.stream.bytesToString(); - }).whenComplete(client.close), completion(parse(equals({ - 'method': 'POST', - 'path': '/', - 'headers': { - 'content-type': ['application/json; charset=utf-8'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'transfer-encoding': ['chunked'] - }, - 'body': '{"hello": "world"}' - })))); - - request.sink.add('{"hello": "world"}'.codeUnits); - request.sink.close(); - }), completes); - }); - - test('#send a StreamedRequest with a custom client', () { - expect(startServer().then((_) { - var ioClient = new HttpClient(); - var client = new http.IOClient(ioClient); - var request = new http.StreamedRequest("POST", serverUrl); - request.headers[HttpHeaders.CONTENT_TYPE] = - 'application/json; charset=utf-8'; - request.headers[HttpHeaders.USER_AGENT] = 'Dart'; - - expect(client.send(request).then((response) { - expect(response.request, equals(request)); - expect(response.statusCode, equals(200)); - expect(response.headers['single'], equals('value')); - // dart:io internally normalizes outgoing headers so that they never - // have multiple headers with the same name, so there's no way to test - // whether we handle that case correctly. - - return response.stream.bytesToString(); - }).whenComplete(client.close), completion(parse(equals({ - 'method': 'POST', - 'path': '/', - 'headers': { - 'content-type': ['application/json; charset=utf-8'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'transfer-encoding': ['chunked'] - }, - 'body': '{"hello": "world"}' - })))); - - request.sink.add('{"hello": "world"}'.codeUnits); - request.sink.close(); - }), completes); - }); - - test('#send with an invalid URL', () { - expect(startServer().then((_) { - var client = new http.Client(); - var url = Uri.parse('http://http.invalid'); - var request = new http.StreamedRequest("POST", url); - request.headers[HttpHeaders.CONTENT_TYPE] = - 'application/json; charset=utf-8'; - - expect(client.send(request), throwsSocketException); - - request.sink.add('{"hello": "world"}'.codeUnits); - request.sink.close(); - }), completes); - }); -} diff --git a/test/io/http_test.dart b/test/io/http_test.dart deleted file mode 100644 index 2419e198ef..0000000000 --- a/test/io/http_test.dart +++ /dev/null @@ -1,436 +0,0 @@ -// 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 'package:http/http.dart' as http; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -main() { - group('http.', () { - tearDown(stopServer); - - test('head', () { - expect(startServer().then((_) { - expect(http.head(serverUrl).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, equals('')); - }), completes); - }), completes); - }); - - test('get', () { - expect(startServer().then((_) { - expect(http.get(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'GET', - 'path': '/', - 'headers': { - 'content-length': ['0'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - }))); - }), completes); - }), completes); - }); - - test('post', () { - expect(startServer().then((_) { - expect(http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'Content-Type': 'text/plain', - 'User-Agent': 'Dart' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'POST', - 'path': '/', - 'headers': { - 'accept-encoding': ['gzip'], - 'content-length': ['0'], - 'content-type': ['text/plain'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - } - }))); - }), completes); - }), completes); - }); - - test('post with string', () { - expect(startServer().then((_) { - expect(http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: 'request body').then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'POST', - 'path': '/', - 'headers': { - 'content-type': ['text/plain; charset=utf-8'], - 'content-length': ['12'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': 'request body' - }))); - }), completes); - }), completes); - }); - - test('post with bytes', () { - expect(startServer().then((_) { - expect(http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: [104, 101, 108, 108, 111]).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'POST', - 'path': '/', - 'headers': { - 'content-length': ['5'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': [104, 101, 108, 108, 111] - }))); - }), completes); - }), completes); - }); - - test('post with fields', () { - expect(startServer().then((_) { - expect(http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: { - 'some-field': 'value', - 'other-field': 'other value' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'POST', - 'path': '/', - 'headers': { - 'content-type': [ - 'application/x-www-form-urlencoded; charset=utf-8' - ], - 'content-length': ['40'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': 'some-field=value&other-field=other+value' - }))); - }), completes); - }), completes); - }); - - test('put', () { - expect(startServer().then((_) { - expect(http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'Content-Type': 'text/plain', - 'User-Agent': 'Dart' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PUT', - 'path': '/', - 'headers': { - 'accept-encoding': ['gzip'], - 'content-length': ['0'], - 'content-type': ['text/plain'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - } - }))); - }), completes); - }), completes); - }); - - test('put with string', () { - expect(startServer().then((_) { - expect(http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: 'request body').then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PUT', - 'path': '/', - 'headers': { - 'content-type': ['text/plain; charset=utf-8'], - 'content-length': ['12'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': 'request body' - }))); - }), completes); - }), completes); - }); - - test('put with bytes', () { - expect(startServer().then((_) { - expect(http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: [104, 101, 108, 108, 111]).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PUT', - 'path': '/', - 'headers': { - 'content-length': ['5'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': [104, 101, 108, 108, 111] - }))); - }), completes); - }), completes); - }); - - test('put with fields', () { - expect(startServer().then((_) { - expect(http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: { - 'some-field': 'value', - 'other-field': 'other value' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PUT', - 'path': '/', - 'headers': { - 'content-type': [ - 'application/x-www-form-urlencoded; charset=utf-8' - ], - 'content-length': ['40'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': 'some-field=value&other-field=other+value' - }))); - }), completes); - }), completes); - }); - - test('patch', () { - expect(startServer().then((_) { - expect(http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'Content-Type': 'text/plain', - 'User-Agent': 'Dart' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PATCH', - 'path': '/', - 'headers': { - 'accept-encoding': ['gzip'], - 'content-length': ['0'], - 'content-type': ['text/plain'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - } - }))); - }), completes); - }), completes); - }); - - test('patch with string', () { - expect(startServer().then((_) { - expect(http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: 'request body').then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PATCH', - 'path': '/', - 'headers': { - 'content-type': ['text/plain; charset=utf-8'], - 'content-length': ['12'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': 'request body' - }))); - }), completes); - }), completes); - }); - - test('patch with bytes', () { - expect(startServer().then((_) { - expect(http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: [104, 101, 108, 108, 111]).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PATCH', - 'path': '/', - 'headers': { - 'content-length': ['5'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': [104, 101, 108, 108, 111] - }))); - }), completes); - }), completes); - }); - - test('patch with fields', () { - expect(startServer().then((_) { - expect(http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }, body: { - 'some-field': 'value', - 'other-field': 'other value' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'PATCH', - 'path': '/', - 'headers': { - 'content-type': [ - 'application/x-www-form-urlencoded; charset=utf-8' - ], - 'content-length': ['40'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - 'body': 'some-field=value&other-field=other+value' - }))); - }), completes); - }), completes); - }); - - test('delete', () { - expect(startServer().then((_) { - expect(http.delete(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }).then((response) { - expect(response.statusCode, equals(200)); - expect(response.body, parse(equals({ - 'method': 'DELETE', - 'path': '/', - 'headers': { - 'content-length': ['0'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - } - }))); - }), completes); - }), completes); - }); - - test('read', () { - expect(startServer().then((_) { - expect(http.read(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }).then((val) => val), completion(parse(equals({ - 'method': 'GET', - 'path': '/', - 'headers': { - 'content-length': ['0'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - })))); - }), completes); - }); - - test('read throws an error for a 4** status code', () { - expect(startServer().then((_) { - expect(http.read(serverUrl.resolve('/error')), throwsClientException); - }), completes); - }); - - test('readBytes', () { - expect(startServer().then((_) { - var future = http.readBytes(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' - }).then((bytes) => new String.fromCharCodes(bytes)); - - expect(future, completion(parse(equals({ - 'method': 'GET', - 'path': '/', - 'headers': { - 'content-length': ['0'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'x-random-header': ['Value'], - 'x-other-header': ['Other Value'] - }, - })))); - }), completes); - }); - - test('readBytes throws an error for a 4** status code', () { - expect(startServer().then((_) { - expect(http.readBytes(serverUrl.resolve('/error')), - throwsClientException); - }), completes); - }); - }); -} diff --git a/test/io/multipart_test.dart b/test/io/multipart_test.dart deleted file mode 100644 index 4071dae2eb..0000000000 --- a/test/io/multipart_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// 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:async'; -import 'dart:io'; - -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as path; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - var tempDir; - setUp(() { - tempDir = Directory.systemTemp.createTempSync('http_test_'); - }); - - tearDown(() => tempDir.deleteSync(recursive: true)); - - test('with a file from disk', () { - expect(new Future.sync(() { - var filePath = path.join(tempDir.path, 'test-file'); - new File(filePath).writeAsStringSync('hello'); - return http.MultipartFile.fromPath('file', filePath); - }).then((file) { - var request = new http.MultipartRequest('POST', dummyUrl); - request.files.add(file); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: application/octet-stream - content-disposition: form-data; name="file"; filename="test-file" - - hello - --{{boundary}}-- - ''')); - }), completes); - }); -} diff --git a/test/io/request_test.dart b/test/io/request_test.dart deleted file mode 100644 index 5408ba8829..0000000000 --- a/test/io/request_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -// 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 'package:http/http.dart' as http; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - test('.send', () { - expect(startServer().then((_) { - - var request = new http.Request('POST', serverUrl); - request.body = "hello"; - request.headers['User-Agent'] = 'Dart'; - - expect(request.send().then((response) { - expect(response.statusCode, equals(200)); - return response.stream.bytesToString(); - }).whenComplete(stopServer), completion(parse(equals({ - 'method': 'POST', - 'path': '/', - 'headers': { - 'content-type': ['text/plain; charset=utf-8'], - 'accept-encoding': ['gzip'], - 'user-agent': ['Dart'], - 'content-length': ['5'] - }, - 'body': 'hello' - })))); - }), completes); - }); - - test('#followRedirects', () { - expect(startServer().then((_) { - var request = new http.Request('POST', serverUrl.resolve('/redirect')) - ..followRedirects = false; - var future = request.send().then((response) { - expect(response.statusCode, equals(302)); - }); - expect(future.catchError((_) {}).then((_) => stopServer()), completes); - expect(future, completes); - }), completes); - }); - - test('#maxRedirects', () { - expect(startServer().then((_) { - var request = new http.Request('POST', serverUrl.resolve('/loop?1')) - ..maxRedirects = 2; - var future = request.send().catchError((error) { - expect(error, isRedirectLimitExceededException); - expect(error.redirects.length, equals(2)); - }); - expect(future.catchError((_) {}).then((_) => stopServer()), completes); - expect(future, completes); - }), completes); - }); -} diff --git a/test/io/streamed_request_test.dart b/test/io/streamed_request_test.dart deleted file mode 100644 index cc05bbb0f5..0000000000 --- a/test/io/streamed_request_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -// 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:convert'; - -import 'package:http/http.dart' as http; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - group('contentLength', () { - test('controls the Content-Length header', () { - return startServer().then((_) { - var request = new http.StreamedRequest('POST', serverUrl); - request.contentLength = 10; - request.sink.add([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - request.sink.close(); - - return request.send(); - }).then((response) { - expect(UTF8.decodeStream(response.stream), - completion(parse(containsPair('headers', - containsPair('content-length', ['10']))))); - }).whenComplete(stopServer); - }); - - test('defaults to sending no Content-Length', () { - return startServer().then((_) { - var request = new http.StreamedRequest('POST', serverUrl); - request.sink.add([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - request.sink.close(); - - return request.send(); - }).then((response) { - expect(UTF8.decodeStream(response.stream), - completion(parse(containsPair('headers', - isNot(contains('content-length')))))); - }).whenComplete(stopServer); - }); - }); - - // Regression test. - test('.send() with a response with no content length', () { - return startServer().then((_) { - var request = new http.StreamedRequest( - 'GET', serverUrl.resolve('/no-content-length')); - request.sink.close(); - return request.send(); - }).then((response) { - expect(UTF8.decodeStream(response.stream), completion(equals('body'))); - }).whenComplete(stopServer); - }); - -} diff --git a/test/io/utils.dart b/test/io/utils.dart deleted file mode 100644 index a97f04fe9c..0000000000 --- a/test/io/utils.dart +++ /dev/null @@ -1,149 +0,0 @@ -// 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:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:http/http.dart'; -import 'package:http/src/utils.dart'; -import 'package:unittest/unittest.dart'; - -export '../utils.dart'; - -/// The current server instance. -HttpServer _server; - -/// The URL for the current server instance. -Uri get serverUrl => Uri.parse('http://localhost:${_server.port}'); - -/// Starts a new HTTP server. -Future startServer() { - return HttpServer.bind("localhost", 0).then((s) { - _server = s; - s.listen((request) { - var path = request.uri.path; - var response = request.response; - - if (path == '/error') { - response.statusCode = 400; - response.contentLength = 0; - response.close(); - return; - } - - if (path == '/loop') { - var n = int.parse(request.uri.query); - response.statusCode = 302; - response.headers.set('location', - serverUrl.resolve('/loop?${n + 1}').toString()); - response.contentLength = 0; - response.close(); - return; - } - - if (path == '/redirect') { - response.statusCode = 302; - response.headers.set('location', serverUrl.resolve('/').toString()); - response.contentLength = 0; - response.close(); - return; - } - - if (path == '/no-content-length') { - response.statusCode = 200; - response.contentLength = -1; - response.write('body'); - response.close(); - return; - } - - new ByteStream(request).toBytes().then((requestBodyBytes) { - var outputEncoding; - var encodingName = request.uri.queryParameters['response-encoding']; - if (encodingName != null) { - outputEncoding = requiredEncodingForCharset(encodingName); - } else { - outputEncoding = ASCII; - } - - response.headers.contentType = - new ContentType( - "application", "json", charset: outputEncoding.name); - response.headers.set('single', 'value'); - - var requestBody; - if (requestBodyBytes.isEmpty) { - requestBody = null; - } else if (request.headers.contentType != null && - request.headers.contentType.charset != null) { - var encoding = requiredEncodingForCharset( - request.headers.contentType.charset); - requestBody = encoding.decode(requestBodyBytes); - } else { - requestBody = requestBodyBytes; - } - - var content = { - 'method': request.method, - 'path': request.uri.path, - 'headers': {} - }; - if (requestBody != null) content['body'] = requestBody; - request.headers.forEach((name, values) { - // These headers are automatically generated by dart:io, so we don't - // want to test them here. - if (name == 'cookie' || name == 'host') return; - - (content['headers'] as Map)[name] = values; - }); - - var body = JSON.encode(content); - response.contentLength = body.length; - response.write(body); - response.close(); - }); - }); - }); -} - -/// Stops the current HTTP server. -void stopServer() { - if (_server != null) { - _server.close(); - _server = null; - } -} - -/// A matcher for functions that throw HttpException. -Matcher get throwsClientException => - throwsA(new isInstanceOf()); - -/// A matcher for RedirectLimitExceededExceptions. -const isRedirectLimitExceededException = - const _RedirectLimitExceededException(); - -/// A matcher for functions that throw RedirectLimitExceededException. -const Matcher throwsRedirectLimitExceededException = - const Throws(isRedirectLimitExceededException); - -class _RedirectLimitExceededException extends TypeMatcher { - const _RedirectLimitExceededException() : - super("RedirectLimitExceededException"); - - bool matches(item, Map matchState) => - item is RedirectException && item.message == "Redirect limit exceeded"; -} - -/// A matcher for SocketExceptions. -const isSocketException = const _SocketException(); - -/// A matcher for functions that throw SocketException. -const Matcher throwsSocketException = - const Throws(isSocketException); - -class _SocketException extends TypeMatcher { - const _SocketException() : super("SocketException"); - bool matches(item, Map matchState) => item is SocketException; -} diff --git a/test/mock_client_test.dart b/test/mock_client_test.dart deleted file mode 100644 index 48e635fee3..0000000000 --- a/test/mock_client_test.dart +++ /dev/null @@ -1,61 +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 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - test('handles a request', () { - var client = new MockClient((request) { - return new Future.value(new http.Response( - JSON.encode(request.bodyFields), 200, - request: request, headers: {'content-type': 'application/json'})); - }); - - expect(client.post("http://example.com/foo", body: { - 'field1': 'value1', - 'field2': 'value2' - }).then((response) => response.body), completion(parse(equals({ - 'field1': 'value1', - 'field2': 'value2' - })))); - }); - - test('handles a streamed request', () { - var client = new MockClient.streaming((request, bodyStream) { - return bodyStream.bytesToString().then((bodyString) { - var controller = new StreamController>(sync: true); - new Future.sync(() { - controller.add('Request body was "$bodyString"'.codeUnits); - controller.close(); - }); - - return new http.StreamedResponse(controller.stream, 200); - }); - }); - - var uri = Uri.parse("http://example.com/foo"); - var request = new http.Request("POST", uri); - request.body = "hello, world"; - var future = client.send(request) - .then(http.Response.fromStream) - .then((response) => response.body); - expect(future, completion(equals('Request body was "hello, world"'))); - }); - - test('handles a request with no body', () { - var client = new MockClient((request) { - return new Future.value(new http.Response('you did it', 200)); - }); - - expect(client.read("http://example.com/foo"), - completion(equals('you did it'))); - }); -} diff --git a/test/multipart_test.dart b/test/multipart_test.dart deleted file mode 100644 index 64586db2d8..0000000000 --- a/test/multipart_test.dart +++ /dev/null @@ -1,234 +0,0 @@ -// 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 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - test('empty', () { - var request = new http.MultipartRequest('POST', dummyUrl); - expect(request, bodyMatches(''' - --{{boundary}}-- - ''')); - }); - - test('with fields and files', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.fields['field1'] = 'value1'; - request.fields['field2'] = 'value2'; - request.files.add(new http.MultipartFile.fromString("file1", "contents1", - filename: "filename1.txt")); - request.files.add(new http.MultipartFile.fromString("file2", "contents2")); - - expect(request, bodyMatches(''' - --{{boundary}} - content-disposition: form-data; name="field1" - - value1 - --{{boundary}} - content-disposition: form-data; name="field2" - - value2 - --{{boundary}} - content-type: text/plain; charset=utf-8 - content-disposition: form-data; name="file1"; filename="filename1.txt" - - contents1 - --{{boundary}} - content-type: text/plain; charset=utf-8 - content-disposition: form-data; name="file2" - - contents2 - --{{boundary}}-- - ''')); - }); - - test('with a unicode field name', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.fields['fïēld'] = 'value'; - - expect(request, bodyMatches(''' - --{{boundary}} - content-disposition: form-data; name="fïēld" - - value - --{{boundary}}-- - ''')); - }); - - test('with a field name with newlines', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.fields['foo\nbar\rbaz\r\nbang'] = 'value'; - - expect(request, bodyMatches(''' - --{{boundary}} - content-disposition: form-data; name="foo%0D%0Abar%0D%0Abaz%0D%0Abang" - - value - --{{boundary}}-- - ''')); - }); - - test('with a field name with a quote', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.fields['foo"bar'] = 'value'; - - expect(request, bodyMatches(''' - --{{boundary}} - content-disposition: form-data; name="foo%22bar" - - value - --{{boundary}}-- - ''')); - }); - - test('with a unicode field value', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.fields['field'] = 'vⱥlūe'; - - expect(request, bodyMatches(''' - --{{boundary}} - content-disposition: form-data; name="field" - content-type: text/plain; charset=utf-8 - content-transfer-encoding: binary - - vⱥlūe - --{{boundary}}-- - ''')); - }); - - test('with a unicode filename', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.files.add(new http.MultipartFile.fromString('file', 'contents', - filename: 'fïlēname.txt')); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: text/plain; charset=utf-8 - content-disposition: form-data; name="file"; filename="fïlēname.txt" - - contents - --{{boundary}}-- - ''')); - }); - - test('with a filename with newlines', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.files.add(new http.MultipartFile.fromString('file', 'contents', - filename: 'foo\nbar\rbaz\r\nbang')); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: text/plain; charset=utf-8 - content-disposition: form-data; name="file"; filename="foo%0D%0Abar%0D%0Abaz%0D%0Abang" - - contents - --{{boundary}}-- - ''')); - }); - - test('with a filename with a quote', () { - var request = new http.MultipartRequest('POST', dummyUrl); - request.files.add(new http.MultipartFile.fromString('file', 'contents', - filename: 'foo"bar')); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: text/plain; charset=utf-8 - content-disposition: form-data; name="file"; filename="foo%22bar" - - contents - --{{boundary}}-- - ''')); - }); - - test('with a string file with a content-type but no charset', () { - var request = new http.MultipartRequest('POST', dummyUrl); - var file = new http.MultipartFile.fromString('file', '{"hello": "world"}', - contentType: new MediaType('application', 'json')); - request.files.add(file); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: application/json; charset=utf-8 - content-disposition: form-data; name="file" - - {"hello": "world"} - --{{boundary}}-- - ''')); - }); - - test('with a file with a iso-8859-1 body', () { - var request = new http.MultipartRequest('POST', dummyUrl); - // "Ã¥" encoded as ISO-8859-1 and then read as UTF-8 results in "å". - var file = new http.MultipartFile.fromString('file', 'non-ascii: "Ã¥"', - contentType: new MediaType('text', 'plain', {'charset': 'iso-8859-1'})); - request.files.add(file); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: text/plain; charset=iso-8859-1 - content-disposition: form-data; name="file" - - non-ascii: "å" - --{{boundary}}-- - ''')); - }); - - test('with a stream file', () { - var request = new http.MultipartRequest('POST', dummyUrl); - var controller = new StreamController>(sync: true); - request.files.add(new http.MultipartFile('file', controller.stream, 5)); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: application/octet-stream - content-disposition: form-data; name="file" - - hello - --{{boundary}}-- - ''')); - - controller.add([104, 101, 108, 108, 111]); - controller.close(); - }); - - test('with an empty stream file', () { - var request = new http.MultipartRequest('POST', dummyUrl); - var controller = new StreamController>(sync: true); - request.files.add(new http.MultipartFile('file', controller.stream, 0)); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: application/octet-stream - content-disposition: form-data; name="file" - - - --{{boundary}}-- - ''')); - - controller.close(); - }); - - test('with a byte file', () { - var request = new http.MultipartRequest('POST', dummyUrl); - var file = new http.MultipartFile.fromBytes( - 'file', [104, 101, 108, 108, 111]); - request.files.add(file); - - expect(request, bodyMatches(''' - --{{boundary}} - content-type: application/octet-stream - content-disposition: form-data; name="file" - - hello - --{{boundary}}-- - ''')); - }); -} diff --git a/test/request_test.dart b/test/request_test.dart deleted file mode 100644 index f2f4c133d9..0000000000 --- a/test/request_test.dart +++ /dev/null @@ -1,354 +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:convert'; - -import 'package:http/http.dart' as http; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - group('#contentLength', () { - test('is computed from bodyBytes', () { - var request = new http.Request('POST', dummyUrl); - request.bodyBytes = [1, 2, 3, 4, 5]; - expect(request.contentLength, equals(5)); - request.bodyBytes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - expect(request.contentLength, equals(10)); - }); - - test('is computed from body', () { - var request = new http.Request('POST', dummyUrl); - request.body = "hello"; - expect(request.contentLength, equals(5)); - request.body = "hello, world"; - expect(request.contentLength, equals(12)); - }); - - test('is not directly mutable', () { - var request = new http.Request('POST', dummyUrl); - expect(() => request.contentLength = 50, throwsUnsupportedError); - }); - }); - - group('#encoding', () { - test('defaults to utf-8', () { - var request = new http.Request('POST', dummyUrl); - expect(request.encoding.name, equals(UTF8.name)); - }); - - test('can be set', () { - var request = new http.Request('POST', dummyUrl); - request.encoding = LATIN1; - expect(request.encoding.name, equals(LATIN1.name)); - }); - - test('is based on the content-type charset if it exists', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'text/plain; charset=iso-8859-1'; - expect(request.encoding.name, equals(LATIN1.name)); - }); - - test('remains the default if the content-type charset is set and unset', - () { - var request = new http.Request('POST', dummyUrl); - request.encoding = LATIN1; - request.headers['Content-Type'] = 'text/plain; charset=utf-8'; - expect(request.encoding.name, equals(UTF8.name)); - - request.headers.remove('Content-Type'); - expect(request.encoding.name, equals(LATIN1.name)); - }); - - test('throws an error if the content-type charset is unknown', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'text/plain; charset=not-a-real-charset'; - expect(() => request.encoding, throwsFormatException); - }); - }); - - group('#bodyBytes', () { - test('defaults to empty', () { - var request = new http.Request('POST', dummyUrl); - expect(request.bodyBytes, isEmpty); - }); - - test('can be set', () { - var request = new http.Request('POST', dummyUrl); - request.bodyBytes = [104, 101, 108, 108, 111]; - expect(request.bodyBytes, equals([104, 101, 108, 108, 111])); - }); - - test('changes when body changes', () { - var request = new http.Request('POST', dummyUrl); - request.body = "hello"; - expect(request.bodyBytes, equals([104, 101, 108, 108, 111])); - }); - }); - - group('#body', () { - test('defaults to empty', () { - var request = new http.Request('POST', dummyUrl); - expect(request.body, isEmpty); - }); - - test('can be set', () { - var request = new http.Request('POST', dummyUrl); - request.body = "hello"; - expect(request.body, equals("hello")); - }); - - test('changes when bodyBytes changes', () { - var request = new http.Request('POST', dummyUrl); - request.bodyBytes = [104, 101, 108, 108, 111]; - expect(request.body, equals("hello")); - }); - - test('is encoded according to the given encoding', () { - var request = new http.Request('POST', dummyUrl); - request.encoding = LATIN1; - request.body = "föøbãr"; - expect(request.bodyBytes, equals([102, 246, 248, 98, 227, 114])); - }); - - test('is decoded according to the given encoding', () { - var request = new http.Request('POST', dummyUrl); - request.encoding = LATIN1; - request.bodyBytes = [102, 246, 248, 98, 227, 114]; - expect(request.body, equals("föøbãr")); - }); - }); - - group('#bodyFields', () { - test("can't be read without setting the content-type", () { - var request = new http.Request('POST', dummyUrl); - expect(() => request.bodyFields, throwsStateError); - }); - - test("can't be read with the wrong content-type", () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'text/plain'; - expect(() => request.bodyFields, throwsStateError); - }); - - test("can't be set with the wrong content-type", () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'text/plain'; - expect(() => request.bodyFields = {}, throwsStateError); - }); - - test('defaults to empty', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/x-www-form-urlencoded'; - expect(request.bodyFields, isEmpty); - }); - - test('can be set with no content-type', () { - var request = new http.Request('POST', dummyUrl); - request.bodyFields = {'hello': 'world'}; - expect(request.bodyFields, equals({'hello': 'world'})); - }); - - test('changes when body changes', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/x-www-form-urlencoded'; - request.body = 'key%201=value&key+2=other%2bvalue'; - expect(request.bodyFields, - equals({'key 1': 'value', 'key 2': 'other+value'})); - }); - - test('is encoded according to the given encoding', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/x-www-form-urlencoded'; - request.encoding = LATIN1; - request.bodyFields = {"föø": "bãr"}; - expect(request.body, equals('f%F6%F8=b%E3r')); - }); - - test('is decoded according to the given encoding', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/x-www-form-urlencoded'; - request.encoding = LATIN1; - request.body = 'f%F6%F8=b%E3r'; - expect(request.bodyFields, equals({"föø": "bãr"})); - }); - }); - - group('content-type header', () { - test('defaults to empty', () { - var request = new http.Request('POST', dummyUrl); - expect(request.headers['Content-Type'], isNull); - }); - - test('defaults to empty if only encoding is set', () { - var request = new http.Request('POST', dummyUrl); - request.encoding = LATIN1; - expect(request.headers['Content-Type'], isNull); - }); - - test('name is case insensitive', () { - var request = new http.Request('POST', dummyUrl); - request.headers['CoNtEnT-tYpE'] = 'application/json'; - expect(request.headers, - containsPair('content-type', 'application/json')); - }); - - test('is set to application/x-www-form-urlencoded with charset utf-8 if ' - 'bodyFields is set', () { - var request = new http.Request('POST', dummyUrl); - request.bodyFields = {'hello': 'world'}; - expect(request.headers['Content-Type'], - equals('application/x-www-form-urlencoded; charset=utf-8')); - }); - - test('is set to application/x-www-form-urlencoded with the given charset ' - 'if bodyFields and encoding are set', () { - var request = new http.Request('POST', dummyUrl); - request.encoding = LATIN1; - request.bodyFields = {'hello': 'world'}; - expect(request.headers['Content-Type'], - equals('application/x-www-form-urlencoded; charset=iso-8859-1')); - }); - - test('is set to text/plain and the given encoding if body and encoding are ' - 'both set', () { - var request = new http.Request('POST', dummyUrl); - request.encoding = LATIN1; - request.body = 'hello, world'; - expect(request.headers['Content-Type'], - equals('text/plain; charset=iso-8859-1')); - }); - - test('is modified to include utf-8 if body is set', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/json'; - request.body = '{"hello": "world"}'; - expect(request.headers['Content-Type'], - equals('application/json; charset=utf-8')); - }); - - test('is modified to include the given encoding if encoding is set', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/json'; - request.encoding = LATIN1; - expect(request.headers['Content-Type'], - equals('application/json; charset=iso-8859-1')); - }); - - test('has its charset overridden by an explicit encoding', () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/json; charset=utf-8'; - request.encoding = LATIN1; - expect(request.headers['Content-Type'], - equals('application/json; charset=iso-8859-1')); - }); - - test("doen't have its charset overridden by setting bodyFields", () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/x-www-form-urlencoded; charset=iso-8859-1'; - request.bodyFields = {'hello': 'world'}; - expect(request.headers['Content-Type'], - equals('application/x-www-form-urlencoded; charset=iso-8859-1')); - }); - - test("doen't have its charset overridden by setting body", () { - var request = new http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/json; charset=iso-8859-1'; - request.body = '{"hello": "world"}'; - expect(request.headers['Content-Type'], - equals('application/json; charset=iso-8859-1')); - }); - }); - - group('#finalize', () { - test('returns a stream that emits the request body', () { - var request = new http.Request('POST', dummyUrl); - request.body = "Hello, world!"; - expect(request.finalize().bytesToString(), - completion(equals("Hello, world!"))); - }); - - test('freezes #persistentConnection', () { - var request = new http.Request('POST', dummyUrl); - request.finalize(); - - expect(request.persistentConnection, isTrue); - expect(() => request.persistentConnection = false, throwsStateError); - }); - - test('freezes #followRedirects', () { - var request = new http.Request('POST', dummyUrl); - request.finalize(); - - expect(request.followRedirects, isTrue); - expect(() => request.followRedirects = false, throwsStateError); - }); - - test('freezes #maxRedirects', () { - var request = new http.Request('POST', dummyUrl); - request.finalize(); - - expect(request.maxRedirects, equals(5)); - expect(() => request.maxRedirects = 10, throwsStateError); - }); - - test('freezes #encoding', () { - var request = new http.Request('POST', dummyUrl); - request.finalize(); - - expect(request.encoding.name, equals(UTF8.name)); - expect(() => request.encoding = ASCII, throwsStateError); - }); - - test('freezes #bodyBytes', () { - var request = new http.Request('POST', dummyUrl); - request.bodyBytes = [1, 2, 3]; - request.finalize(); - - expect(request.bodyBytes, equals([1, 2, 3])); - expect(() => request.bodyBytes = [4, 5, 6], throwsStateError); - }); - - test('freezes #body', () { - var request = new http.Request('POST', dummyUrl); - request.body = "hello"; - request.finalize(); - - expect(request.body, equals("hello")); - expect(() => request.body = "goodbye", throwsStateError); - }); - - test('freezes #bodyFields', () { - var request = new http.Request('POST', dummyUrl); - request.bodyFields = {"hello": "world"}; - request.finalize(); - - expect(request.bodyFields, equals({"hello": "world"})); - expect(() => request.bodyFields = {}, throwsStateError); - }); - - test("can't be called twice", () { - var request = new http.Request('POST', dummyUrl); - request.finalize(); - expect(request.finalize, throwsStateError); - }); - }); - - group('#toString()', () { - test('includes the method and URL', () { - var request = new http.Request('POST', dummyUrl); - expect(request.toString(), 'POST $dummyUrl'); - }); - }); -} - diff --git a/test/response_test.dart b/test/response_test.dart deleted file mode 100644 index 0b33f908df..0000000000 --- a/test/response_test.dart +++ /dev/null @@ -1,75 +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 'package:http/http.dart' as http; -import 'package:unittest/unittest.dart'; - -void main() { - group('()', () { - test('sets body', () { - var response = new http.Response("Hello, world!", 200); - expect(response.body, equals("Hello, world!")); - }); - - test('sets bodyBytes', () { - var response = new http.Response("Hello, world!", 200); - expect(response.bodyBytes, equals( - [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33])); - }); - - test('respects the inferred encoding', () { - var response = new http.Response("föøbãr", 200, - headers: {'content-type': 'text/plain; charset=iso-8859-1'}); - expect(response.bodyBytes, equals( - [102, 246, 248, 98, 227, 114])); - }); - }); - - group('.bytes()', () { - test('sets body', () { - var response = new http.Response.bytes([104, 101, 108, 108, 111], 200); - expect(response.body, equals("hello")); - }); - - test('sets bodyBytes', () { - var response = new http.Response.bytes([104, 101, 108, 108, 111], 200); - expect(response.bodyBytes, equals([104, 101, 108, 108, 111])); - }); - - test('respects the inferred encoding', () { - var response = new http.Response.bytes([102, 246, 248, 98, 227, 114], 200, - headers: {'content-type': 'text/plain; charset=iso-8859-1'}); - expect(response.body, equals("föøbãr")); - }); - }); - - group('.fromStream()', () { - test('sets body', () { - var controller = new StreamController>(sync: true); - var streamResponse = new http.StreamedResponse( - controller.stream, 200, contentLength: 13); - var future = http.Response.fromStream(streamResponse) - .then((response) => response.body); - expect(future, completion(equals("Hello, world!"))); - - controller.add([72, 101, 108, 108, 111, 44, 32]); - controller.add([119, 111, 114, 108, 100, 33]); - controller.close(); - }); - - test('sets bodyBytes', () { - var controller = new StreamController>(sync: true); - var streamResponse = new http.StreamedResponse( - controller.stream, 200, contentLength: 5); - var future = http.Response.fromStream(streamResponse) - .then((response) => response.bodyBytes); - expect(future, completion(equals([104, 101, 108, 108, 111]))); - - controller.add([104, 101, 108, 108, 111]); - controller.close(); - }); - }); -} diff --git a/test/streamed_request_test.dart b/test/streamed_request_test.dart deleted file mode 100644 index c7e56e2c64..0000000000 --- a/test/streamed_request_test.dart +++ /dev/null @@ -1,28 +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 'package:http/http.dart' as http; -import 'package:unittest/unittest.dart'; - -import 'utils.dart'; - -void main() { - group('contentLength', () { - test('defaults to null', () { - var request = new http.StreamedRequest('POST', dummyUrl); - expect(request.contentLength, isNull); - }); - - test('disallows negative values', () { - var request = new http.StreamedRequest('POST', dummyUrl); - expect(() => request.contentLength = -1, throwsArgumentError); - }); - - test('is frozen by finalize()', () { - var request = new http.StreamedRequest('POST', dummyUrl); - request.finalize(); - expect(() => request.contentLength = 10, throwsStateError); - }); - }); -} From 51410521cc2fb9f85116569252baa8a64e63c03e Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 23 Jun 2017 13:56:33 -0700 Subject: [PATCH 19/41] Fix accidental character --- lib/src/pipeline.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/pipeline.dart b/lib/src/pipeline.dart index d7fe1e4d70..fc100ba0de 100644 --- a/lib/src/pipeline.dart +++ b/lib/src/pipeline.dart @@ -1,4 +1,4 @@ -k// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2017, 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. From 2841cd75e2a98cae217449bf5d8cbbbad8a1a374 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 23 Jun 2017 13:56:44 -0700 Subject: [PATCH 20/41] Expose pipeline and handler --- lib/http.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/http.dart b/lib/http.dart index 6b80d7f1eb..2728e1994b 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -13,8 +13,10 @@ import 'src/response.dart'; export 'src/base_client.dart'; export 'src/client.dart'; export 'src/exception.dart'; +export 'src/handler.dart'; export 'src/io_client.dart'; export 'src/middleware.dart'; +export 'src/pipeline.dart'; export 'src/request.dart'; export 'src/response.dart'; From 15cc5de97b2f233d98be67aaf73523b0793b2a37 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 23 Jun 2017 14:13:19 -0700 Subject: [PATCH 21/41] Remove MockClient (#93) --- lib/src/mock_client.dart | 86 ---------------------------------------- lib/testing.dart | 25 ------------ 2 files changed, 111 deletions(-) delete mode 100644 lib/src/mock_client.dart delete mode 100644 lib/testing.dart diff --git a/lib/src/mock_client.dart b/lib/src/mock_client.dart deleted file mode 100644 index acda5fde7a..0000000000 --- a/lib/src/mock_client.dart +++ /dev/null @@ -1,86 +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 '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; - - MockClient._(this._handler); - - /// Creates a [MockClient] with a handler that receives [Request]s and sends - /// [Response]s. - MockClient(MockClientHandler fn) - : this._((baseRequest, bodyStream) { - return bodyStream.toBytes().then((bodyBytes) { - var 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) { - 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); - }); - }); - - /// Creates a [MockClient] with a handler that receives [StreamedRequest]s and - /// sends [StreamedResponse]s. - MockClient.streaming(MockClientStreamHandler fn) - : this._((request, bodyStream) { - return fn(request, bodyStream).then((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. - Future send(BaseRequest request) async { - var 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 MockClientStreamHandler( - BaseRequest request, ByteStream bodyStream); - -/// A handler function that receives [Request]s and sends [Response]s. Note that -/// [request] will be finalized. -typedef Future MockClientHandler(Request request); diff --git a/lib/testing.dart b/lib/testing.dart deleted file mode 100644 index d5a7874201..0000000000 --- a/lib/testing.dart +++ /dev/null @@ -1,25 +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. - -/// This library contains testing classes for the HTTP library. -/// -/// The [MockClient] class is a drop-in replacement for `http.Client` that -/// allows test code to set up a local request handler in order to fake a server -/// that responds to HTTP requests: -/// -/// import 'dart:convert'; -/// import 'package:http/testing.dart'; -/// -/// var client = new MockClient((request) async { -/// if (request.url.path != "/data.json") { -/// return new Response("", 404); -/// } -/// return new Response( -/// JSON.encode({ -/// 'numbers': [1, 4, 15, 19, 214] -/// }), -/// 200, -/// headers: {'content-type': 'application/json'}); -/// }); -export 'src/mock_client.dart'; From f62d1a6facc299684f132f1d90a9c46fbc643607 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 23 Jun 2017 16:20:19 -0700 Subject: [PATCH 22/41] Adding client tests (#95) --- test/client.dart | 9 + test/client_test.dart | 390 +++++++++++++++++++++++++++++++++++ test/hybrid/client_html.dart | 12 ++ test/hybrid/client_io.dart | 9 + test/hybrid/server.dart | 144 +++++++++++++ test/utils.dart | 46 +---- 6 files changed, 571 insertions(+), 39 deletions(-) create mode 100644 test/client.dart create mode 100644 test/client_test.dart create mode 100644 test/hybrid/client_html.dart create mode 100644 test/hybrid/client_io.dart create mode 100644 test/hybrid/server.dart diff --git a/test/client.dart b/test/client.dart new file mode 100644 index 0000000000..0fba590a95 --- /dev/null +++ b/test/client.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2017, 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 'package:http/http.dart'; + +Client platformClient() => null; + +String userAgent() => null; diff --git a/test/client_test.dart b/test/client_test.dart new file mode 100644 index 0000000000..a8e466d0e6 --- /dev/null +++ b/test/client_test.dart @@ -0,0 +1,390 @@ +// 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 'package:test/test.dart'; + +import 'package:http/http.dart'; + +import 'client.dart' + if (dart.library.io) 'hybrid/client_io.dart' + if (dart.library.html) 'hybrid/client_html.dart'; +import 'utils.dart'; + +void main() { + group('client', () { + // The server url of the spawned server + var serverUrl; + + setUp(() async { + var channel = spawnHybridUri('hybrid/server.dart'); + serverUrl = Uri.parse(await channel.stream.first); + }); + + test('head', () async { + var response = await platformClient().head(serverUrl); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect(body, equals('')); + }); + + test('get', () async { + var response = await platformClient().get(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'GET', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('post with string', () async { + var response = await platformClient().post( + serverUrl, + 'request body', + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'POST', + 'path': '/', + 'headers': { + 'content-type': ['text/plain; charset=utf-8'], + 'content-length': ['12'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'request body' + }))); + }); + + test('post with bytes', () async { + var response = await platformClient().post( + serverUrl, + [104, 101, 108, 108, 111], + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'POST', + 'path': '/', + 'headers': { + 'content-length': ['5'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': [104, 101, 108, 108, 111] + }))); + }); + + test('post with fields', () async { + var response = await platformClient().post( + serverUrl, + {'some-field': 'value', 'other-field': 'other value'}, + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'POST', + 'path': '/', + 'headers': { + 'content-type': [ + 'application/x-www-form-urlencoded; charset=utf-8' + ], + 'content-length': ['40'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'some-field=value&other-field=other+value' + }))); + }); + + test('put with string', () async { + var response = await platformClient().put( + serverUrl, + 'request body', + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PUT', + 'path': '/', + 'headers': { + 'content-type': ['text/plain; charset=utf-8'], + 'content-length': ['12'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'request body' + }))); + }); + + test('put with bytes', () async { + var response = await platformClient().put( + serverUrl, + [104, 101, 108, 108, 111], + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PUT', + 'path': '/', + 'headers': { + 'content-length': ['5'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': [104, 101, 108, 108, 111] + }))); + }); + + test('put with fields', () async { + var response = await platformClient().put( + serverUrl, + {'some-field': 'value', 'other-field': 'other value'}, + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PUT', + 'path': '/', + 'headers': { + 'content-type': [ + 'application/x-www-form-urlencoded; charset=utf-8' + ], + 'content-length': ['40'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'some-field=value&other-field=other+value' + }))); + }); + + test('patch with string', () async { + var response = await platformClient().patch( + serverUrl, + 'request body', + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PATCH', + 'path': '/', + 'headers': { + 'content-type': ['text/plain; charset=utf-8'], + 'content-length': ['12'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'request body' + }))); + }); + + test('patch with bytes', () async { + var response = await platformClient().patch( + serverUrl, + [104, 101, 108, 108, 111], + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PATCH', + 'path': '/', + 'headers': { + 'content-length': ['5'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': [104, 101, 108, 108, 111] + }))); + }); + + test('patch with fields', () async { + var response = await platformClient().patch( + serverUrl, + {'some-field': 'value', 'other-field': 'other value'}, + headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }, + ); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'PATCH', + 'path': '/', + 'headers': { + 'content-type': [ + 'application/x-www-form-urlencoded; charset=utf-8' + ], + 'content-length': ['40'], + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + }, + 'body': 'some-field=value&other-field=other+value' + }))); + }); + + test('delete', () async { + var response = await platformClient().delete(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + var body = await response.readAsString(); + + expect(response.statusCode, equals(200)); + expect( + body, + parse(equals({ + 'method': 'DELETE', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('read', () async { + var body = await platformClient().read(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + + expect( + body, + parse(equals({ + 'method': 'GET', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('read throws an error for a 4** status code', () async { + expect(() => platformClient().read(serverUrl.resolve('/error')), + throwsClientException()); + }); + + test('readBytes', () async { + var body = await platformClient().readBytes(serverUrl, headers: { + 'X-Random-Header': 'Value', + 'X-Other-Header': 'Other Value', + 'User-Agent': userAgent() + }); + + expect( + new String.fromCharCodes(body), + parse(equals({ + 'method': 'GET', + 'path': '/', + 'headers': { + 'user-agent': [userAgent()], + 'x-random-header': ['Value'], + 'x-other-header': ['Other Value'] + } + }))); + }); + + test('readBytes throws an error for a 4** status code', () async { + expect(() => platformClient().readBytes(serverUrl.resolve('/error')), + throwsClientException()); + }); + }); +} diff --git a/test/hybrid/client_html.dart b/test/hybrid/client_html.dart new file mode 100644 index 0000000000..1c1abd9310 --- /dev/null +++ b/test/hybrid/client_html.dart @@ -0,0 +1,12 @@ +// Copyright (c) 2017, 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:html' as html; + +import 'package:http/http.dart'; +import 'package:http/browser_client.dart'; + +Client platformClient() => new BrowserClient(); + +String userAgent() => html.window.navigator.userAgent; diff --git a/test/hybrid/client_io.dart b/test/hybrid/client_io.dart new file mode 100644 index 0000000000..fe41139b44 --- /dev/null +++ b/test/hybrid/client_io.dart @@ -0,0 +1,9 @@ +// Copyright (c) 2017, 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 'package:http/http.dart'; + +Client platformClient() => new Client(); + +String userAgent() => 'Dart'; diff --git a/test/hybrid/server.dart b/test/hybrid/server.dart new file mode 100644 index 0000000000..3b54a8193a --- /dev/null +++ b/test/hybrid/server.dart @@ -0,0 +1,144 @@ +// Copyright (c) 2017, 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:io'; + +import 'package:async/async.dart'; +import 'package:http/src/utils.dart'; +import "package:stream_channel/stream_channel.dart"; + +/// The list of headers to ignore when sending back confirmation. +final _ignoreHeaders = [ + // Browser headers (Chrome) + 'accept', + 'accept-language', + 'accept-encoding', + 'connection', + 'origin', + 'referer', + + // Dart IO headers + 'cookie', + 'host', +]; + +/// Creates a server used to test a `http` client. +/// +/// On startup the server will bind to `localhost`. Then it will send the url +/// as a string back through the [channel]. +/// +/// The server has the following explicit endpoints used to test individual +/// functionality. +/// * /error - Will return a 400 status code. +/// * /loop - Which is used to check for max redirects. +/// * /redirect - Which is used to test that a redirect works. +/// * /no-content-length - Which returns a body with no content. +/// +/// All other requests will be responded to. This is used to test the +/// individual HTTP methods. The server will return back the following +/// information in a string. +/// +/// { +/// method: 'METHOD_NAME', +/// path: 'ENDPOINT_PATH', +/// headers: { +/// KEY VALUE STORE OF INDIVIDUAL HEADERS +/// }, +/// body: OPTIONAL +/// } +hybridMain(StreamChannel channel) async { + var server = await HttpServer.bind('localhost', 0); + var serverUrl = Uri.parse('http://localhost:${server.port}'); + + server.listen((request) { + var path = request.uri.path; + var response = request.response; + + if (path == '/error') { + response.statusCode = 400; + response.contentLength = 0; + response.close(); + return; + } + + if (path == '/loop') { + var n = int.parse(request.uri.query); + response.statusCode = 302; + response.headers + .set('location', serverUrl.resolve('/loop?${n + 1}').toString()); + response.contentLength = 0; + response.close(); + return; + } + + if (path == '/redirect') { + response.statusCode = 302; + response.headers.set('location', serverUrl.resolve('/').toString()); + response.contentLength = 0; + response.close(); + return; + } + + if (path == '/no-content-length') { + response.statusCode = 200; + response.contentLength = -1; + response.write('body'); + response.close(); + return; + } + + collectBytes(request).then((requestBodyBytes) { + var outputEncoding; + var encodingName = request.uri.queryParameters['response-encoding']; + if (encodingName != null) { + outputEncoding = requiredEncodingForCharset(encodingName); + } else { + outputEncoding = ASCII; + } + + response.headers.contentType = + new ContentType("application", "json", charset: outputEncoding.name); + + // Add CORS headers for browser testing + response.headers.set('access-control-allow-origin', '*'); + response.headers.set( + 'access-control-allow-headers', 'X-Random-Header,X-Other-Header'); + response.headers.set('access-control-allow-methods', + 'GET, PUT, POST, DELETE, PATCH, HEAD'); + + var requestBody; + if (requestBodyBytes.isEmpty) { + requestBody = null; + } else if (request.headers.contentType != null && + request.headers.contentType.charset != null) { + var encoding = + requiredEncodingForCharset(request.headers.contentType.charset); + requestBody = encoding.decode(requestBodyBytes); + } else { + requestBody = requestBodyBytes; + } + + var content = { + 'method': request.method, + 'path': request.uri.path, + 'headers': {} + }; + if (requestBody != null) content['body'] = requestBody; + request.headers.forEach((name, values) { + // Ignore headers that are generated by the client + if (_ignoreHeaders.contains(name)) return; + + (content['headers'] as Map)[name] = values; + }); + + var body = JSON.encode(content); + response.contentLength = body.length; + response.write(body); + response.close(); + }); + }); + + channel.sink.add(serverUrl.toString()); +} diff --git a/test/utils.dart b/test/utils.dart index 1bc78f7405..505ee5fda3 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -4,9 +4,9 @@ import 'dart:convert'; +import 'package:test/test.dart'; + import 'package:http/http.dart' as http; -import 'package:http_parser/http_parser.dart'; -import 'package:unittest/unittest.dart'; /// A dummy URL for constructing requests that won't be sent. Uri get dummyUrl => Uri.parse('http://dartlang.org/'); @@ -66,46 +66,14 @@ class _Parse extends Matcher { } } -/// A matcher that validates the body of a multipart request after finalization. -/// The string "{{boundary}}" in [pattern] will be replaced by the boundary -/// string for the request, and LF newlines will be replaced with CRLF. -/// Indentation will be normalized. -Matcher bodyMatches(String pattern) => new _BodyMatches(pattern); - -class _BodyMatches extends Matcher { - final String _pattern; - - _BodyMatches(this._pattern); - - bool matches(item, Map matchState) { - if (item is! http.MultipartRequest) return false; - - var future = item.finalize().toBytes().then((bodyBytes) { - var body = UTF8.decode(bodyBytes); - var contentType = new MediaType.parse(item.headers['content-type']); - var boundary = contentType.parameters['boundary']; - var expected = cleanUpLiteral(_pattern) - .replaceAll("\n", "\r\n") - .replaceAll("{{boundary}}", boundary); - - expect(body, equals(expected)); - expect(item.contentLength, equals(bodyBytes.length)); - }); - - return completes.matches(future, matchState); - } - - Description describe(Description description) { - return description.add('has a body that matches "$_pattern"'); - } -} - /// A matcher that matches a [http.ClientException] with the given [message]. /// /// [message] can be a String or a [Matcher]. -Matcher isClientException(message) => predicate((error) { +Matcher isClientException([message]) => predicate((error) { expect(error, new isInstanceOf()); - expect(error.message, message); + if (message != null) { + expect(error.message, message); + } return true; }); @@ -113,4 +81,4 @@ Matcher isClientException(message) => predicate((error) { /// [http.ClientException] with the given [message]. /// /// [message] can be a String or a [Matcher]. -Matcher throwsClientException(message) => throwsA(isClientException(message)); +Matcher throwsClientException([message]) => throwsA(isClientException(message)); From bceba84b45058c5338ca842c631ea1c3a662a4e1 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Fri, 23 Jun 2017 14:54:08 -0700 Subject: [PATCH 23/41] Add .travis file --- .travis.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..b3a6b8a022 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: dart +sudo: false + +dart: + - dev + - stable + +dart_task: + - test + - dartfmt + - dartanalyzer + +# Only run one instance of the formatter and the analyzer, rather than running +# them against each Dart version. +matrix: + include: + - dart: stable + dart_task: dartfmt + - dart: dev + dart_task: dartanalyzer + +# Only building master means that we don't run two builds for each pull request. +branches: + only: [master] + +cache: + directories: + - $HOME/.pub-cache From 673bf5b3f136ab4b731a2a281e74a38a4bda99e7 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 26 Jun 2017 14:28:24 -0700 Subject: [PATCH 24/41] test on vm and firefox --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b3a6b8a022..123a402463 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,8 @@ dart: - stable dart_task: - - test + - test: --platform vm + - test: --platform firefox - dartfmt - dartanalyzer From b2b853f3835750ecb0d835b0f37029f030ccd4d5 Mon Sep 17 00:00:00 2001 From: Kevin Moore Date: Mon, 26 Jun 2017 15:29:54 -0700 Subject: [PATCH 25/41] nits --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 123a402463..980160ff89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,7 @@ dart: - stable dart_task: - - test: --platform vm - - test: --platform firefox - - dartfmt - - dartanalyzer + - test: --platform vm,firefox # Only run one instance of the formatter and the analyzer, rather than running # them against each Dart version. From 40ccf46720cd128794543e73078c2040de267285 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 27 Jun 2017 14:10:11 -0700 Subject: [PATCH 26/41] Add Request tests (#102) --- test/request_test.dart | 221 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 test/request_test.dart diff --git a/test/request_test.dart b/test/request_test.dart new file mode 100644 index 0000000000..2becbcc4c3 --- /dev/null +++ b/test/request_test.dart @@ -0,0 +1,221 @@ +// Copyright (c) 2017, 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 'package:async/async.dart'; +import 'package:test/test.dart'; + +import 'package:http/http.dart' as http; + +import 'utils.dart'; + +void main() { + group('#contentLength', () { + test('is computed from bodyBytes', () { + var request = new http.Request('POST', dummyUrl, body: [1, 2, 3, 4, 5]); + expect(request.contentLength, equals(5)); + request = new http.Request('POST', dummyUrl, + body: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(request.contentLength, equals(10)); + }); + + test('is computed from body', () { + var request = new http.Request('POST', dummyUrl, body: 'hello'); + expect(request.contentLength, equals(5)); + request = new http.Request('POST', dummyUrl, body: 'hello, world'); + expect(request.contentLength, equals(12)); + }); + }); + + group('#encoding', () { + test('defaults to utf-8', () { + var request = new http.Request('POST', dummyUrl); + expect(request.encoding.name, equals(UTF8.name)); + }); + + test('can be set', () { + var request = new http.Request('POST', dummyUrl, encoding: LATIN1); + expect(request.encoding.name, equals(LATIN1.name)); + }); + + test('is based on the content-type charset if it exists', () { + var request = new http.Request('POST', dummyUrl, + headers: {'Content-Type': 'text/plain; charset=iso-8859-1'}); + expect(request.encoding.name, equals(LATIN1.name)); + }); + + test('throws an error if the content-type charset is unknown', () { + var request = new http.Request('POST', dummyUrl, + headers: {'Content-Type': 'text/plain; charset=not-a-real-charset'}); + expect(() => request.encoding, throwsFormatException); + }); + }); + + group('#bodyBytes', () { + test('defaults to empty', () { + var request = new http.Request('POST', dummyUrl); + expect(collectBytes(request.read()), completion(isEmpty)); + }); + }); + + group('#body', () { + test('defaults to empty', () { + var request = new http.Request('POST', dummyUrl); + expect(request.readAsString(), completion(isEmpty)); + }); + + test('is encoded according to the given encoding', () { + var request = + new http.Request('POST', dummyUrl, encoding: LATIN1, body: "föøbãr"); + expect(collectBytes(request.read()), + completion(equals([102, 246, 248, 98, 227, 114]))); + }); + + test('is decoded according to the given encoding', () { + var request = new http.Request('POST', dummyUrl, + encoding: LATIN1, body: [102, 246, 248, 98, 227, 114]); + expect(request.readAsString(), completion(equals("föøbãr"))); + }); + }); + + group('#bodyFields', () { + test('is encoded according to the given encoding', () { + var request = new http.Request('POST', dummyUrl, + headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + encoding: LATIN1, + body: {"föø": "bãr"}); + expect(request.readAsString(), completion(equals('f%F6%F8=b%E3r'))); + }); + }); + + group('content-type header', () { + test('defaults to empty', () { + var request = new http.Request('POST', dummyUrl); + expect(request.headers['Content-Type'], isNull); + }); + + test('defaults to empty if only encoding is set', () { + var request = new http.Request('POST', dummyUrl, encoding: LATIN1); + expect(request.headers['Content-Type'], isNull); + }); + + test('name is case insensitive', () { + var request = new http.Request('POST', dummyUrl, + headers: {'CoNtEnT-tYpE': 'application/json'}); + expect(request.headers, containsPair('content-type', 'application/json')); + }); + + test( + 'is set to application/x-www-form-urlencoded with charset utf-8 if ' + 'bodyFields is set', () { + var request = + new http.Request('POST', dummyUrl, body: {'hello': 'world'}); + expect(request.headers['Content-Type'], + equals('application/x-www-form-urlencoded; charset=utf-8')); + }); + + test( + 'is set to application/x-www-form-urlencoded with the given charset ' + 'if bodyFields and encoding are set', () { + var request = new http.Request('POST', dummyUrl, + encoding: LATIN1, body: {'hello': 'world'}); + expect(request.headers['Content-Type'], + equals('application/x-www-form-urlencoded; charset=iso-8859-1')); + }); + + test( + 'is set to text/plain and the given encoding if body and encoding are ' + 'both set', () { + var request = new http.Request('POST', dummyUrl, + encoding: LATIN1, body: 'hello, world'); + expect(request.headers['Content-Type'], + equals('text/plain; charset=iso-8859-1')); + }); + + test('is modified to include utf-8 if body is set', () { + var request = new http.Request('POST', dummyUrl, + headers: {'Content-Type': 'application/json'}, + body: '{"hello": "world"}'); + expect(request.headers['Content-Type'], + equals('application/json; charset=utf-8')); + }); + + test('is modified to include the given encoding if encoding is set', () { + var request = new http.Request('POST', dummyUrl, + headers: {'Content-Type': 'application/json'}, encoding: LATIN1); + expect(request.headers['Content-Type'], + equals('application/json; charset=iso-8859-1')); + }); + + test('has its charset overridden by an explicit encoding', () { + var request = new http.Request('POST', dummyUrl, + headers: {'Content-Type': 'application/json; charset=utf-8'}, + encoding: LATIN1); + expect(request.headers['Content-Type'], + equals('application/json; charset=iso-8859-1')); + }); + + test("doen't have its charset overridden by setting bodyFields", () { + var request = new http.Request('POST', dummyUrl, headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=iso-8859-1' + }, body: { + 'hello': 'world' + }); + expect(request.headers['Content-Type'], + equals('application/x-www-form-urlencoded; charset=iso-8859-1')); + }); + + test("doen't have its charset overridden by setting body", () { + var request = new http.Request('POST', dummyUrl, + headers: {'Content-Type': 'application/json; charset=iso-8859-1'}, + body: '{"hello": "world"}'); + expect(request.headers['Content-Type'], + equals('application/json; charset=iso-8859-1')); + }); + }); + + group('change', () { + test('with no arguments returns instance with equal values', () { + var request = new http.Request('GET', dummyUrl, + headers: {'header1': 'header value 1'}, + body: 'hello, world', + context: {'context1': 'context value 1'}); + + var copy = request.change(); + + expect(copy.method, request.method); + expect(copy.headers, same(request.headers)); + expect(copy.url, request.url); + expect(copy.context, same(request.context)); + expect(copy.readAsString(), completion('hello, world')); + }); + + test("allows the original request to be read", () { + var request = new http.Request('GET', dummyUrl); + var changed = request.change(); + + expect(request.read().toList(), completion(isEmpty)); + expect(changed.read, throwsStateError); + }); + + test("allows the changed request to be read", () { + var request = new http.Request('GET', dummyUrl); + var changed = request.change(); + + expect(changed.read().toList(), completion(isEmpty)); + expect(request.read, throwsStateError); + }); + + test("allows another changed request to be read", () { + var request = new http.Request('GET', dummyUrl); + var changed1 = request.change(); + var changed2 = request.change(); + + expect(changed2.read().toList(), completion(isEmpty)); + expect(changed1.read, throwsStateError); + expect(request.read, throwsStateError); + }); + }); +} From ce2bdc6490705ba3ac18105899663af246a277f0 Mon Sep 17 00:00:00 2001 From: Don Date: Sat, 29 Jul 2017 14:27:09 -0700 Subject: [PATCH 27/41] Remove "'" from boundary characters (#106) (#108) Fixes https://github.com/dart-lang/http/issues/105 --- CHANGELOG.md | 5 +++++ lib/src/boundary_characters.dart | 2 +- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8545e484a7..755eb861d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.11.3+14 + +* Remove single quote ("'" - ASCII 39) from boundary characters. + Causes issues with Google Cloud Storage. + ## 0.11.3+13 * remove boundary characters that package:http_parser cannot parse. diff --git a/lib/src/boundary_characters.dart b/lib/src/boundary_characters.dart index 03b7ac2d5e..cc5742a30a 100644 --- a/lib/src/boundary_characters.dart +++ b/lib/src/boundary_characters.dart @@ -10,7 +10,7 @@ /// [RFC 2046]: http://tools.ietf.org/html/rfc2046#section-5.1.1. /// [RFC 1521]: https://tools.ietf.org/html/rfc1521#section-4 const List BOUNDARY_CHARACTERS = const [ - 39, 43, 95, 45, 46, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 43, 95, 45, 46, 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, diff --git a/pubspec.yaml b/pubspec.yaml index 1f678acf01..956379a5bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,4 +15,4 @@ dev_dependencies: dependency_overrides: package_resolver: '^1.0.0' environment: - sdk: ">=1.24.0-dev.0.0 <2.0.0" + sdk: ">=1.24.0 <2.0.0" From e1533c4b88ea2f986deea6fe135e3af7ef28a5c3 Mon Sep 17 00:00:00 2001 From: perlatus Date: Wed, 20 Sep 2017 18:00:18 -0400 Subject: [PATCH 28/41] Add http package prefix for import consistency (#111) In the example, `Client` and `BaseClient` are package qualified, but `BaseRequest` and `StreamedResponse` aren't. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d852914d4..5df9255fb7 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ class UserAgentClient extends http.BaseClient { UserAgentClient(this.userAgent, this._inner); - Future send(BaseRequest request) { + Future send(http.BaseRequest request) { request.headers['user-agent'] = userAgent; return _inner.send(request); } From ebd0b336cd4b0edc35218b6eb0e1bf4d72461894 Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 20 Sep 2017 16:13:01 -0700 Subject: [PATCH 29/41] Fix content-length-header (#110) --- lib/src/io_client.dart | 3 --- test/client_test.dart | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart index 99d52c4e1e..6b2b52039f 100644 --- a/lib/src/io_client.dart +++ b/lib/src/io_client.dart @@ -31,9 +31,6 @@ class IOClient extends BaseClient { ioRequest ..followRedirects = context['io.followRedirects'] ?? true ..maxRedirects = context['io.maxRedirects'] ?? 5 - ..contentLength = request.contentLength == null - ? -1 - : request.contentLength ..persistentConnection = context['io.persistentConnection'] ?? true; request.headers.forEach((name, value) { ioRequest.headers.set(name, value); diff --git a/test/client_test.dart b/test/client_test.dart index a8e466d0e6..6f09fab035 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -330,6 +330,7 @@ void main() { 'method': 'DELETE', 'path': '/', 'headers': { + 'content-length': ['0'], 'user-agent': [userAgent()], 'x-random-header': ['Value'], 'x-other-header': ['Other Value'] From 5f21a3b5b6958a6230387cf447a141f5f2cdc360 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 21 Sep 2017 16:24:12 -0700 Subject: [PATCH 30/41] Rename file (#114) --- .analysis_options => analysis_options.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .analysis_options => analysis_options.yaml (100%) diff --git a/.analysis_options b/analysis_options.yaml similarity index 100% rename from .analysis_options rename to analysis_options.yaml From 77d470ac45a4c8972fe5ad4e58170dc0607240a0 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 28 Sep 2017 14:13:02 -0700 Subject: [PATCH 31/41] Stop depending on stack_trace (#115) At this point, we're only using this for Chain.current which might as well just be StackTrace.current. --- lib/browser_client.dart | 5 ++--- pubspec.yaml | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/browser_client.dart b/lib/browser_client.dart index ff3c80ce6a..2b1cb3b863 100644 --- a/lib/browser_client.dart +++ b/lib/browser_client.dart @@ -7,7 +7,6 @@ import 'dart:html'; import 'dart:typed_data'; import 'package:async/async.dart'; -import 'package:stack_trace/stack_trace.dart'; import 'src/base_client.dart'; import 'src/exception.dart'; @@ -64,7 +63,7 @@ class BrowserClient extends BaseClient { reader.onError.first.then((error) { completer.completeError( new ClientException(error.toString(), request.url), - new Chain.current()); + StackTrace.current); }); reader.readAsArrayBuffer(blob); @@ -75,7 +74,7 @@ class BrowserClient extends BaseClient { // specific information about the error itself. completer.completeError( new ClientException("XMLHttpRequest error.", request.url), - new Chain.current()); + StackTrace.current); }); xhr.send(bytes); diff --git a/pubspec.yaml b/pubspec.yaml index 956379a5bc..958b2babf8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,6 @@ dependencies: collection: "^1.5.0" http_parser: ">=0.0.1 <4.0.0" path: ">=0.9.0 <2.0.0" - stack_trace: ">=0.9.1 <2.0.0" dev_dependencies: test: "^0.12.18" # Override dependency on package_resolver to enable test From 4171b73320808ffdea0b22a3d268869a3713a29c Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 19 Oct 2017 15:43:20 -0700 Subject: [PATCH 32/41] Use generic method syntax (#125) --- lib/src/utils.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 3e9fdf39bb..42c0d27e0d 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -17,11 +17,10 @@ import 'http_unmodifiable_map.dart'; /// [updates] is used. /// /// If [updates] is `null` or empty, [original] is returned unchanged. -Map/**/ updateMap/**/( - Map/**/ original, Map/**/ updates) { +Map updateMap(Map original, Map updates) { if (updates == null || updates.isEmpty) return original; - return new Map.from(original)..addAll(updates); + return new Map.from(original)..addAll(updates); } /// Converts a [Map] from parameter names to values to a URL query string. From 7b0eb3b08a67e8422b40c7a3d399332bed20f89b Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 19 Oct 2017 17:00:22 -0700 Subject: [PATCH 33/41] Add content type library (#124) --- lib/src/content_type.dart | 25 +++++++++++++++++++++++++ lib/src/message.dart | 26 +++++++++++--------------- lib/src/utils.dart | 18 ------------------ 3 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 lib/src/content_type.dart diff --git a/lib/src/content_type.dart b/lib/src/content_type.dart new file mode 100644 index 0000000000..8a1222ac03 --- /dev/null +++ b/lib/src/content_type.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2017, 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 'package:http_parser/http_parser.dart'; + +/// Returns the [Encoding] that corresponds to [charset]. +/// +/// Returns `null` if [charset] is `null` or if no [Encoding] was found that +/// corresponds to [charset]. +Encoding encodingForCharset(String charset) { + if (charset == null) return null; + return Encoding.getByName(charset); +} + +/// Determines the encoding from the media [type]. +/// +/// Returns `null` if the charset is not specified in the [type] or if no +/// [Encoding] was found that corresponds to the `charset`. +Encoding encodingForMediaType(MediaType type) { + if (type == null) return null; + return encodingForCharset(type.parameters['charset']); +} diff --git a/lib/src/message.dart b/lib/src/message.dart index 96608dda66..ae16e24963 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -9,6 +9,7 @@ import 'package:collection/collection.dart'; import 'package:http_parser/http_parser.dart'; import 'body.dart'; +import 'content_type.dart'; import 'http_unmodifiable_map.dart'; import 'utils.dart'; @@ -80,10 +81,12 @@ abstract class Message { /// If not set, `null`. int get contentLength { if (_contentLengthCache != null) return _contentLengthCache; - if (!headers.containsKey('content-length')) return null; - _contentLengthCache = int.parse(headers['content-length']); + var contentLengthHeader = getHeader(headers, 'content-length'); + if (contentLengthHeader == null) return null; + _contentLengthCache = int.parse(contentLengthHeader); return _contentLengthCache; } + int _contentLengthCache; /// The MIME type declared in [headers]. @@ -92,11 +95,7 @@ abstract class Message { /// the MIME type, without any Content-Type parameters. /// /// If [headers] doesn't have a Content-Type header, this will be `null`. - String get mimeType { - var contentType = _contentType; - if (contentType == null) return null; - return contentType.mimeType; - } + String get mimeType => _contentType?.mimeType; /// The encoding of the body returned by [read]. /// @@ -105,22 +104,19 @@ abstract class Message { /// /// If [headers] doesn't have a Content-Type header or it specifies an /// encoding that [dart:convert] doesn't support, this will be `null`. - Encoding get encoding { - var contentType = _contentType; - if (contentType == null) return null; - if (!contentType.parameters.containsKey('charset')) return null; - return Encoding.getByName(contentType.parameters['charset']); - } + Encoding get encoding => encodingForMediaType(_contentType); /// The parsed version of the Content-Type header in [headers]. /// /// This is cached for efficient access. MediaType get _contentType { if (_contentTypeCache != null) return _contentTypeCache; - if (!headers.containsKey('content-type')) return null; - _contentTypeCache = new MediaType.parse(headers['content-type']); + var contentLengthHeader = getHeader(headers, 'content-type'); + if (contentLengthHeader == null) return null; + _contentTypeCache = new MediaType.parse(contentLengthHeader); return _contentTypeCache; } + MediaType _contentTypeCache; /// Returns the message body as byte chunks. diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 42c0d27e0d..6839c87be7 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -52,24 +52,6 @@ List split1(String toSplit, String pattern) { ]; } -/// 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; - var 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) { - var 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. From ada4c0821c15ca7d16e5457ae2c38374cfde1170 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 20 Oct 2017 15:37:43 -0700 Subject: [PATCH 34/41] Fixing test directory (#126) --- test/client_test.dart | 2 -- test/hybrid/server.dart | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/test/client_test.dart b/test/client_test.dart index 6f09fab035..7db52bd49b 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -4,8 +4,6 @@ import 'package:test/test.dart'; -import 'package:http/http.dart'; - import 'client.dart' if (dart.library.io) 'hybrid/client_io.dart' if (dart.library.html) 'hybrid/client_html.dart'; diff --git a/test/hybrid/server.dart b/test/hybrid/server.dart index 3b54a8193a..65bd3e5dc5 100644 --- a/test/hybrid/server.dart +++ b/test/hybrid/server.dart @@ -6,7 +6,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:async/async.dart'; -import 'package:http/src/utils.dart'; +import 'package:http/src/content_type.dart'; import "package:stream_channel/stream_channel.dart"; /// The list of headers to ignore when sending back confirmation. @@ -93,7 +93,7 @@ hybridMain(StreamChannel channel) async { var outputEncoding; var encodingName = request.uri.queryParameters['response-encoding']; if (encodingName != null) { - outputEncoding = requiredEncodingForCharset(encodingName); + outputEncoding = encodingForCharset(encodingName); } else { outputEncoding = ASCII; } @@ -114,7 +114,7 @@ hybridMain(StreamChannel channel) async { } else if (request.headers.contentType != null && request.headers.contentType.charset != null) { var encoding = - requiredEncodingForCharset(request.headers.contentType.charset); + encodingForCharset(request.headers.contentType.charset); requestBody = encoding.decode(requestBodyBytes); } else { requestBody = requestBodyBytes; From 441963f212ebdd5143ca4963d9bbf10c47d067b1 Mon Sep 17 00:00:00 2001 From: Don Date: Fri, 20 Oct 2017 16:20:09 -0700 Subject: [PATCH 35/41] Cleanup contents of util (#127) --- lib/src/utils.dart | 96 +++++----------------------------------------- 1 file changed, 10 insertions(+), 86 deletions(-) diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 6839c87be7..bb5c735ec4 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:collection/collection.dart'; @@ -29,107 +28,32 @@ Map updateMap(Map original, Map updates) { /// //=> "foo=bar&baz=bang" String mapToQuery(Map map, {Encoding encoding}) { var pairs = >[]; - map.forEach((key, value) => - pairs.add([Uri.encodeQueryComponent(key, encoding: encoding), - Uri.encodeQueryComponent(value, encoding: encoding)])); + map.forEach((key, value) => pairs.add([ + Uri.encodeQueryComponent(key, encoding: encoding), + Uri.encodeQueryComponent(value, encoding: encoding) + ])); return pairs.map((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 split1(String toSplit, String pattern) { - if (toSplit.isEmpty) return []; - - var index = toSplit.indexOf(pattern); - if (index == -1) return [toSplit]; - return [ - toSplit.substring(0, index), - toSplit.substring(index + pattern.length) - ]; -} - - /// A regular expression that matches strings that are composed entirely of /// ASCII-compatible characters. -final RegExp _ASCII_ONLY = new RegExp(r"^[\x00-\x7F]+$"); +final RegExp _asciiOnly = new RegExp(r"^[\x00-\x7F]+$"); /// Returns whether [string] is composed entirely of ASCII-compatible /// characters. -bool isPlainAscii(String string) => _ASCII_ONLY.hasMatch(string); +bool isPlainAscii(String string) => _asciiOnly.hasMatch(string); -/// Converts [input] into a [Uint8List]. +/// Pipes all data and errors from [stream] into [sink]. /// -/// If [input] is a [TypedData], this just returns a view on [input]. -Uint8List toUint8List(List input) { - if (input is Uint8List) return input; - if (input is TypedData) { - // TODO(nweiz): remove "as" when issue 11080 is fixed. - return new Uint8List.view((input as TypedData).buffer); - } - return new Uint8List.fromList(input); -} - -/// 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 onDone(Stream stream, void onDone()) => - stream.transform(new StreamTransformer.fromHandlers(handleDone: (sink) { - 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 store(Stream stream, EventSink sink) { - var completer = new Completer(); - 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. +/// Completes [Future] once [stream] is done. Unlike [store], [sink] remains +/// open after [stream] is done. Future writeStreamToSink(Stream stream, EventSink sink) { var completer = new Completer(); stream.listen(sink.add, - onError: sink.addError, - onDone: () => completer.complete()); + onError: sink.addError, onDone: () => completer.complete()); return completer.future; } -/// A pair of values. -class Pair { - E first; - F last; - - Pair(this.first, this.last); - - String toString() => '($first, $last)'; - - bool operator==(other) { - if (other is! Pair) return false; - return other.first == first && other.last == last; - } - - int get hashCode => first.hashCode ^ last.hashCode; -} - -/// Configures [future] so that its result (success or exception) is passed on -/// to [completer]. -void chainToCompleter(Future future, Completer completer) { - future.then(completer.complete, onError: completer.completeError); -} - /// Returns the header with the given [name] in [headers]. /// /// This works even if [headers] is `null`, or if it's not yet a From f940c48b996658d4111a8a92b2810ae799c120a0 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 24 Oct 2017 20:40:55 -0700 Subject: [PATCH 36/41] Add User-Agent to headers (#129) --- test/hybrid/server.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/hybrid/server.dart b/test/hybrid/server.dart index 65bd3e5dc5..ee305e6571 100644 --- a/test/hybrid/server.dart +++ b/test/hybrid/server.dart @@ -103,8 +103,8 @@ hybridMain(StreamChannel channel) async { // Add CORS headers for browser testing response.headers.set('access-control-allow-origin', '*'); - response.headers.set( - 'access-control-allow-headers', 'X-Random-Header,X-Other-Header'); + response.headers.set('access-control-allow-headers', + 'X-Random-Header,X-Other-Header,User-Agent'); response.headers.set('access-control-allow-methods', 'GET, PUT, POST, DELETE, PATCH, HEAD'); From 5983d3fd4384b5063709f72626b08267e799227a Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 26 Oct 2017 13:03:02 -0700 Subject: [PATCH 37/41] Implement multipart requests (#113) --- lib/http.dart | 1 + lib/src/boundary.dart | 41 +++++ lib/src/boundary_characters.dart | 18 --- lib/src/message.dart | 13 +- lib/src/multipart_body.dart | 159 +++++++++++++++++++ lib/src/multipart_file.dart | 174 ++++++++++++--------- lib/src/multipart_request.dart | 165 -------------------- lib/src/request.dart | 69 ++++++--- pubspec.yaml | 3 +- test/multipart_test.dart | 254 +++++++++++++++++++++++++++++++ test/utils.dart | 57 +++++-- 11 files changed, 662 insertions(+), 292 deletions(-) create mode 100644 lib/src/boundary.dart delete mode 100644 lib/src/boundary_characters.dart create mode 100644 lib/src/multipart_body.dart delete mode 100644 lib/src/multipart_request.dart create mode 100644 test/multipart_test.dart diff --git a/lib/http.dart b/lib/http.dart index 2728e1994b..4736abf2da 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -16,6 +16,7 @@ export 'src/exception.dart'; export 'src/handler.dart'; export 'src/io_client.dart'; export 'src/middleware.dart'; +export 'src/multipart_file.dart'; export 'src/pipeline.dart'; export 'src/request.dart'; export 'src/response.dart'; diff --git a/lib/src/boundary.dart b/lib/src/boundary.dart new file mode 100644 index 0000000000..6dd352daad --- /dev/null +++ b/lib/src/boundary.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2017, 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:math'; + +/// All character codes that are valid in multipart boundaries. +/// +/// This is the intersection of the characters allowed in the `bcharsnospace` +/// production defined in [RFC 2046][] and those allowed in the `token` +/// production defined in [RFC 1521][]. +/// +/// [RFC 2046]: http://tools.ietf.org/html/rfc2046#section-5.1.1. +/// [RFC 1521]: https://tools.ietf.org/html/rfc1521#section-4 +const List _boundaryCharacters = const [ + 43, 95, 45, 46, 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 +]; + +/// 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. +const int _boundaryLength = 70; + +final Random _random = new Random(); + +/// Returns a randomly-generated multipart boundary string +String boundaryString() { + var prefix = 'dart-http-boundary-'; + var list = new List.generate( + _boundaryLength - prefix.length, + (index) => + _boundaryCharacters[_random.nextInt(_boundaryCharacters.length)], + growable: false); + return '$prefix${new String.fromCharCodes(list)}'; +} diff --git a/lib/src/boundary_characters.dart b/lib/src/boundary_characters.dart deleted file mode 100644 index cc5742a30a..0000000000 --- a/lib/src/boundary_characters.dart +++ /dev/null @@ -1,18 +0,0 @@ -// 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. - -/// All character codes that are valid in multipart boundaries. This is the -/// intersection of the characters allowed in the `bcharsnospace` production -/// defined in [RFC 2046][] and those allowed in the `token` production -/// defined in [RFC 1521][]. -/// -/// [RFC 2046]: http://tools.ietf.org/html/rfc2046#section-5.1.1. -/// [RFC 1521]: https://tools.ietf.org/html/rfc1521#section-4 -const List BOUNDARY_CHARACTERS = const [ - 43, 95, 45, 46, 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 -]; diff --git a/lib/src/message.dart b/lib/src/message.dart index ae16e24963..6111027948 100644 --- a/lib/src/message.dart +++ b/lib/src/message.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:http_parser/http_parser.dart'; @@ -121,16 +122,24 @@ abstract class Message { /// Returns the message body as byte chunks. /// - /// Throws a [StateError] if [read] or [readAsString] has already been called. + /// Throws a [StateError] if [read] or [readAsBytes] or [readAsString] has + /// already been called. Stream> read() => _body.read(); + /// Returns the message body as a list of bytes. + /// + /// Throws a [StateError] if [read] or [readAsBytes] or [readAsString] has + /// already been called. + Future> readAsBytes() => collectBytes(read()); + /// Returns the message body as a string. /// /// If [encoding] is passed, that's used to decode the body. Otherwise the /// encoding is taken from the Content-Type header. If that doesn't exist or /// doesn't have a "charset" parameter, UTF-8 is used. /// - /// Throws a [StateError] if [read] or [readAsString] has already been called. + /// Throws a [StateError] if [read] or [readAsBytes] or [readAsString] has + /// already been called. Future readAsString([Encoding encoding]) { encoding ??= this.encoding ?? UTF8; return encoding.decodeStream(read()); diff --git a/lib/src/multipart_body.dart b/lib/src/multipart_body.dart new file mode 100644 index 0000000000..961db27973 --- /dev/null +++ b/lib/src/multipart_body.dart @@ -0,0 +1,159 @@ +// Copyright (c) 2017, 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:typed_data/typed_buffers.dart'; + +import 'body.dart'; +import 'multipart_file.dart'; +import 'utils.dart'; + +/// A `multipart/form-data` request [Body]. +/// +/// Such a request has both string fields, which function as normal form +/// fields, and (potentially streamed) binary files. +class MultipartBody implements Body { + /// The contents of the message body. + /// + /// This will be `null` after [read] is called. + Stream> _stream; + + final int contentLength; + + /// Multipart forms do not have an encoding. + Encoding get encoding => null; + + /// Creates a [MultipartBody] from the given [fields] and [files]. + /// + /// The [boundary] is used to separate key value pairs within the body. + factory MultipartBody(Map fields, + Iterable files, String boundary) { + var controller = new StreamController>(sync: true); + var buffer = new Uint8Buffer(); + + void writeAscii(String string) { + buffer.addAll(string.codeUnits); + } + + void writeUtf8(String string) { + buffer.addAll(UTF8.encode(string)); + } + + void writeLine() { + buffer..add(13)..add(10); // \r\n + } + + // Write the fields to the buffer. + fields.forEach((name, value) { + writeAscii('--$boundary\r\n'); + writeUtf8(_headerForField(name, value)); + writeUtf8(value); + writeLine(); + }); + + controller.add(buffer); + + // Iterate over the files to get the length and compute the headers ahead of + // time so the length can be synchronously accessed. + var fileList = files.toList(); + var fileHeaders = >[]; + var fileContentsLength = 0; + + for (var file in fileList) { + var header = [] + ..addAll('--$boundary\r\n'.codeUnits) + ..addAll(UTF8.encode(_headerForFile(file))); + + fileContentsLength += header.length + file.length + 2; + fileHeaders.add(header); + } + + // Ending characters. + var ending = '--$boundary--\r\n'.codeUnits; + fileContentsLength += ending.length; + + // Write the files to the stream asynchronously. + _writeFilesToStream(controller, fileList, fileHeaders, ending); + + return new MultipartBody._( + controller.stream, buffer.length + fileContentsLength); + } + + MultipartBody._(this._stream, this.contentLength); + + /// Returns a [Stream] representing the body. + /// + /// Can only be called once. + Stream> read() { + if (_stream == null) { + throw new StateError("The 'read' method can only be called once on a " + 'http.Request/http.Response object.'); + } + var stream = _stream; + _stream = null; + return stream; + } + + /// Writes the [files] to the [controller]. + static Future _writeFilesToStream( + StreamController> controller, + List files, + List> fileHeaders, + List ending) async { + for (var i = 0; i < files.length; ++i) { + controller.add(fileHeaders[i]); + + // file.read() can throw synchronously + try { + await writeStreamToSink(files[i].read(), controller); + } catch (exception, stackTrace) { + controller.addError(exception, stackTrace); + } + + controller.add([13, 10]); + } + + controller + ..add(ending) + ..close(); + } + + /// Returns the header string for a field. + static String _headerForField(String name, String value) { + var 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. + static String _headerForFile(MultipartFile file) { + var 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'; + } + + static final _newlineRegExp = new RegExp(r'\r\n|\r|\n'); + + /// Encode [value] in the same way browsers do. + static 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. + value.replaceAll(_newlineRegExp, '%0D%0A').replaceAll('"', '%22'); +} diff --git a/lib/src/multipart_file.dart b/lib/src/multipart_file.dart index da4bface78..de75371c01 100644 --- a/lib/src/multipart_file.dart +++ b/lib/src/multipart_file.dart @@ -4,108 +4,132 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:async/async.dart'; import 'package:http_parser/http_parser.dart'; -import 'package:path/path.dart' as path; +import 'package:mime/mime.dart'; -import 'byte_stream.dart'; -import 'utils.dart'; +import 'content_type.dart'; -/// A file to be uploaded as part of a [MultipartRequest]. This doesn't need to -/// correspond to a physical file. +/// A file to be uploaded as part of a `multipart/form-data` Request. +/// +/// This doesn't need to correspond to a physical file. class MultipartFile { + /// The stream that will emit the file's contents. + Stream> _stream; + /// 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]. + /// The size of the file in bytes. + /// + /// This must be known in advance, even if this file is created from a + /// [Stream]. 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`. + /// 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; - - /// Whether [finalize] has been called. - bool get isFinalized => _isFinalized; - bool _isFinalized = false; - - /// 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. + /// Creates a [MultipartFile] from the [value]. + /// + /// [value] can be either a [String] or a [List]. /// - /// [contentType] currently defaults to `application/octet-stream`, but in the - /// future may be inferred from [filename]. - MultipartFile(this.field, Stream> 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. + /// For a String [value] the content will be encoded using [encoding] which + /// defaults to [UTF8]. The `charset` from [contentType] is ignored when + /// encoding the String. /// - /// [contentType] currently defaults to `application/octet-stream`, but in the - /// future may be inferred from [filename]. - factory MultipartFile.fromBytes(String field, List value, - {String filename, MediaType contentType}) { - var stream = new ByteStream.fromBytes(value); - return new MultipartFile(field, stream, value.length, - filename: filename, - contentType: contentType); + /// [contentType] if not specified will attempt to be looked up from the + /// bytes contained within the [stream] and the [filename] if provided. It + /// will default to `plain/text` for [String]s and `application/octet-stream` + /// for [List]. + factory MultipartFile(String field, value, + {String filename, MediaType contentType, Encoding encoding}) { + List bytes; + var defaultMediaType; + + if (value is String) { + encoding ??= UTF8; + bytes = encoding.encode(value); + defaultMediaType = new MediaType('text', 'plain'); + } else if (value is List) { + bytes = value; + defaultMediaType = new MediaType('application', 'octet-stream'); + } else { + throw new ArgumentError.value( + value, 'value', 'value must be either a String or a List'); + } + + contentType ??= _lookUpMediaType(filename, bytes) ?? defaultMediaType; + + if (encoding != null) { + contentType = contentType.change(parameters: {'charset': encoding.name}); + } + + return new MultipartFile.fromStream( + field, new Stream.fromIterable([bytes]), bytes.length, + filename: filename, contentType: contentType); } - /// Creates a new [MultipartFile] from a string. + /// Creates a new [MultipartFile] from a chunked [stream] of bytes. /// - /// 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; - var encoding = encodingForCharset(contentType.parameters['charset'], UTF8); - contentType = contentType.change(parameters: {'charset': encoding.name}); - - return new MultipartFile.fromBytes(field, encoding.encode(value), - filename: filename, - contentType: contentType); - } + /// The [length] of the file in bytes must be known in advance. If it's not + /// then use [loadStream] to create the [MultipartFile] instance. + /// + /// [contentType] if not specified will attempt to be looked up from the + /// [filename] if provided. It will default to `application/octet-stream`. + MultipartFile.fromStream(this.field, Stream> stream, this.length, + {String filename, MediaType contentType}) + : _stream = stream, + filename = filename, + contentType = contentType ?? + _lookUpMediaType(filename) ?? + new MediaType('application', 'octet-stream'); - // TODO(nweiz): Infer the content-type from the filename. - /// Creates a new [MultipartFile] from a path to a file on disk. + /// Creates a new [MultipartFile] from the [stream]. /// - /// [filename] defaults to the basename of [filePath]. [contentType] currently - /// defaults to `application/octet-stream`, but in the future may be inferred - /// from [filename]. + /// This method should be used when the length of [stream] in bytes is not + /// known ahead of time. /// - /// Throws an [UnsupportedError] if `dart:io` isn't supported in this - /// environment. - static Future fromPath(String field, String filePath, + /// [contentType] if not specified will attempt to be looked up from the + /// bytes contained within the [stream] and the [filename] if provided. It + /// will default to `application/octet-stream`. + static Future loadStream( + String field, Stream> stream, {String filename, MediaType contentType}) async { - if (filename == null) filename = path.basename(filePath); - var file = new File(filePath); - var length = await file.length(); - var stream = new ByteStream(DelegatingStream.typed(file.openRead())); - return new MultipartFile(field, stream, length, - filename: filename, - contentType: contentType); + var bytes = await collectBytes(stream); + + return new MultipartFile(field, bytes, + 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."); + /// Returns a [Stream] representing the contents of the file. + /// + /// Can only be called once. + Stream> read() { + if (_stream == null) { + throw new StateError('The "read" method can only be called once on a ' + 'http.MultipartFile object.'); } - _isFinalized = true; - return _stream; + var stream = _stream; + _stream = null; + return stream; + } + + /// Looks up the [MediaType] from the [filename]'s extension or from + /// magic numbers contained within a file header's [bytes]. + static MediaType _lookUpMediaType(String filename, [List bytes]) { + if (filename == null && bytes == null) return null; + + // lookupMimeType expects filename to be non-null but its possible that + // this can be called with bytes but no filename. + // FIXME: https://github.com/dart-lang/mime/issues/11 + var mimeType = lookupMimeType(filename ?? '', headerBytes: bytes); + + return mimeType != null ? new MediaType.parse(mimeType) : null; } } diff --git a/lib/src/multipart_request.dart b/lib/src/multipart_request.dart deleted file mode 100644 index 8132f80924..0000000000 --- a/lib/src/multipart_request.dart +++ /dev/null @@ -1,165 +0,0 @@ -// 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 'boundary_characters.dart'; -import 'byte_stream.dart'; -import 'multipart_file.dart'; -import 'utils.dart'; - -final _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 fields; - - /// The private version of [files]. - final List _files; - - /// Creates a new [MultipartRequest]. - MultipartRequest(String method, Uri url) - : fields = {}, - _files = [], - super(method, url); - - /// The list of files to upload for this request. - List get files => _files; - - /// The total length of the request body, in bytes. This is calculated from - /// [fields] and [files] and cannot be set manually. - int get contentLength { - var length = 0; - - fields.forEach((name, value) { - length += "--".length + _BOUNDARY_LENGTH + "\r\n".length + - UTF8.encode(_headerForField(name, value)).length + - UTF8.encode(value).length + "\r\n".length; - }); - - for (var 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; - } - - void 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. - ByteStream finalize() { - // TODO(nweiz): freeze fields and files - var boundary = _boundaryString(); - headers['content-type'] = 'multipart/form-data; boundary=$boundary'; - super.finalize(); - - var controller = new StreamController>(sync: true); - - void writeAscii(String string) { - controller.add(UTF8.encode(string)); - } - - writeUtf8(String string) => controller.add(UTF8.encode(string)); - writeLine() => controller.add([13, 10]); // \r\n - - fields.forEach((name, value) { - writeAscii('--$boundary\r\n'); - writeAscii(_headerForField(name, value)); - writeUtf8(value); - writeLine(); - }); - - Future.forEach(_files, (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); - } - - /// Returns the header string for a field. The return value is guaranteed to - /// contain only ASCII characters. - String _headerForField(String name, String value) { - var 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) { - var 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() { - var prefix = "dart-http-boundary-"; - var list = new List.generate(_BOUNDARY_LENGTH - prefix.length, - (index) => - BOUNDARY_CHARACTERS[_random.nextInt(BOUNDARY_CHARACTERS.length)], - growable: false); - return "$prefix${new String.fromCharCodes(list)}"; - } -} diff --git a/lib/src/request.dart b/lib/src/request.dart index f49c5b9f16..c50f018648 100644 --- a/lib/src/request.dart +++ b/lib/src/request.dart @@ -4,7 +4,10 @@ import 'dart:convert'; +import 'boundary.dart'; import 'message.dart'; +import 'multipart_body.dart'; +import 'multipart_file.dart'; import 'utils.dart'; /// Represents an HTTP request to be sent to a server. @@ -45,8 +48,7 @@ class Request extends Message { /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.head(url, - {Map headers, Map context}) + Request.head(url, {Map headers, Map context}) : this('HEAD', url, headers: headers, context: context); /// Creates a new GET [Request] to [url], which can be a [Uri] or a [String]. @@ -56,8 +58,7 @@ class Request extends Message { /// /// Extra [context] can be used to pass information between inner middleware /// and handlers. - Request.get(url, - {Map headers, Map context}) + Request.get(url, {Map headers, Map context}) : this('GET', url, headers: headers, context: context); /// Creates a new POST [Request] to [url], which can be a [Uri] or a [String]. @@ -77,7 +78,7 @@ class Request extends Message { Map headers, Map context}) : this('POST', url, - body: body, encoding: encoding, headers: headers, context: context); + body: body, encoding: encoding, headers: headers, context: context); /// Creates a new PUT [Request] to [url], which can be a [Uri] or a [String]. /// @@ -96,7 +97,7 @@ class Request extends Message { Map headers, Map context}) : this('PUT', url, - body: body, encoding: encoding, headers: headers, context: context); + body: body, encoding: encoding, headers: headers, context: context); /// Creates a new PATCH [Request] to [url], which can be a [Uri] or a /// [String]. @@ -116,7 +117,7 @@ class Request extends Message { Map headers, Map context}) : this('PATCH', url, - body: body, encoding: encoding, headers: headers, context: context); + body: body, encoding: encoding, headers: headers, context: context); /// Creates a new DELETE [Request] to [url], which can be a [Uri] or a /// [String]. @@ -130,11 +131,44 @@ class Request extends Message { {Map headers, Map context}) : this('DELETE', url, headers: headers, context: context); - Request._(this.method, this.url, - body, - Encoding encoding, + /// Creates a new + /// [`multipart/form-data`](https://en.wikipedia.org/wiki/MIME#Multipart_messages) + /// [Request] to [url], which can be a [Uri] or a [String]. + /// + /// The content of the body is specified through the values of [fields] and + /// [files]. + /// + /// If [method] is not specified it defaults to POST. + /// + /// [headers] are the HTTP headers for the request. If [headers] is `null`, + /// it is treated as empty. + /// + /// Extra [context] can be used to pass information between inner middleware + /// and handlers. + factory Request.multipart(url, + {String method, Map headers, - Map context) + Map context, + Map fields, + Iterable files}) { + fields ??= const {}; + files ??= const []; + headers ??= {}; + + var boundary = boundaryString(); + + return new Request._( + method ?? 'POST', + getUrl(url), + new MultipartBody(fields, files, boundary), + null, + updateMap(headers, + {'content-type': 'multipart/form-data; boundary=$boundary'}), + context); + } + + Request._(this.method, this.url, body, Encoding encoding, + Map headers, Map context) : super(body, encoding: encoding, headers: headers, context: context); /// Creates a new [Request] by copying existing values and applying specified @@ -149,18 +183,11 @@ class Request extends Message { /// [body] is the request body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. Request change( - {Map headers, - Map context, - body}) { + {Map headers, Map context, body}) { var updatedHeaders = updateMap(this.headers, headers); var updatedContext = updateMap(this.context, context); - return new Request._( - this.method, - this.url, - body ?? getBody(this), - this.encoding, - updatedHeaders, - updatedContext); + return new Request._(this.method, this.url, body ?? getBody(this), + this.encoding, updatedHeaders, updatedContext); } } diff --git a/pubspec.yaml b/pubspec.yaml index 958b2babf8..fbe50ba65e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,8 @@ dependencies: async: "^1.13.0" collection: "^1.5.0" http_parser: ">=0.0.1 <4.0.0" - path: ">=0.9.0 <2.0.0" + mime: "^0.9.0" + typed_data: "^1.0.0" dev_dependencies: test: "^0.12.18" # Override dependency on package_resolver to enable test diff --git a/test/multipart_test.dart b/test/multipart_test.dart new file mode 100644 index 0000000000..7e539f5da0 --- /dev/null +++ b/test/multipart_test.dart @@ -0,0 +1,254 @@ +// Copyright (c) 2017, 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:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('empty', () { + var request = new http.Request.multipart(dummyUrl); + expect(request, multipartBodyMatches(''' + --{{boundary}}-- + ''')); + }); + + test('with fields and files', () { + var fields = { + 'field1': 'value1', + 'field2': 'value2', + }; + var files = [ + new http.MultipartFile('file1', 'contents1', filename: 'filename1.txt'), + new http.MultipartFile('file2', 'contents2'), + ]; + + var request = + new http.Request.multipart(dummyUrl, fields: fields, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-disposition: form-data; name="field1" + + value1 + --{{boundary}} + content-disposition: form-data; name="field2" + + value2 + --{{boundary}} + content-type: text/plain; charset=utf-8 + content-disposition: form-data; name="file1"; filename="filename1.txt" + + contents1 + --{{boundary}} + content-type: text/plain; charset=utf-8 + content-disposition: form-data; name="file2" + + contents2 + --{{boundary}}-- + ''')); + }); + + test('with a unicode field name', () { + var fields = {'fïēld': 'value'}; + + var request = new http.Request.multipart(dummyUrl, fields: fields); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-disposition: form-data; name="fïēld" + + value + --{{boundary}}-- + ''')); + }); + + test('with a field name with newlines', () { + var fields = {'foo\nbar\rbaz\r\nbang': 'value'}; + var request = new http.Request.multipart(dummyUrl, fields: fields); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-disposition: form-data; name="foo%0D%0Abar%0D%0Abaz%0D%0Abang" + + value + --{{boundary}}-- + ''')); + }); + + test('with a field name with a quote', () { + var fields = {'foo"bar': 'value'}; + var request = new http.Request.multipart(dummyUrl, fields: fields); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-disposition: form-data; name="foo%22bar" + + value + --{{boundary}}-- + ''')); + }); + + test('with a unicode field value', () { + var fields = {'field': 'vⱥlūe'}; + var request = new http.Request.multipart(dummyUrl, fields: fields); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-disposition: form-data; name="field" + content-type: text/plain; charset=utf-8 + content-transfer-encoding: binary + + vⱥlūe + --{{boundary}}-- + ''')); + }); + + test('with a unicode filename', () { + var files = [ + new http.MultipartFile('file', 'contents', filename: 'fïlēname.txt') + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: text/plain; charset=utf-8 + content-disposition: form-data; name="file"; filename="fïlēname.txt" + + contents + --{{boundary}}-- + ''')); + }); + + test('with a filename with newlines', () { + var files = [ + new http.MultipartFile('file', 'contents', + filename: 'foo\nbar\rbaz\r\nbang') + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: text/plain; charset=utf-8 + content-disposition: form-data; name="file"; filename="foo%0D%0Abar%0D%0Abaz%0D%0Abang" + + contents + --{{boundary}}-- + ''')); + }); + + test('with a filename with a quote', () { + var files = [ + new http.MultipartFile('file', 'contents', filename: 'foo"bar') + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: text/plain; charset=utf-8 + content-disposition: form-data; name="file"; filename="foo%22bar" + + contents + --{{boundary}}-- + ''')); + }); + + test('with a string file with a content-type but no charset', () { + var files = [ + new http.MultipartFile('file', '{"hello": "world"}', + contentType: new MediaType('application', 'json')) + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: application/json; charset=utf-8 + content-disposition: form-data; name="file" + + {"hello": "world"} + --{{boundary}}-- + ''')); + }); + + test('with a file with a iso-8859-1 body', () { + // "Ã¥" encoded as ISO-8859-1 and then read as UTF-8 results in "å". + var files = [ + new http.MultipartFile('file', 'non-ascii: "Ã¥"', + encoding: LATIN1, + contentType: + new MediaType('text', 'plain')) + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: text/plain; charset=iso-8859-1 + content-disposition: form-data; name="file" + + non-ascii: "å" + --{{boundary}}-- + ''')); + }); + + test('with a stream file', () { + var controller = new StreamController>(sync: true); + var files = [ + new http.MultipartFile.fromStream('file', controller.stream, 5) + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: application/octet-stream + content-disposition: form-data; name="file" + + hello + --{{boundary}}-- + ''')); + + controller + ..add([104, 101, 108, 108, 111]) + ..close(); + }); + + test('with an empty stream file', () { + var controller = new StreamController>(sync: true); + var files = [ + new http.MultipartFile.fromStream('file', controller.stream, 0) + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: application/octet-stream + content-disposition: form-data; name="file" + + + --{{boundary}}-- + ''')); + + controller.close(); + }); + + test('with a byte file', () { + var files = [ + new http.MultipartFile('file', [104, 101, 108, 108, 111]) + ]; + var request = new http.Request.multipart(dummyUrl, files: files); + + expect(request, multipartBodyMatches(''' + --{{boundary}} + content-type: application/octet-stream + content-disposition: form-data; name="file" + + hello + --{{boundary}}-- + ''')); + }); +} diff --git a/test/utils.dart b/test/utils.dart index 505ee5fda3..bf8da3a2d4 100644 --- a/test/utils.dart +++ b/test/utils.dart @@ -4,9 +4,10 @@ import 'dart:convert'; -import 'package:test/test.dart'; - +import 'package:async/async.dart'; import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; +import 'package:test/test.dart'; /// A dummy URL for constructing requests that won't be sent. Uri get dummyUrl => Uri.parse('http://dartlang.org/'); @@ -61,21 +62,57 @@ class _Parse extends Matcher { } Description describe(Description description) { - return description.add('parses to a value that ') - .addDescriptionOf(_matcher); + return description + .add('parses to a value that ') + .addDescriptionOf(_matcher); + } +} + +/// A matcher that validates the body of a multipart request after finalization. +/// +/// The string "{{boundary}}" in [pattern] will be replaced by the boundary +/// string for the request, and LF newlines will be replaced with CRLF. +/// Indentation will be normalized. +Matcher multipartBodyMatches(String pattern) => + new _MultipartBodyMatches(pattern); + +class _MultipartBodyMatches extends Matcher { + final String _pattern; + + _MultipartBodyMatches(this._pattern); + + bool matches(item, Map matchState) { + if (item is! http.Request) return false; + + var future = item.readAsBytes().then((bodyBytes) { + var body = UTF8.decode(bodyBytes); + var contentType = new MediaType.parse(item.headers['content-type']); + var boundary = contentType.parameters['boundary']; + var expected = cleanUpLiteral(_pattern) + .replaceAll('\n', '\r\n') + .replaceAll('{{boundary}}', boundary); + + expect(body, equals(expected)); + expect(item.contentLength, equals(bodyBytes.length)); + }); + + return completes.matches(future, matchState); } + + Description describe(Description description) => + description.add('has a body that matches "$_pattern"'); } /// A matcher that matches a [http.ClientException] with the given [message]. /// /// [message] can be a String or a [Matcher]. Matcher isClientException([message]) => predicate((error) { - expect(error, new isInstanceOf()); - if (message != null) { - expect(error.message, message); - } - return true; -}); + expect(error, new isInstanceOf()); + if (message != null) { + expect(error.message, message); + } + return true; + }); /// A matcher that matches function or future that throws a /// [http.ClientException] with the given [message]. From 6d3d7c55903d83e099b9e86e6fead2568ddc0e75 Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 26 Oct 2017 14:04:09 -0700 Subject: [PATCH 38/41] dartfmt (#131) --- lib/browser_client.dart | 4 +--- lib/http.dart | 34 +++++++++++++++++----------------- lib/src/base_client.dart | 25 +++++++++++-------------- lib/src/client.dart | 16 ++++++++-------- lib/src/handler_client.dart | 3 +-- lib/src/io_client.dart | 10 ++++------ lib/src/middleware.dart | 30 ++++++++++++++---------------- lib/src/response.dart | 25 +++++++++++-------------- test/hybrid/server.dart | 3 +-- test/multipart_test.dart | 4 +--- 10 files changed, 69 insertions(+), 85 deletions(-) diff --git a/lib/browser_client.dart b/lib/browser_client.dart index 2b1cb3b863..85d9034a66 100644 --- a/lib/browser_client.dart +++ b/lib/browser_client.dart @@ -52,9 +52,7 @@ class BrowserClient extends BaseClient { reader.onLoad.first.then((_) { var body = reader.result as Uint8List; - completer.complete(new Response( - xhr.responseUrl, - xhr.status, + completer.complete(new Response(xhr.responseUrl, xhr.status, reasonPhrase: xhr.statusText, body: new Stream.fromIterable([body]), headers: xhr.responseHeaders)); diff --git a/lib/http.dart b/lib/http.dart index 4736abf2da..32c0c48253 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -30,7 +30,7 @@ export 'src/response.dart'; /// /// For more fine-grained control over the request, use [Request] instead. Future head(url, {Map headers}) => - _withClient((client) => client.head(url, headers: headers)); + _withClient((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]. @@ -41,7 +41,7 @@ Future head(url, {Map headers}) => /// /// For more fine-grained control over the request, use [Request] instead. Future get(url, {Map headers}) => - _withClient((client) => client.get(url, headers: headers)); + _withClient((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]. @@ -62,10 +62,10 @@ Future get(url, {Map headers}) => /// /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. -Future post(url, body, {Map headers, - Encoding encoding}) => - _withClient((client) => client.post(url, body, - headers: headers, encoding: encoding)); +Future post(url, body, + {Map headers, Encoding encoding}) => + _withClient((client) => + client.post(url, body, headers: headers, 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,10 +86,10 @@ Future post(url, body, {Map headers, /// /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. -Future put(url, body, {Map headers, - Encoding encoding}) => - _withClient((client) => client.put(url, body, - headers: headers, encoding: encoding)); +Future put(url, body, + {Map headers, Encoding encoding}) => + _withClient((client) => + client.put(url, body, headers: headers, 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]. @@ -110,10 +110,10 @@ Future put(url, body, {Map headers, /// /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. -Future patch(url, body, {Map headers, - Encoding encoding}) => - _withClient((client) => client.patch(url, body, - headers: headers, encoding: encoding)); +Future patch(url, body, + {Map headers, Encoding encoding}) => + _withClient((client) => + client.patch(url, body, headers: headers, encoding: encoding)); /// Sends an HTTP DELETE request with the given headers to the given URL, which /// can be a [Uri] or a [String]. @@ -124,7 +124,7 @@ Future patch(url, body, {Map headers, /// /// For more fine-grained control over the request, use [Request] instead. Future delete(url, {Map headers}) => - _withClient((client) => client.delete(url, headers: headers)); + _withClient((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 @@ -140,7 +140,7 @@ Future delete(url, {Map headers}) => /// For more fine-grained control over the request and response, use [Request] /// instead. Future read(url, {Map headers}) => - _withClient((client) => client.read(url, headers: headers)); + _withClient((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 @@ -156,7 +156,7 @@ Future read(url, {Map headers}) => /// For more fine-grained control over the request and response, use [Request] /// instead. Future readBytes(url, {Map headers}) => - _withClient((client) => client.readBytes(url, headers: headers)); + _withClient((client) => client.readBytes(url, headers: headers)); Future _withClient(Future fn(Client client)) async { var client = new Client(); diff --git a/lib/src/base_client.dart b/lib/src/base_client.dart index 29dd1107e3..176422409f 100644 --- a/lib/src/base_client.dart +++ b/lib/src/base_client.dart @@ -22,14 +22,14 @@ abstract class BaseClient implements Client { /// /// For more fine-grained control over the request, use [send] instead. Future head(url, {Map headers}) => - send(new Request.head(url, headers: headers)); + send(new Request.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]. /// /// For more fine-grained control over the request, use [send] instead. Future get(url, {Map headers}) => - send(new Request.get(url, headers: headers)); + send(new Request.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]. @@ -49,10 +49,9 @@ abstract class BaseClient implements Client { /// [encoding] defaults to UTF-8. /// /// For more fine-grained control over the request, use [send] instead. - Future post(url, body, {Map headers, - Encoding encoding}) => - send(new Request.post(url, body, headers: headers, - encoding: encoding)); + Future post(url, body, + {Map headers, Encoding encoding}) => + send(new Request.post(url, body, headers: headers, 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]. @@ -72,10 +71,9 @@ abstract class BaseClient implements Client { /// [encoding] defaults to UTF-8. /// /// For more fine-grained control over the request, use [send] instead. - Future put(url, body, {Map headers, - Encoding encoding}) => - send(new Request.put(url, body, headers: headers, - encoding: encoding)); + Future put(url, body, + {Map headers, Encoding encoding}) => + send(new Request.put(url, body, headers: headers, 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]. @@ -95,10 +93,9 @@ abstract class BaseClient implements Client { /// [encoding] defaults to UTF-8. /// /// For more fine-grained control over the request, use [send] instead. - Future patch(url, body, {Map headers, - Encoding encoding}) => - send(new Request.patch(url, body, headers: headers, - encoding: encoding)); + Future patch(url, body, + {Map headers, Encoding encoding}) => + send(new Request.patch(url, body, headers: headers, encoding: encoding)); /// Sends an HTTP DELETE request with the given headers to the given URL, /// which can be a [Uri] or a [String]. diff --git a/lib/src/client.dart b/lib/src/client.dart index 82b4dc4bed..0ae108c263 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -36,8 +36,8 @@ abstract class Client { /// [Future]. It will be called when [Client.send] is invoked. /// /// When [Client.close] is called the [onClose] function will be called. - factory Client.handler(Handler handler, {void onClose()}) - => new HandlerClient(handler, onClose ?? () {}); + factory Client.handler(Handler handler, {void onClose()}) => + new HandlerClient(handler, onClose ?? () {}); /// Sends an HTTP HEAD request with the given headers to the given URL, which /// can be a [Uri] or a [String]. @@ -69,8 +69,8 @@ abstract class Client { /// [encoding] defaults to [UTF8]. /// /// For more fine-grained control over the request, use [send] instead. - Future post(url, body, {Map headers, - Encoding encoding}); + Future post(url, body, + {Map headers, 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]. @@ -90,8 +90,8 @@ abstract class Client { /// [encoding] defaults to [UTF8]. /// /// For more fine-grained control over the request, use [send] instead. - Future put(url, body, {Map headers, - Encoding encoding}); + Future put(url, body, + {Map headers, 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]. @@ -111,8 +111,8 @@ abstract class Client { /// [encoding] defaults to [UTF8]. /// /// For more fine-grained control over the request, use [send] instead. - Future patch(url, body, {Map headers, - Encoding encoding}); + Future patch(url, body, + {Map headers, Encoding encoding}); /// Sends an HTTP DELETE request with the given headers to the given URL, /// which can be a [Uri] or a [String]. diff --git a/lib/src/handler_client.dart b/lib/src/handler_client.dart index 475a3067e5..3540d3d022 100644 --- a/lib/src/handler_client.dart +++ b/lib/src/handler_client.dart @@ -18,8 +18,7 @@ class HandlerClient extends BaseClient { final void Function() _close; /// Creates a new client using the [_handler] and [onClose] functions. - HandlerClient(this._handler, void onClose()) - : _close = onClose; + HandlerClient(this._handler, void onClose()) : _close = onClose; /// Sends an HTTP request and asynchronously returns the response. Future send(Request request) => _handler(request); diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart index 6b2b52039f..5d634c4bd0 100644 --- a/lib/src/io_client.dart +++ b/lib/src/io_client.dart @@ -29,9 +29,9 @@ class IOClient extends BaseClient { var context = request.context; ioRequest - ..followRedirects = context['io.followRedirects'] ?? true - ..maxRedirects = context['io.maxRedirects'] ?? 5 - ..persistentConnection = context['io.persistentConnection'] ?? true; + ..followRedirects = context['io.followRedirects'] ?? true + ..maxRedirects = context['io.maxRedirects'] ?? 5 + ..persistentConnection = context['io.persistentConnection'] ?? true; request.headers.forEach((name, value) { ioRequest.headers.set(name, value); }); @@ -44,9 +44,7 @@ class IOClient extends BaseClient { headers[key] = values.join(','); }); - return new Response( - _responseUrl(request, response), - response.statusCode, + return new Response(_responseUrl(request, response), response.statusCode, reasonPhrase: response.reasonPhrase, body: DelegatingStream.typed>(response).handleError( (error) => throw new ClientException(error.message, error.uri), diff --git a/lib/src/middleware.dart b/lib/src/middleware.dart index 5635a5285c..1e9b349695 100644 --- a/lib/src/middleware.dart +++ b/lib/src/middleware.dart @@ -40,27 +40,25 @@ typedef Client Middleware(Client inner); /// If provided, [errorHandler] receives errors thrown by the inner handler. It /// does not receive errors thrown by [requestHandler] or [responseHandler]. /// It can either return a new response or throw an error. -Middleware createMiddleware({ - Future requestHandler(Request request), - Future responseHandler(Response response), - void onClose(), - void errorHandler(error, [StackTrace stackTrace]) -}) { +Middleware createMiddleware( + {Future requestHandler(Request request), + Future responseHandler(Response response), + void onClose(), + void errorHandler(error, [StackTrace stackTrace])}) { requestHandler ??= (request) async => request; responseHandler ??= (response) async => response; return (inner) { return new HandlerClient( - (request) => - requestHandler(request) - .then((req) => inner.send(req)) - .then((res) => responseHandler(res), onError: errorHandler), - onClose == null - ? inner.close - : () { - onClose(); - inner.close(); - }, + (request) => requestHandler(request) + .then((req) => inner.send(req)) + .then((res) => responseHandler(res), onError: errorHandler), + onClose == null + ? inner.close + : () { + onClose(); + inner.close(); + }, ); }; } diff --git a/lib/src/response.dart b/lib/src/response.dart index 7a7cfe2a07..8db825c7ec 100644 --- a/lib/src/response.dart +++ b/lib/src/response.dart @@ -42,10 +42,13 @@ class Response extends Message { Encoding encoding, Map headers, Map context}) - : this._(getUrl(finalUrl), statusCode, reasonPhrase ?? '', - body, encoding, headers, context); + : this._(getUrl(finalUrl), statusCode, reasonPhrase ?? '', body, encoding, + headers, context); - Response._(this.finalUrl, this.statusCode, this.reasonPhrase, + Response._( + this.finalUrl, + this.statusCode, + this.reasonPhrase, body, Encoding encoding, Map headers, @@ -68,20 +71,12 @@ class Response extends Message { /// [body] is the request body. It may be either a [String], a [List], a /// [Stream>], or `null` to indicate no body. Response change( - {Map headers, - Map context, - body}) { + {Map headers, Map context, body}) { var updatedHeaders = updateMap(this.headers, headers); var updatedContext = updateMap(this.context, context); - return new Response._( - this.finalUrl, - this.statusCode, - this.reasonPhrase, - body ?? getBody(this), - this.encoding, - updatedHeaders, - updatedContext); + return new Response._(this.finalUrl, this.statusCode, this.reasonPhrase, + body ?? getBody(this), this.encoding, updatedHeaders, updatedContext); } /// The date and time after which the response's data should be considered @@ -95,6 +90,7 @@ class Response extends Message { _expiresCache = parseHttpDate(headers['expires']); return _expiresCache; } + DateTime _expiresCache; /// The date and time the source of the response's data was last modified. @@ -107,5 +103,6 @@ class Response extends Message { _lastModifiedCache = parseHttpDate(headers['last-modified']); return _lastModifiedCache; } + DateTime _lastModifiedCache; } diff --git a/test/hybrid/server.dart b/test/hybrid/server.dart index ee305e6571..2a5e898d10 100644 --- a/test/hybrid/server.dart +++ b/test/hybrid/server.dart @@ -113,8 +113,7 @@ hybridMain(StreamChannel channel) async { requestBody = null; } else if (request.headers.contentType != null && request.headers.contentType.charset != null) { - var encoding = - encodingForCharset(request.headers.contentType.charset); + var encoding = encodingForCharset(request.headers.contentType.charset); requestBody = encoding.decode(requestBodyBytes); } else { requestBody = requestBodyBytes; diff --git a/test/multipart_test.dart b/test/multipart_test.dart index 7e539f5da0..bf0d22bbdf 100644 --- a/test/multipart_test.dart +++ b/test/multipart_test.dart @@ -180,9 +180,7 @@ void main() { // "Ã¥" encoded as ISO-8859-1 and then read as UTF-8 results in "å". var files = [ new http.MultipartFile('file', 'non-ascii: "Ã¥"', - encoding: LATIN1, - contentType: - new MediaType('text', 'plain')) + encoding: LATIN1, contentType: new MediaType('text', 'plain')) ]; var request = new http.Request.multipart(dummyUrl, files: files); From fc32f5135a416334c7711ac878f51f1b6a46556d Mon Sep 17 00:00:00 2001 From: Don Date: Thu, 26 Oct 2017 15:42:48 -0700 Subject: [PATCH 39/41] Documentation cleanup (#132) --- lib/browser_client.dart | 13 ++--- lib/http.dart | 31 ++++++------ lib/src/base_client.dart | 99 ++----------------------------------- lib/src/client.dart | 7 +-- lib/src/exception.dart | 4 ++ lib/src/io_client.dart | 3 -- lib/src/multipart_body.dart | 3 -- lib/src/pipeline.dart | 1 + lib/src/utils.dart | 4 +- 9 files changed, 33 insertions(+), 132 deletions(-) diff --git a/lib/browser_client.dart b/lib/browser_client.dart index 85d9034a66..3e6779bfa4 100644 --- a/lib/browser_client.dart +++ b/lib/browser_client.dart @@ -19,11 +19,10 @@ import 'src/response.dart'; /// A `dart:html`-based HTTP client that runs in the browser and is backed by /// XMLHttpRequests. /// -/// This client inherits some of the limitations of XMLHttpRequest. It ignores -/// the [BaseRequest.contentLength], [BaseRequest.persistentConnection], -/// [BaseRequest.followRedirects], and [BaseRequest.maxRedirects] fields. It is -/// also unable to stream requests or responses; a request will only be sent and -/// a response will only be returned once all the data is available. +/// This client inherits some of the limitations of XMLHttpRequest. It is +/// unable to directly set some headers, such as `content-length`. It is also +/// unable to stream requests or responses; a request will only be sent and a +/// response will only be returned once all the data is available. class BrowserClient extends BaseClient { /// The currently active XHRs. /// @@ -33,7 +32,6 @@ class BrowserClient extends BaseClient { /// Creates a new HTTP client. BrowserClient(); - /// Sends an HTTP request and asynchronously returns the response. Future send(Request request) async { var bytes = await collectBytes(request.read()); var xhr = new HttpRequest(); @@ -90,9 +88,6 @@ class BrowserClient extends BaseClient { request.open(method, url, async: asynch, user: user, password: password); } - /// Closes the client. - /// - /// This terminates all active requests. void close() { for (var xhr in _xhrs) { xhr.abort(); diff --git a/lib/http.dart b/lib/http.dart index 32c0c48253..ddf2711ddb 100644 --- a/lib/http.dart +++ b/lib/http.dart @@ -28,7 +28,9 @@ export 'src/response.dart'; /// 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. /// -/// For more fine-grained control over the request, use [Request] instead. +/// 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 head(url, {Map headers}) => _withClient((client) => client.head(url, headers: headers)); @@ -39,7 +41,9 @@ Future head(url, {Map headers}) => /// 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. /// -/// For more fine-grained control over the request, use [Request] instead. +/// 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 get(url, {Map headers}) => _withClient((client) => client.get(url, headers: headers)); @@ -60,8 +64,9 @@ Future get(url, {Map headers}) => /// /// [encoding] defaults to [UTF8]. /// -/// For more fine-grained control over the request, use [Request] or -/// [StreamedRequest] instead. +/// 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 post(url, body, {Map headers, Encoding encoding}) => _withClient((client) => @@ -84,8 +89,9 @@ Future post(url, body, /// /// [encoding] defaults to [UTF8]. /// -/// For more fine-grained control over the request, use [Request] or -/// [StreamedRequest] instead. +/// 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 put(url, body, {Map headers, Encoding encoding}) => _withClient((client) => @@ -108,8 +114,9 @@ Future put(url, body, /// /// [encoding] defaults to [UTF8]. /// -/// For more fine-grained control over the request, use [Request] or -/// [StreamedRequest] instead. +/// 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 patch(url, body, {Map headers, Encoding encoding}) => _withClient((client) => @@ -121,8 +128,6 @@ Future patch(url, body, /// 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. -/// -/// For more fine-grained control over the request, use [Request] instead. Future delete(url, {Map headers}) => _withClient((client) => client.delete(url, headers: headers)); @@ -136,9 +141,6 @@ Future delete(url, {Map headers}) => /// 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. -/// -/// For more fine-grained control over the request and response, use [Request] -/// instead. Future read(url, {Map headers}) => _withClient((client) => client.read(url, headers: headers)); @@ -152,9 +154,6 @@ Future read(url, {Map headers}) => /// 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. -/// -/// For more fine-grained control over the request and response, use [Request] -/// instead. Future readBytes(url, {Map headers}) => _withClient((client) => client.readBytes(url, headers: headers)); diff --git a/lib/src/base_client.dart b/lib/src/base_client.dart index 176422409f..48784f7d95 100644 --- a/lib/src/base_client.dart +++ b/lib/src/base_client.dart @@ -13,106 +13,32 @@ import 'exception.dart'; import 'request.dart'; import '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 +/// The abstract base class for an HTTP client. +/// +/// 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. Future head(url, {Map headers}) => send(new Request.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]. - /// - /// For more fine-grained control over the request, use [send] instead. Future get(url, {Map headers}) => send(new Request.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]. - /// - /// [body] sets the body of the request. It can be a [String], a [List] - /// or a [Map]. 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. Future post(url, body, {Map headers, Encoding encoding}) => send(new Request.post(url, body, headers: headers, 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] - /// or a [Map]. 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. Future put(url, body, {Map headers, Encoding encoding}) => send(new Request.put(url, body, headers: headers, 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] - /// or a [Map]. 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. Future patch(url, body, {Map headers, Encoding encoding}) => send(new Request.patch(url, body, headers: headers, 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 delete(url, {Map headers}) => send(new Request.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 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 read(url, {Map headers}) async { var response = await get(url, headers: headers); _checkResponseSuccess(url, response); @@ -120,15 +46,6 @@ abstract class BaseClient implements Client { return await response.readAsString(); } - /// 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. Future readBytes(url, {Map headers}) async { var response = await get(url, headers: headers); _checkResponseSuccess(url, response); @@ -136,13 +53,6 @@ abstract class BaseClient implements Client { return await collectBytes(response.read()); } - /// 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. Future send(Request request); /// Throws an error if [response] is not successful. @@ -156,8 +66,5 @@ abstract class BaseClient implements Client { 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. void close() {} } diff --git a/lib/src/client.dart b/lib/src/client.dart index 0ae108c263..8ce424d2e1 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -145,8 +145,9 @@ abstract class Client { /// Sends an HTTP request and asynchronously returns the response. Future send(Request 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. + /// 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(); } diff --git a/lib/src/exception.dart b/lib/src/exception.dart index db2c2240a4..3373a62ac9 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -4,11 +4,15 @@ /// An exception caused by an error in a pkg/http client. class ClientException implements Exception { + /// Message describing the problem. final String message; /// The URL of the HTTP request or response that failed. final Uri uri; + /// Creates a [ClientException] explained in [message]. + /// + /// The [uri] points to the URL being requested if applicable. ClientException(this.message, [this.uri]); String toString() => message; diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart index 5d634c4bd0..e0f790bc0d 100644 --- a/lib/src/io_client.dart +++ b/lib/src/io_client.dart @@ -22,7 +22,6 @@ class IOClient extends BaseClient { /// Creates a new HTTP client. IOClient([HttpClient inner]) : _inner = inner ?? new HttpClient(); - /// Sends an HTTP request and asynchronously returns the response. Future send(Request request) async { try { var ioRequest = await _inner.openUrl(request.method, request.url); @@ -57,8 +56,6 @@ class IOClient extends BaseClient { } } - /// Closes the client. This terminates all active connections. If a client - /// remains unclosed, the Dart process may not terminate. void close() { if (_inner != null) _inner.close(force: true); _inner = null; diff --git a/lib/src/multipart_body.dart b/lib/src/multipart_body.dart index 961db27973..63fd25b1fa 100644 --- a/lib/src/multipart_body.dart +++ b/lib/src/multipart_body.dart @@ -84,9 +84,6 @@ class MultipartBody implements Body { MultipartBody._(this._stream, this.contentLength); - /// Returns a [Stream] representing the body. - /// - /// Can only be called once. Stream> read() { if (_stream == null) { throw new StateError("The 'read' method can only be called once on a " diff --git a/lib/src/pipeline.dart b/lib/src/pipeline.dart index fc100ba0de..b538ae72c9 100644 --- a/lib/src/pipeline.dart +++ b/lib/src/pipeline.dart @@ -20,6 +20,7 @@ class Pipeline { /// The [Middleware] that is invoked at this stage. final Middleware _middleware; + /// Creates a [Pipeline]. const Pipeline() : _parent = null, _middleware = null; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index bb5c735ec4..e4cf005cfe 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -45,8 +45,8 @@ bool isPlainAscii(String string) => _asciiOnly.hasMatch(string); /// Pipes all data and errors from [stream] into [sink]. /// -/// Completes [Future] once [stream] is done. Unlike [store], [sink] remains -/// open after [stream] is done. +/// Completes [Future] once [stream] is done. [sink] remains open after [stream] +/// is done. Future writeStreamToSink(Stream stream, EventSink sink) { var completer = new Completer(); stream.listen(sink.add, From 29a007721856743c41519dfff7d3b40c5ca750c1 Mon Sep 17 00:00:00 2001 From: Don Date: Mon, 30 Oct 2017 18:34:06 -0700 Subject: [PATCH 40/41] Modify context variable names and write docs (#133) --- lib/browser_client.dart | 10 +++++++++- lib/src/io_client.dart | 25 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/browser_client.dart b/lib/browser_client.dart index 3e6779bfa4..2932060ae0 100644 --- a/lib/browser_client.dart +++ b/lib/browser_client.dart @@ -23,6 +23,13 @@ import 'src/response.dart'; /// unable to directly set some headers, such as `content-length`. It is also /// unable to stream requests or responses; a request will only be sent and a /// response will only be returned once all the data is available. +/// +/// You can control the underlying `dart:html` [HttpRequest] by adding values to +/// [Request.context]: +/// +/// * `"http.html.with_credentials"` is a boolean that defaults to `false`. If +/// it's `true`, cross-site requests will include credentials such as cookies +/// or authorization headers. See also [HttpRequest.withCredentials]. class BrowserClient extends BaseClient { /// The currently active XHRs. /// @@ -38,7 +45,8 @@ class BrowserClient extends BaseClient { _xhrs.add(xhr); _openHttpRequest(xhr, request.method, request.url.toString(), asynch: true); xhr.responseType = 'blob'; - xhr.withCredentials = request.context['html.withCredentials'] ?? false; + xhr.withCredentials = + request.context['http.html.with_credentials'] ?? false; request.headers.forEach(xhr.setRequestHeader); var completer = new Completer(); diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart index e0f790bc0d..92b1b20cf3 100644 --- a/lib/src/io_client.dart +++ b/lib/src/io_client.dart @@ -15,6 +15,24 @@ import 'response.dart'; /// A `dart:io`-based HTTP client. /// /// This is the default client when running on the command line. +/// +/// You can control the underlying `dart:io` [HttpRequest] by adding values to +/// [Request.context]: +/// +/// * `"http.io.follow_redirects"` is a boolean. If it's `true` (the default) +/// then the request will automatically follow HTTP redirects. If it's +/// `false`, the client will need to handle redirects manually. See also +/// [HttpClientRequest.followRedirects]. +/// +/// * `"http.io.max_redirects"` is an integer that specifies the maximum number +/// of redirects that will be followed if `follow_redirects` is `true`. If the +/// site redirects more than this, [send] will throw a [ClientException]. It +/// defaults to `5`. See also [HttpClientRequest.maxRedirects]. +/// +/// * `"http.io.persistent_connection"` is a boolean. If it's `true` (the +/// default) the client will request that the TCP connection be kept alive +/// after the request completes. See also +/// [HttpClientRequest.persistentConnection]. class IOClient extends BaseClient { /// The underlying `dart:io` HTTP client. HttpClient _inner; @@ -28,9 +46,10 @@ class IOClient extends BaseClient { var context = request.context; ioRequest - ..followRedirects = context['io.followRedirects'] ?? true - ..maxRedirects = context['io.maxRedirects'] ?? 5 - ..persistentConnection = context['io.persistentConnection'] ?? true; + ..followRedirects = context['http.io.follow_redirects'] ?? true + ..maxRedirects = context['http.io.max_redirects'] ?? 5 + ..persistentConnection = + context['http.io.persistent_connection'] ?? true; request.headers.forEach((name, value) { ioRequest.headers.set(name, value); }); From 155430c3e8a57f02b734058c2349a5b3ad9115d8 Mon Sep 17 00:00:00 2001 From: Don Date: Tue, 31 Oct 2017 10:56:50 -0700 Subject: [PATCH 41/41] Add timeout support in BrowserClient --- lib/browser_client.dart | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/browser_client.dart b/lib/browser_client.dart index 2932060ae0..91bd0df9e9 100644 --- a/lib/browser_client.dart +++ b/lib/browser_client.dart @@ -30,6 +30,11 @@ import 'src/response.dart'; /// * `"http.html.with_credentials"` is a boolean that defaults to `false`. If /// it's `true`, cross-site requests will include credentials such as cookies /// or authorization headers. See also [HttpRequest.withCredentials]. +/// +/// * `"http.html.timeout"` is an integer that specifies the time in +/// milliseconds before a request is automatically terminated. It defaults to +/// `0` which prevents a request from timing out. See also +/// [HttpRequest.timeout]. class BrowserClient extends BaseClient { /// The currently active XHRs. /// @@ -44,9 +49,12 @@ class BrowserClient extends BaseClient { var xhr = new HttpRequest(); _xhrs.add(xhr); _openHttpRequest(xhr, request.method, request.url.toString(), asynch: true); - xhr.responseType = 'blob'; - xhr.withCredentials = - request.context['http.html.with_credentials'] ?? false; + + xhr + ..responseType = 'blob' + ..withCredentials = request.context['http.html.with_credentials'] ?? false + ..timeout = request.context['http.html.timeout'] ?? 0; + request.headers.forEach(xhr.setRequestHeader); var completer = new Completer(); @@ -73,6 +81,13 @@ class BrowserClient extends BaseClient { reader.readAsArrayBuffer(blob); }); + xhr.onTimeout.first.then((error) { + completer.completeError( + new ClientException( + 'XMLHttpRequest timeout after ${xhr.timeout}ms.', request.url), + StackTrace.current); + }); + xhr.onError.first.then((_) { // Unfortunately, the underlying XMLHttpRequest API doesn't expose any // specific information about the error itself.