diff --git a/analysis_options.yaml b/analysis_options.yaml index e536739953..f45f6c9689 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,16 +8,16 @@ analyzer: linter: rules: - - avoid_bool_literals_in_conditional_expressions - - avoid_classes_with_only_static_members - - avoid_private_typedef_functions - - avoid_returning_this - - avoid_unused_constructor_parameters - - cascade_invocations - - join_return_with_assignment - - missing_whitespace_between_adjacent_strings - - no_adjacent_strings_in_list - - no_runtimeType_toString - - prefer_const_declarations - - prefer_expression_function_bodies - - use_string_buffers + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + - avoid_returning_this + - avoid_unused_constructor_parameters + - cascade_invocations + - join_return_with_assignment + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - prefer_const_declarations + - prefer_expression_function_bodies + - use_string_buffers diff --git a/pkgs/http/lib/http.dart b/pkgs/http/lib/http.dart index 62004240c7..2e2a802c85 100644 --- a/pkgs/http/lib/http.dart +++ b/pkgs/http/lib/http.dart @@ -20,6 +20,7 @@ export 'src/base_response.dart'; export 'src/byte_stream.dart'; export 'src/client.dart' hide zoneClient; export 'src/exception.dart'; +export 'src/headers.dart'; export 'src/multipart_file.dart'; export 'src/multipart_request.dart'; export 'src/request.dart'; diff --git a/pkgs/http/lib/retry.dart b/pkgs/http/lib/retry.dart index dedba9a9e7..95e9e0aa08 100644 --- a/pkgs/http/lib/retry.dart +++ b/pkgs/http/lib/retry.dart @@ -133,10 +133,10 @@ final class RetryClient extends BaseClient { /// Returns a copy of [original] with the given [body]. StreamedRequest _copyRequest(BaseRequest original, Stream> body) { - final request = StreamedRequest(original.method, original.url) + final request = StreamedRequest(original.method, original.url, + headers: original.headers) ..contentLength = original.contentLength ..followRedirects = original.followRedirects - ..headers.addAll(original.headers) ..maxRedirects = original.maxRedirects ..persistentConnection = original.persistentConnection; diff --git a/pkgs/http/lib/src/base_client.dart b/pkgs/http/lib/src/base_client.dart index 48a7f92fe9..f99945b992 100644 --- a/pkgs/http/lib/src/base_client.dart +++ b/pkgs/http/lib/src/base_client.dart @@ -19,42 +19,42 @@ import 'streamed_response.dart'; /// maybe [close], and then they get various convenience methods for free. abstract mixin class BaseClient implements Client { @override - Future head(Uri url, {Map? headers}) => + Future head(Uri url, {Object? headers}) => _sendUnstreamed('HEAD', url, headers); @override - Future get(Uri url, {Map? headers}) => + Future get(Uri url, {Object? headers}) => _sendUnstreamed('GET', url, headers); @override Future post(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Object? headers, Object? body, Encoding? encoding}) => _sendUnstreamed('POST', url, headers, body, encoding); @override Future put(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Object? headers, Object? body, Encoding? encoding}) => _sendUnstreamed('PUT', url, headers, body, encoding); @override Future patch(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Object? headers, Object? body, Encoding? encoding}) => _sendUnstreamed('PATCH', url, headers, body, encoding); @override Future delete(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Object? headers, Object? body, Encoding? encoding}) => _sendUnstreamed('DELETE', url, headers, body, encoding); @override - Future read(Uri url, {Map? headers}) async { + Future read(Uri url, {Object? headers}) async { final response = await get(url, headers: headers); _checkResponseSuccess(url, response); return response.body; } @override - Future readBytes(Uri url, {Map? headers}) async { + Future readBytes(Uri url, {Object? headers}) async { final response = await get(url, headers: headers); _checkResponseSuccess(url, response); return response.bodyBytes; @@ -71,12 +71,10 @@ abstract mixin class BaseClient implements Client { Future send(BaseRequest request); /// Sends a non-streaming [Request] and returns a non-streaming [Response]. - Future _sendUnstreamed( - String method, Uri url, Map? headers, + Future _sendUnstreamed(String method, Uri url, Object? headers, [Object? body, Encoding? encoding]) async { - var request = Request(method, url); + final request = Request(method, url, headers: headers); - if (headers != null) request.headers.addAll(headers); if (encoding != null) request.encoding = encoding; if (body != null) { if (body is String) { diff --git a/pkgs/http/lib/src/base_request.dart b/pkgs/http/lib/src/base_request.dart index 70a78695aa..6763e4b7de 100644 --- a/pkgs/http/lib/src/base_request.dart +++ b/pkgs/http/lib/src/base_request.dart @@ -2,8 +2,6 @@ // 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:meta/meta.dart'; import '../http.dart' show ClientException, get; @@ -11,6 +9,7 @@ import 'base_client.dart'; import 'base_response.dart'; import 'byte_stream.dart'; import 'client.dart'; +import 'headers.dart'; import 'streamed_response.dart'; import 'utils.dart'; @@ -82,7 +81,7 @@ abstract class BaseRequest { // TODO(nweiz): automatically parse cookies from headers // TODO(nweiz): make this a HttpHeaders object - final Map headers; + final Headers headers; /// Whether [finalize] has been called. bool get finalized => _finalized; @@ -96,11 +95,9 @@ abstract class BaseRequest { return method; } - BaseRequest(String method, this.url) + BaseRequest(String method, this.url, {Object? headers}) : method = _validateMethod(method), - headers = LinkedHashMap( - equals: (key1, key2) => key1.toLowerCase() == key2.toLowerCase(), - hashCode: (key) => key.toLowerCase().hashCode); + headers = Headers(headers); /// Finalizes the HTTP request in preparation for it being sent. /// diff --git a/pkgs/http/lib/src/base_response.dart b/pkgs/http/lib/src/base_response.dart index ed95f6cdb2..6962c445e3 100644 --- a/pkgs/http/lib/src/base_response.dart +++ b/pkgs/http/lib/src/base_response.dart @@ -4,6 +4,7 @@ import 'base_client.dart'; import 'base_request.dart'; +import 'headers.dart'; /// The base class for HTTP responses. /// @@ -47,20 +48,22 @@ abstract class BaseResponse { /// by a single space. Leading and trailing whitespace in header values are /// always removed. // TODO(nweiz): make this a HttpHeaders object. - final Map headers; + final Headers headers; final bool isRedirect; /// Whether the server requested that a persistent connection be maintained. final bool persistentConnection; - BaseResponse(this.statusCode, - {this.contentLength, - this.request, - this.headers = const {}, - this.isRedirect = false, - this.persistentConnection = true, - this.reasonPhrase}) { + BaseResponse( + this.statusCode, { + this.contentLength, + this.request, + Object? headers, + this.isRedirect = false, + this.persistentConnection = true, + this.reasonPhrase, + }) : headers = Headers(headers) { if (statusCode < 100) { throw ArgumentError('Invalid status code $statusCode.'); } else if (contentLength != null && contentLength! < 0) { diff --git a/pkgs/http/lib/src/browser_client.dart b/pkgs/http/lib/src/browser_client.dart index 80db8b1291..821af09627 100644 --- a/pkgs/http/lib/src/browser_client.dart +++ b/pkgs/http/lib/src/browser_client.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:js_interop'; import 'package:web/helpers.dart'; @@ -62,14 +63,22 @@ class BrowserClient extends BaseClient { ..open(request.method, '${request.url}', true) ..responseType = 'arraybuffer' ..withCredentials = withCredentials; - for (var header in request.headers.entries) { - xhr.setRequestHeader(header.key, header.value); + + // Set the headers. + for (final (name, value) in request.headers.entries()) { + xhr.setRequestHeader(name, value); + } + + // Sets cookies. + for (final cookie in request.headers.getSetCookie()) { + xhr.setRequestHeader('Set-Cookie', cookie); } var completer = Completer(); unawaited(xhr.onLoad.first.then((_) { - if (xhr.responseHeaders['content-length'] case final contentLengthHeader + if (xhr.responseHeaders.get('content-length') + case final contentLengthHeader when contentLengthHeader != null && !_digitRegex.hasMatch(contentLengthHeader)) { completer.completeError(ClientException( @@ -118,12 +127,11 @@ class BrowserClient extends BaseClient { } extension on XMLHttpRequest { - Map get responseHeaders { - // from Closure's goog.net.Xhrio.getResponseHeaders. - var headers = {}; - var headersString = getAllResponseHeaders(); - var headersList = headersString.split('\r\n'); - for (var header in headersList) { + Headers get responseHeaders { + final headers = Headers(); + final lines = const LineSplitter().convert(getAllResponseHeaders()); + + for (var header in lines) { if (header.isEmpty) { continue; } @@ -132,14 +140,13 @@ extension on XMLHttpRequest { if (splitIdx == -1) { continue; } + var key = header.substring(0, splitIdx).toLowerCase(); var value = header.substring(splitIdx + 2); - if (headers.containsKey(key)) { - headers[key] = '${headers[key]}, $value'; - } else { - headers[key] = value; - } + + headers.append(key, value); } + return headers; } } diff --git a/pkgs/http/lib/src/headers.dart b/pkgs/http/lib/src/headers.dart new file mode 100644 index 0000000000..9ffcf688eb --- /dev/null +++ b/pkgs/http/lib/src/headers.dart @@ -0,0 +1,175 @@ +import 'dart:convert'; + +/// This Fetch API interface allows you to perform various actions on HTTP +/// request and response headers. These actions include retrieving, setting, +/// adding to, and removing. A Headers object has an associated header list, +/// which is initially empty and consists of zero or more name and value pairs. +///  You can add to this using methods like append() (see Examples.) In all +/// methods of this interface, header names are matched by case-insensitive +/// byte sequence. +/// +/// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers) +class Headers { + final List<(String, String)> _storage; + + /// Internal constructor, to create a new instance of `Headers`. + const Headers._(this._storage); + + /// The Headers() constructor creates a new Headers object. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/Headers) + factory Headers([Object? init]) => Headers._((init,).toStorage()); + + /// Appends a new value onto an existing header inside a Headers object, or + /// adds the header if it does not already exist. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/append) + void append(String name, String value) => _storage.add((name, value)); + + /// Deletes a header from a Headers object. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/delete) + void delete(String name) => + _storage.removeWhere((element) => element.$1.equals(name)); + + /// Returns an iterator allowing to go through all key/value pairs contained + /// in this object. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/entries) + Iterable<(String, String)> entries() sync* { + for (final (name, value) in _storage) { + // https://fetch.spec.whatwg.org/#ref-for-forbidden-response-header-name%E2%91%A0 + if (name.equals('set-cookie')) continue; + + yield (name, value); + } + } + + /// Executes a provided function once for each key/value pair in this Headers object. + /// + /// [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Headers/forEach) + void forEach(void Function(String value, String name, Headers parent) fn) => + entries().forEach((element) => fn(element.$2, element.$1, this)); + + /// Returns a String sequence of all the values of a header within a Headers + /// object with a given name. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/get) + String? get(String name) => switch (_storage.valuesOf(name)) { + Iterable values when values.isNotEmpty => values.join(', '), + _ => null, + }; + + /// Returns an array containing the values of all Set-Cookie headers + /// associated with a response. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/getSetCookie) + Iterable getSetCookie() => _storage.valuesOf('Set-Cookie'); + + /// Returns a boolean stating whether a Headers object contains a certain + /// header. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/has) + bool has(String name) => _storage.any((element) => element.$1.equals(name)); + + /// Returns an iterator allowing you to go through all keys of the key/value + /// pairs contained in this object. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/keys) + Iterable keys() => _storage.map((e) => e.$1).toSet(); + + /// Sets a new value for an existing header inside a Headers object, or adds + /// the header if it does not already exist. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/set) + void set(String name, String value) => this + ..delete(name) + ..append(name, value); + + /// Returns an iterator allowing you to go through all values of the + /// key/value pairs contained in this object. + /// + /// [MDN Reference](https://developer.mozilla.org/docs/Web/API/Headers/values) + Iterable values() => keys().map(get).whereType(); +} + +extension on String { + bool equals(String other) => other.toLowerCase() == toLowerCase(); +} + +extension on Iterable<(String, String)> { + Iterable valuesOf(String name) => + where((element) => element.$1.equals(name)).map((e) => e.$2); +} + +extension on (Object?,) { + List<(String, String)> toStorage() => switch (this.$1) { + Headers value => value.toStorage(), + String value => value.toStorage(), + Iterable value => value.toStorage(), + Iterable<(String, String)> value => value.toList(), + Iterable> value => value.toStorage(), + Map value => value.toStorage(), + Map> value => value.toStorage(), + _ => [], + }; +} + +extension on Map> { + List<(String, String)> toStorage() => entries + .map((e) => e.value.map((value) => (e.key, value))) + .expand((e) => e) + .toList(); +} + +extension on Map { + List<(String, String)> toStorage() => + entries.map((e) => (e.key, e.value)).toList(); +} + +extension on Iterable> { + List<(String, String)> toStorage() { + final storage = <(String, String)>[]; + for (final element in this) { + switch (element) { + case Iterable value when value.length == 2: + storage.add((value.first, value.last)); + break; + case Iterable value when value.length == 1: + final pair = value.first.toHeadersPair(); + if (pair != null) storage.add(pair); + break; + case Iterable value when value.length > 2: + for (final element in value.skip(1)) { + storage.add((value.first, element)); + } + break; + } + } + + return storage; + } +} + +extension on Iterable { + List<(String, String)> toStorage() => + map((e) => e.toHeadersPair()).whereType<(String, String)>().toList(); +} + +extension on Headers { + List<(String, String)> toStorage() => entries().toList(); +} + +extension on String { + /// Converts a string to a list of headers. + List<(String, String)> toStorage() => + const LineSplitter().convert(this).toStorage(); + + /// Parses to a header pair. + (String, String)? toHeadersPair() { + final index = indexOf(':'); + if (index == -1) return null; + + return (substring(0, index), substring(index + 1).trim()); + } +} diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart index db66b028c4..c0b45b69ad 100644 --- a/pkgs/http/lib/src/io_client.dart +++ b/pkgs/http/lib/src/io_client.dart @@ -102,10 +102,25 @@ class IOClient extends BaseClient { ..maxRedirects = request.maxRedirects ..contentLength = (request.contentLength ?? -1) ..persistentConnection = request.persistentConnection; - request.headers.forEach((name, value) { - ioRequest.headers.set(name, value); + + final defaultUserAgent = ioRequest.headers.value('user-agent'); + ioRequest.headers.removeAll('user-agent'); + + // Set the headers. + request.headers.forEach((value, name, _) { + ioRequest.headers.add(name, value); }); + if (ioRequest.headers.value('user-agent') == null && + defaultUserAgent != null) { + ioRequest.headers.set('user-agent', defaultUserAgent); + } + + // Set cookies. + for (final cookie in request.headers.getSetCookie()) { + ioRequest.headers.add('set-cookie', cookie); + } + var response = await stream.pipe(ioRequest) as HttpClientResponse; var headers = {}; diff --git a/pkgs/http/lib/src/mock_client.dart b/pkgs/http/lib/src/mock_client.dart index 52f108a577..b1a6a49166 100644 --- a/pkgs/http/lib/src/mock_client.dart +++ b/pkgs/http/lib/src/mock_client.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'base_client.dart'; import 'base_request.dart'; import 'byte_stream.dart'; +import 'headers.dart'; import 'request.dart'; import 'response.dart'; import 'streamed_request.dart'; @@ -100,3 +101,15 @@ typedef MockClientStreamHandler = Future Function( /// /// Note that [request] will be finalized. typedef MockClientHandler = Future Function(Request request); + +extension on Headers { + /// Appends other headers to this. + void addAll(Headers other) { + other.forEach((value, name, _) => append(name, value)); + + // Set cookies. + for (final cookie in other.getSetCookie()) { + append('set-cookie', cookie); + } + } +} diff --git a/pkgs/http/lib/src/multipart_request.dart b/pkgs/http/lib/src/multipart_request.dart index 79525421fb..d0219c4c3b 100644 --- a/pkgs/http/lib/src/multipart_request.dart +++ b/pkgs/http/lib/src/multipart_request.dart @@ -87,7 +87,8 @@ class MultipartRequest extends BaseRequest { ByteStream finalize() { // TODO: freeze fields and files final boundary = _boundaryString(); - headers['content-type'] = 'multipart/form-data; boundary=$boundary'; + headers.set('content-type', 'multipart/form-data; boundary=$boundary'); + super.finalize(); return ByteStream(_finalize(boundary)); } diff --git a/pkgs/http/lib/src/request.dart b/pkgs/http/lib/src/request.dart index c15e55169d..c0b822ceb5 100644 --- a/pkgs/http/lib/src/request.dart +++ b/pkgs/http/lib/src/request.dart @@ -149,7 +149,7 @@ class Request extends BaseRequest { body = mapToQuery(fields, encoding: encoding); } - Request(super.method, super.url) + Request(super.method, super.url, {super.headers}) : _defaultEncoding = utf8, _bodyBytes = Uint8List(0); @@ -163,17 +163,17 @@ class Request extends BaseRequest { /// The `Content-Type` header of the request (if it exists) as a [MediaType]. MediaType? get _contentType { - var contentType = headers['content-type']; + var contentType = headers.get('content-type'); if (contentType == null) return null; return MediaType.parse(contentType); } set _contentType(MediaType? value) { if (value == null) { - headers.remove('content-type'); - } else { - headers['content-type'] = value.toString(); + return headers.delete('content-type'); } + + headers.set('content-type', value.toString()); } /// Throw an error if this request has been finalized. diff --git a/pkgs/http/lib/src/response.dart b/pkgs/http/lib/src/response.dart index 1ba7c466cf..cac141de99 100644 --- a/pkgs/http/lib/src/response.dart +++ b/pkgs/http/lib/src/response.dart @@ -9,6 +9,7 @@ import 'package:http_parser/http_parser.dart'; import 'base_request.dart'; import 'base_response.dart'; +import 'headers.dart'; import 'streamed_response.dart'; import 'utils.dart'; @@ -30,7 +31,7 @@ class Response extends BaseResponse { /// Creates a new HTTP response with a string body. Response(String body, int statusCode, {BaseRequest? request, - Map headers = const {}, + Headers? headers, bool isRedirect = false, bool persistentConnection = true, String? reasonPhrase}) @@ -68,14 +69,14 @@ class Response extends BaseResponse { /// /// Defaults to [latin1] if the headers don't specify a charset or if that /// charset is unknown. -Encoding _encodingForHeaders(Map headers) => +Encoding _encodingForHeaders(Headers? headers) => encodingForCharset(_contentTypeForHeaders(headers).parameters['charset']); /// Returns the [MediaType] object for the given headers's content-type. /// /// Defaults to `application/octet-stream`. -MediaType _contentTypeForHeaders(Map headers) { - var contentType = headers['content-type']; +MediaType _contentTypeForHeaders(Headers? headers) { + var contentType = headers?.get('content-type'); if (contentType != null) return MediaType.parse(contentType); return MediaType('application', 'octet-stream'); } diff --git a/pkgs/http/lib/src/streamed_request.dart b/pkgs/http/lib/src/streamed_request.dart index d10386e263..bb7e3ac998 100644 --- a/pkgs/http/lib/src/streamed_request.dart +++ b/pkgs/http/lib/src/streamed_request.dart @@ -42,7 +42,7 @@ class StreamedRequest extends BaseRequest { final StreamController> _controller; /// Creates a new streaming request. - StreamedRequest(super.method, super.url) + StreamedRequest(super.method, super.url, {super.headers}) : _controller = StreamController>(sync: true); /// Freezes all mutable fields and returns a single-subscription [ByteStream] diff --git a/pkgs/http/test/http_retry_test.dart b/pkgs/http/test/http_retry_test.dart index da51154c4a..fa508a717c 100644 --- a/pkgs/http/test/http_retry_test.dart +++ b/pkgs/http/test/http_retry_test.dart @@ -53,13 +53,13 @@ void main() { MockClient(expectAsync1((request) async { count++; return Response('', 503, - headers: {'retry': count < 2 ? 'true' : 'false'}); + headers: Headers({'retry': count < 2 ? 'true' : 'false'})); }, count: 2)), - when: (response) => response.headers['retry'] == 'true', + when: (response) => response.headers.get('retry') == 'true', delay: (_) => Duration.zero); final response = await client.get(Uri.http('example.org', '')); - expect(response.headers, containsPair('retry', 'false')); + expect(response.headers.get('retry'), equals('false')); expect(response.statusCode, equals(503)); }); @@ -204,7 +204,9 @@ void main() { MockClient(expectAsync1((request) async { expect(request.contentLength, equals(5)); expect(request.followRedirects, isFalse); - expect(request.headers, containsPair('foo', 'bar')); + // // expect(request.headers, containsPair('foo', 'bar')); + expect(request.headers.keys(), contains('foo')); + expect(request.headers.values(), contains('bar')); expect(request.maxRedirects, equals(12)); expect(request.method, equals('POST')); expect(request.persistentConnection, isFalse); @@ -217,7 +219,8 @@ void main() { final request = Request('POST', Uri.http('example.org', '')) ..body = 'hello' ..followRedirects = false - ..headers['foo'] = 'bar' + // ..headers['foo'] = 'bar' + ..headers.set('foo', 'bar') ..maxRedirects = 12 ..persistentConnection = false; @@ -228,7 +231,7 @@ void main() { test('async when, whenError and onRetry', () async { final client = RetryClient( MockClient(expectAsync1( - (request) async => request.headers['Authorization'] != null + (request) async => request.headers.get('Authorization') != null ? Response('', 200) : Response('', 401), count: 2)), @@ -245,7 +248,8 @@ void main() { onRetry: (request, response, retryCount) async { expect(response?.statusCode, equals(401)); await Future.delayed(const Duration(milliseconds: 500)); - request.headers['Authorization'] = 'Bearer TOKEN'; + // request.headers['Authorization'] = 'Bearer TOKEN'; + request.headers.set('Authorization', 'Bearer TOKEN'); }, ); diff --git a/pkgs/http/test/io/client_test.dart b/pkgs/http/test/io/client_test.dart index fd426a8b3f..9e861164a6 100644 --- a/pkgs/http/test/io/client_test.dart +++ b/pkgs/http/test/io/client_test.dart @@ -38,9 +38,12 @@ void main() { test('#send a StreamedRequest', () async { var client = http.Client(); var request = http.StreamedRequest('POST', serverUrl) - ..headers[HttpHeaders.contentTypeHeader] = - 'application/json; charset=utf-8' - ..headers[HttpHeaders.userAgentHeader] = 'Dart'; + // ..headers[HttpHeaders.contentTypeHeader] = + // 'application/json; charset=utf-8' + // ..headers[HttpHeaders.userAgentHeader] = 'Dart'; + ..headers + .set(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8') + ..headers.set(HttpHeaders.userAgentHeader, 'Dart'); var responseFuture = client.send(request); request.sink.add('{"hello": "world"}'.codeUnits); @@ -50,7 +53,7 @@ void main() { expect(response.request, equals(request)); expect(response.statusCode, equals(200)); - expect(response.headers['single'], equals('value')); + expect(response.headers.get('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. @@ -76,9 +79,12 @@ void main() { var ioClient = HttpClient(); var client = http_io.IOClient(ioClient); var request = http.StreamedRequest('POST', serverUrl) - ..headers[HttpHeaders.contentTypeHeader] = - 'application/json; charset=utf-8' - ..headers[HttpHeaders.userAgentHeader] = 'Dart'; + // ..headers[HttpHeaders.contentTypeHeader] = + // 'application/json; charset=utf-8' + ..headers + .set(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8') + // ..headers[HttpHeaders.userAgentHeader] = 'Dart'; + ..headers.set(HttpHeaders.userAgentHeader, 'Dart'); var responseFuture = client.send(request); request.sink.add('{"hello": "world"}'.codeUnits); @@ -88,7 +94,7 @@ void main() { expect(response.request, equals(request)); expect(response.statusCode, equals(200)); - expect(response.headers['single'], equals('value')); + expect(response.headers.get('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. @@ -114,8 +120,8 @@ void main() { var client = http.Client(); var url = Uri.http('http.invalid', ''); var request = http.StreamedRequest('POST', url); - request.headers[HttpHeaders.contentTypeHeader] = - 'application/json; charset=utf-8'; + request.headers + .set(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8'); expect( client.send(request), diff --git a/pkgs/http/test/io/request_test.dart b/pkgs/http/test/io/request_test.dart index ac6b44c3fd..fd23ff11ca 100644 --- a/pkgs/http/test/io/request_test.dart +++ b/pkgs/http/test/io/request_test.dart @@ -19,7 +19,8 @@ void main() { test('send happy case', () async { final request = http.Request('GET', serverUrl) ..body = 'hello' - ..headers['User-Agent'] = 'Dart'; + // ..headers['User-Agent'] = 'Dart'; + ..headers.set('User-Agent', 'Dart'); final response = await request.send(); diff --git a/pkgs/http/test/mock_client_test.dart b/pkgs/http/test/mock_client_test.dart index 625285cb33..c9dc0ea5fc 100644 --- a/pkgs/http/test/mock_client_test.dart +++ b/pkgs/http/test/mock_client_test.dart @@ -5,6 +5,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:http/src/headers.dart'; import 'package:http/src/request.dart'; import 'package:http/testing.dart'; import 'package:test/test.dart'; @@ -15,7 +16,8 @@ void main() { test('handles a request', () async { var client = MockClient((request) async => http.Response( json.encode(request.bodyFields), 200, - request: request, headers: {'content-type': 'application/json'})); + request: request, + headers: Headers({'content-type': 'application/json'}))); var response = await client.post(Uri.http('example.com', '/foo'), body: {'field1': 'value1', 'field2': 'value2'}); @@ -52,7 +54,7 @@ void main() { [137, 80, 78, 71, 13, 10, 26, 10] // PNG header ); expect(response.request, null); - expect(response.headers, containsPair('content-type', 'image/png')); + expect(response.headers.get('content-type'), contains('image/png')); }); test('pngResponse with request', () { @@ -63,6 +65,6 @@ void main() { [137, 80, 78, 71, 13, 10, 26, 10] // PNG header ); expect(response.request, request); - expect(response.headers, containsPair('content-type', 'image/png')); + expect(response.headers.get('content-type'), contains('image/png')); }); } diff --git a/pkgs/http/test/request_test.dart b/pkgs/http/test/request_test.dart index 59cb0988c5..4950e90f8a 100644 --- a/pkgs/http/test/request_test.dart +++ b/pkgs/http/test/request_test.dart @@ -43,8 +43,8 @@ void main() { }); test('is based on the content-type charset if it exists', () { - var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'text/plain; charset=iso-8859-1'; + var request = http.Request('POST', dummyUrl, + headers: {'Content-Type': 'text/plain; charset=iso-8859-1'}); expect(request.encoding.name, equals(latin1.name)); }); @@ -52,17 +52,17 @@ void main() { () { var request = http.Request('POST', dummyUrl) ..encoding = latin1 - ..headers['Content-Type'] = 'text/plain; charset=utf-8'; + ..headers.set('Content-Type', 'text/plain; charset=utf-8'); expect(request.encoding.name, equals(utf8.name)); - request.headers.remove('Content-Type'); + request.headers.delete('Content-Type'); expect(request.encoding.name, equals(latin1.name)); }); test('throws an error if the content-type charset is unknown', () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'text/plain; charset=not-a-real-charset'; + request.headers + .set('Content-Type', 'text/plain; charset=not-a-real-charset'); expect(() => request.encoding, throwsFormatException); }); }); @@ -125,19 +125,21 @@ void main() { test("can't be read with the wrong content-type", () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'text/plain'; + request.headers.set('Content-Type', 'text/plain'); expect(() => request.bodyFields, throwsStateError); }); test("can't be set with the wrong content-type", () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'text/plain'; + // request.headers['Content-Type'] = 'text/plain'; + request.headers.set('Content-Type', 'text/plain'); expect(() => request.bodyFields = {}, throwsStateError); }); test('defaults to empty', () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + // request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + request.headers.set('Content-Type', 'application/x-www-form-urlencoded'); expect(request.bodyFields, isEmpty); }); @@ -149,7 +151,8 @@ void main() { test('changes when body changes', () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + // request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + request.headers.set('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'})); @@ -157,7 +160,8 @@ void main() { test('is encoded according to the given encoding', () { var request = http.Request('POST', dummyUrl) - ..headers['Content-Type'] = 'application/x-www-form-urlencoded' + // ..headers['Content-Type'] = 'application/x-www-form-urlencoded' + ..headers.set('Content-Type', 'application/x-www-form-urlencoded') ..encoding = latin1 ..bodyFields = {'föø': 'bãr'}; expect(request.body, equals('f%F6%F8=b%E3r')); @@ -165,7 +169,8 @@ void main() { test('is decoded according to the given encoding', () { var request = http.Request('POST', dummyUrl) - ..headers['Content-Type'] = 'application/x-www-form-urlencoded' + // ..headers['Content-Type'] = 'application/x-www-form-urlencoded' + ..headers.set('Content-Type', 'application/x-www-form-urlencoded') ..encoding = latin1 ..body = 'f%F6%F8=b%E3r'; expect(request.bodyFields, equals({'föø': 'bãr'})); @@ -175,18 +180,19 @@ void main() { group('content-type header', () { test('defaults to empty', () { var request = http.Request('POST', dummyUrl); - expect(request.headers['Content-Type'], isNull); + expect(request.headers.get('Content-Type'), isNull); }); test('defaults to empty if only encoding is set', () { var request = http.Request('POST', dummyUrl)..encoding = latin1; - expect(request.headers['Content-Type'], isNull); + // expect(request.headers['Content-Type'], isNull); + expect(request.headers.get('Content-Type'), isNull); }); test('name is case insensitive', () { var request = http.Request('POST', dummyUrl); - request.headers['CoNtEnT-tYpE'] = 'application/json'; - expect(request.headers, containsPair('content-type', 'application/json')); + request.headers.set('CoNtEnT-tYpE', 'application/json'); + expect(request.headers.get('content-type'), equals('application/json')); }); test( @@ -194,7 +200,8 @@ void main() { 'bodyFields is set', () { var request = http.Request('POST', dummyUrl) ..bodyFields = {'hello': 'world'}; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('application/x-www-form-urlencoded; charset=utf-8')); }); @@ -204,7 +211,8 @@ void main() { var request = http.Request('POST', dummyUrl) ..encoding = latin1 ..bodyFields = {'hello': 'world'}; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('application/x-www-form-urlencoded; charset=iso-8859-1')); }); @@ -214,48 +222,60 @@ void main() { var request = http.Request('POST', dummyUrl) ..encoding = latin1 ..body = 'hello, world'; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('text/plain; charset=iso-8859-1')); }); test('is modified to include utf-8 if body is set', () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/json'; + // request.headers['Content-Type'] = 'application/json'; + request.headers.set('Content-Type', 'application/json'); request.body = '{"hello": "world"}'; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('application/json; charset=utf-8')); }); test('is modified to include the given encoding if encoding is set', () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/json'; + // request.headers['Content-Type'] = 'application/json'; + request.headers.set('Content-Type', 'application/json'); request.encoding = latin1; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('application/json; charset=iso-8859-1')); }); test('has its charset overridden by an explicit encoding', () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/json; charset=utf-8'; + // request.headers['Content-Type'] = 'application/json; charset=utf-8'; + request.headers.set('Content-Type', 'application/json; charset=utf-8'); request.encoding = latin1; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('application/json; charset=iso-8859-1')); }); test("doesn't have its charset overridden by setting bodyFields", () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = - 'application/x-www-form-urlencoded; charset=iso-8859-1'; + + request.headers.set('Content-Type', + 'application/x-www-form-urlencoded; charset=iso-8859-1'); request.bodyFields = {'hello': 'world'}; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('application/x-www-form-urlencoded; charset=iso-8859-1')); }); test("doesn't have its charset overridden by setting body", () { var request = http.Request('POST', dummyUrl); - request.headers['Content-Type'] = 'application/json; charset=iso-8859-1'; + // request.headers['Content-Type'] = 'application/json; charset=iso-8859-1'; + request.headers + .set('Content-Type', 'application/json; charset=iso-8859-1'); request.body = '{"hello": "world"}'; - expect(request.headers['Content-Type'], + // expect(request.headers['Content-Type'], + expect(request.headers.get('Content-Type'), equals('application/json; charset=iso-8859-1')); }); }); diff --git a/pkgs/http/test/response_test.dart b/pkgs/http/test/response_test.dart index 38061c1ef4..bd80064203 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:http/http.dart' as http; +import 'package:http/src/headers.dart'; import 'package:test/test.dart'; void main() { @@ -24,7 +25,7 @@ void main() { test('respects the inferred encoding', () { var response = http.Response('föøbãr', 200, - headers: {'content-type': 'text/plain; charset=iso-8859-1'}); + headers: Headers({'content-type': 'text/plain; charset=iso-8859-1'})); expect(response.bodyBytes, equals([102, 246, 248, 98, 227, 114])); }); }); diff --git a/pkgs/http/test/utils.dart b/pkgs/http/test/utils.dart index d4c319f73f..38759d276e 100644 --- a/pkgs/http/test/utils.dart +++ b/pkgs/http/test/utils.dart @@ -91,7 +91,7 @@ class _BodyMatches extends Matcher { Future _checks(http.MultipartRequest item) async { var bodyBytes = await item.finalize().toBytes(); var body = utf8.decode(bodyBytes); - var contentType = MediaType.parse(item.headers['content-type']!); + var contentType = MediaType.parse(item.headers.get('content-type')!); var boundary = contentType.parameters['boundary']!; var expected = cleanUpLiteral(_pattern) .replaceAll('\n', '\r\n')