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..19845be08d 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'; @@ -34,7 +35,7 @@ export 'src/streamed_response.dart'; /// 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 head(Uri url, {Map? headers}) => +Future head(Uri url, {Map>? headers}) => _withClient((client) => client.head(url, headers: headers)); /// Sends an HTTP GET request with the given headers to the given URL. @@ -44,7 +45,7 @@ Future head(Uri url, {Map? headers}) => /// the same server, you should use a single [Client] for all of those requests. /// /// For more fine-grained control over the request, use [Request] instead. -Future get(Uri url, {Map? headers}) => +Future get(Uri url, {Map>? headers}) => _withClient((client) => client.get(url, headers: headers)); /// Sends an HTTP POST request with the given headers and body to the given URL. @@ -66,7 +67,9 @@ Future get(Uri url, {Map? headers}) => /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. Future post(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _withClient((client) => client.post(url, headers: headers, body: body, encoding: encoding)); @@ -89,7 +92,9 @@ Future post(Uri url, /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. Future put(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _withClient((client) => client.put(url, headers: headers, body: body, encoding: encoding)); @@ -113,7 +118,9 @@ Future put(Uri url, /// For more fine-grained control over the request, use [Request] or /// [StreamedRequest] instead. Future patch(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _withClient((client) => client.patch(url, headers: headers, body: body, encoding: encoding)); @@ -125,7 +132,9 @@ Future patch(Uri url, /// /// For more fine-grained control over the request, use [Request] instead. Future delete(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _withClient((client) => client.delete(url, headers: headers, body: body, encoding: encoding)); @@ -141,7 +150,7 @@ Future delete(Uri url, /// /// For more fine-grained control over the request and response, use [Request] /// instead. -Future read(Uri url, {Map? headers}) => +Future read(Uri url, {Map>? headers}) => _withClient((client) => client.read(url, headers: headers)); /// Sends an HTTP GET request with the given headers to the given URL and @@ -157,7 +166,8 @@ Future read(Uri url, {Map? headers}) => /// /// For more fine-grained control over the request and response, use [Request] /// instead. -Future readBytes(Uri url, {Map? headers}) => +Future readBytes(Uri url, + {Map>? headers}) => _withClient((client) => client.readBytes(url, headers: headers)); Future _withClient(Future Function(Client) fn) async { diff --git a/pkgs/http/lib/retry.dart b/pkgs/http/lib/retry.dart index dedba9a9e7..a808e27d91 100644 --- a/pkgs/http/lib/retry.dart +++ b/pkgs/http/lib/retry.dart @@ -136,10 +136,16 @@ final class RetryClient extends BaseClient { final request = StreamedRequest(original.method, original.url) ..contentLength = original.contentLength ..followRedirects = original.followRedirects - ..headers.addAll(original.headers) ..maxRedirects = original.maxRedirects ..persistentConnection = original.persistentConnection; + for (final (name, value) in original.headers.entries()) { + request.headers.append(name, value); + } + for (final cookie in original.headers.getSetCookie()) { + request.headers.append('set-cookie', cookie); + } + body.listen(request.sink.add, onError: request.sink.addError, onDone: request.sink.close, diff --git a/pkgs/http/lib/src/base_client.dart b/pkgs/http/lib/src/base_client.dart index 48a7f92fe9..b3cd6623fe 100644 --- a/pkgs/http/lib/src/base_client.dart +++ b/pkgs/http/lib/src/base_client.dart @@ -9,6 +9,7 @@ import 'base_request.dart'; import 'byte_stream.dart'; import 'client.dart'; import 'exception.dart'; +import 'headers.dart'; import 'request.dart'; import 'response.dart'; import 'streamed_response.dart'; @@ -19,42 +20,51 @@ 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, {Map>? headers}) => _sendUnstreamed('HEAD', url, headers); @override - Future get(Uri url, {Map? headers}) => + Future get(Uri url, {Map>? headers}) => _sendUnstreamed('GET', url, headers); @override Future post(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _sendUnstreamed('POST', url, headers, body, encoding); @override Future put(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _sendUnstreamed('PUT', url, headers, body, encoding); @override Future patch(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _sendUnstreamed('PATCH', url, headers, body, encoding); @override Future delete(Uri url, - {Map? headers, Object? body, Encoding? encoding}) => + {Map>? headers, + Object? body, + Encoding? encoding}) => _sendUnstreamed('DELETE', url, headers, body, encoding); @override - Future read(Uri url, {Map? headers}) async { + Future read(Uri url, {Map>? 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, + {Map>? headers}) async { final response = await get(url, headers: headers); _checkResponseSuccess(url, response); return response.bodyBytes; @@ -72,11 +82,20 @@ abstract mixin class BaseClient implements Client { /// Sends a non-streaming [Request] and returns a non-streaming [Response]. Future _sendUnstreamed( - String method, Uri url, Map? headers, + String method, Uri url, Map>? headers, [Object? body, Encoding? encoding]) async { var request = Request(method, url); - if (headers != null) request.headers.addAll(headers); + if (headers != null) { + final newHeaders = Headers(headers); + for (final (name, value) in newHeaders.entries()) { + request.headers.append(name, value); + } + for (final cookie in newHeaders.getSetCookie()) { + request.headers.append('Set-Cookie', cookie); + } + } + 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..25aae9d86e 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 = Headers(); /// Whether [finalize] has been called. bool get finalized => _finalized; @@ -96,11 +95,7 @@ abstract class BaseRequest { return method; } - BaseRequest(String method, this.url) - : method = _validateMethod(method), - headers = LinkedHashMap( - equals: (key1, key2) => key1.toLowerCase() == key2.toLowerCase(), - hashCode: (key) => key.toLowerCase().hashCode); + BaseRequest(String method, this.url) : method = _validateMethod(method); /// 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..d853f6600f 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,7 +48,7 @@ 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; @@ -57,10 +58,11 @@ abstract class BaseResponse { BaseResponse(this.statusCode, {this.contentLength, this.request, - this.headers = const {}, + Headers? headers, this.isRedirect = false, this.persistentConnection = true, - this.reasonPhrase}) { + 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..14b2872f5f 100644 --- a/pkgs/http/lib/src/browser_client.dart +++ b/pkgs/http/lib/src/browser_client.dart @@ -3,14 +3,16 @@ // 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'; +import 'package:web/helpers.dart' hide Headers; import 'base_client.dart'; import 'base_request.dart'; import 'byte_stream.dart'; import 'exception.dart'; +import 'headers.dart'; import 'streamed_response.dart'; final _digitRegex = RegExp(r'^\d+$'); @@ -62,14 +64,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); + + // Sets all headers without set-cookie headers. + for (final (name, value) in request.headers.entries()) { + xhr.setRequestHeader(name, value); + } + + // Sets cookie headers. + 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 +128,12 @@ class BrowserClient extends BaseClient { } extension on XMLHttpRequest { - Map get responseHeaders { + Headers get responseHeaders { + final headers = Headers(); + final lines = const LineSplitter().convert(getAllResponseHeaders()); + // from Closure's goog.net.Xhrio.getResponseHeaders. - var headers = {}; - var headersString = getAllResponseHeaders(); - var headersList = headersString.split('\r\n'); - for (var header in headersList) { + for (var header in lines) { if (header.isEmpty) { continue; } @@ -132,14 +142,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; return headers; } } diff --git a/pkgs/http/lib/src/client.dart b/pkgs/http/lib/src/client.dart index 7429ca8824..db9456601e 100644 --- a/pkgs/http/lib/src/client.dart +++ b/pkgs/http/lib/src/client.dart @@ -44,12 +44,12 @@ abstract interface class Client { /// Sends an HTTP HEAD request with the given headers to the given URL. /// /// For more fine-grained control over the request, use [send] instead. - Future head(Uri url, {Map? headers}); + Future head(Uri url, {Map>? headers}); /// Sends an HTTP GET request with the given headers to the given URL. /// /// For more fine-grained control over the request, use [send] instead. - Future get(Uri url, {Map? headers}); + Future get(Uri url, {Map>? headers}); /// Sends an HTTP POST request with the given headers and body to the given /// URL. @@ -72,7 +72,9 @@ abstract interface class Client { /// /// For more fine-grained control over the request, use [send] instead. Future post(Uri url, - {Map? headers, Object? body, Encoding? encoding}); + {Map>? headers, + Object? body, + Encoding? encoding}); /// Sends an HTTP PUT request with the given headers and body to the given /// URL. @@ -93,7 +95,9 @@ abstract interface class Client { /// /// For more fine-grained control over the request, use [send] instead. Future put(Uri url, - {Map? headers, Object? body, Encoding? encoding}); + {Map>? headers, + Object? body, + Encoding? encoding}); /// Sends an HTTP PATCH request with the given headers and body to the given /// URL. @@ -114,13 +118,17 @@ abstract interface class Client { /// /// For more fine-grained control over the request, use [send] instead. Future patch(Uri url, - {Map? headers, Object? body, Encoding? encoding}); + {Map>? headers, + Object? body, + Encoding? encoding}); /// Sends an HTTP DELETE request with the given headers to the given URL. /// /// For more fine-grained control over the request, use [send] instead. Future delete(Uri url, - {Map? headers, Object? body, Encoding? encoding}); + {Map>? headers, + Object? body, + Encoding? encoding}); /// Sends an HTTP GET request with the given headers to the given URL and /// returns a Future that completes to the body of the response as a String. @@ -130,7 +138,7 @@ abstract interface class Client { /// /// For more fine-grained control over the request and response, use [send] or /// [get] instead. - Future read(Uri url, {Map? headers}); + Future read(Uri url, {Map>? headers}); /// Sends an HTTP GET request with the given headers to the given URL and /// returns a Future that completes to the body of the response as a list of @@ -141,7 +149,8 @@ abstract interface class Client { /// /// For more fine-grained control over the request and response, use [send] or /// [get] instead. - Future readBytes(Uri url, {Map? headers}); + Future readBytes(Uri url, + {Map>? headers}); /// Sends an HTTP request and asynchronously returns the response. Future send(BaseRequest request); diff --git a/pkgs/http/lib/src/headers.dart b/pkgs/http/lib/src/headers.dart new file mode 100644 index 0000000000..58594b89a7 --- /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)); + } +} diff --git a/pkgs/http/lib/src/io_client.dart b/pkgs/http/lib/src/io_client.dart index db66b028c4..2276a8ab48 100644 --- a/pkgs/http/lib/src/io_client.dart +++ b/pkgs/http/lib/src/io_client.dart @@ -8,6 +8,7 @@ import 'base_client.dart'; import 'base_request.dart'; import 'client.dart'; import 'exception.dart'; +import 'headers.dart'; import 'io_streamed_response.dart'; /// Create an [IOClient]. @@ -102,18 +103,27 @@ class IOClient extends BaseClient { ..maxRedirects = request.maxRedirects ..contentLength = (request.contentLength ?? -1) ..persistentConnection = request.persistentConnection; - request.headers.forEach((name, value) { - ioRequest.headers.set(name, value); + + // Sets headers with set-cookie headers. + request.headers.forEach((value, name, parent) { + ioRequest.headers.add(name, value); }); + // Sets cookie headers. + for (final cookie in request.headers.getSetCookie()) { + ioRequest.headers.add('set-cookie', cookie); + } + var response = await stream.pipe(ioRequest) as HttpClientResponse; - var headers = {}; - response.headers.forEach((key, values) { + final headers = Headers(); + response.headers.forEach((name, values) { // TODO: Remove trimRight() when // https://github.com/dart-lang/sdk/issues/53005 is resolved and the // package:http SDK constraint requires that version or later. - headers[key] = values.map((value) => value.trimRight()).join(','); + for (final value in values) { + headers.append(name, value.trimRight()); + } }); return IOStreamedResponse( diff --git a/pkgs/http/lib/src/mock_client.dart b/pkgs/http/lib/src/mock_client.dart index 52f108a577..22fbea3198 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'; @@ -42,10 +43,16 @@ class MockClient extends BaseClient { ..persistentConnection = baseRequest.persistentConnection ..followRedirects = baseRequest.followRedirects ..maxRedirects = baseRequest.maxRedirects - ..headers.addAll(baseRequest.headers) ..bodyBytes = bodyBytes ..finalize(); + for (final (name, value) in baseRequest.headers.entries()) { + request.headers.append(name, value); + } + for (final cookie in baseRequest.headers.getSetCookie()) { + request.headers.append('set-cookie', cookie); + } + final response = await fn(request); return StreamedResponse( ByteStream.fromBytes(response.bodyBytes), response.statusCode, @@ -79,10 +86,10 @@ class MockClient extends BaseClient { /// Return a response containing a PNG image. static Response pngResponse({BaseRequest? request}) { - final headers = { + final headers = Headers({ 'content-type': 'image/png', 'content-length': '${_pngImageData.length}' - }; + }); return Response.bytes(_pngImageData, 200, request: request, headers: headers, reasonPhrase: 'OK'); diff --git a/pkgs/http/lib/src/multipart_request.dart b/pkgs/http/lib/src/multipart_request.dart index 79525421fb..6e3591237b 100644 --- a/pkgs/http/lib/src/multipart_request.dart +++ b/pkgs/http/lib/src/multipart_request.dart @@ -87,7 +87,7 @@ 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..4015bf72d5 100644 --- a/pkgs/http/lib/src/request.dart +++ b/pkgs/http/lib/src/request.dart @@ -163,17 +163,18 @@ 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 the content type is null, remove the header. 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/test/http_retry_test.dart b/pkgs/http/test/http_retry_test.dart index da51154c4a..a7eff9d05f 100644 --- a/pkgs/http/test/http_retry_test.dart +++ b/pkgs/http/test/http_retry_test.dart @@ -53,9 +53,9 @@ 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', '')); @@ -217,7 +217,7 @@ void main() { final request = Request('POST', Uri.http('example.org', '')) ..body = 'hello' ..followRedirects = false - ..headers['foo'] = 'bar' + ..headers.append('foo', 'bar') ..maxRedirects = 12 ..persistentConnection = false; @@ -228,7 +228,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 +245,7 @@ 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.set('Authorization', 'Bearer TOKEN'); }, ); diff --git a/pkgs/http/test/io/client_test.dart b/pkgs/http/test/io/client_test.dart index fd426a8b3f..c8aac64aa2 100644 --- a/pkgs/http/test/io/client_test.dart +++ b/pkgs/http/test/io/client_test.dart @@ -38,9 +38,9 @@ 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 + .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 +50,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 +76,9 @@ 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 + .set(HttpHeaders.contentTypeHeader, 'application/json; charset=utf-8') + ..headers.set(HttpHeaders.userAgentHeader, 'Dart'); var responseFuture = client.send(request); request.sink.add('{"hello": "world"}'.codeUnits); @@ -88,7 +88,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 +114,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/http_test.dart b/pkgs/http/test/io/http_test.dart index 3f9aad815e..37ff7eebae 100644 --- a/pkgs/http/test/io/http_test.dart +++ b/pkgs/http/test/io/http_test.dart @@ -38,9 +38,9 @@ void main() { test('get', () async { var response = await http.get(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }); expect(response.statusCode, equals(200)); expect( @@ -65,10 +65,10 @@ void main() { test('post', () async { var response = await http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'Content-Type': 'text/plain', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'Content-Type': ['text/plain'], + 'User-Agent': ['Dart'] }); expect(response.statusCode, equals(200)); expect( @@ -90,9 +90,9 @@ void main() { test('post with string', () async { var response = await http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: 'request body'); expect(response.statusCode, equals(200)); @@ -115,9 +115,9 @@ void main() { test('post with bytes', () async { var response = await http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: [ 104, 101, @@ -144,9 +144,9 @@ void main() { test('post with fields', () async { var response = await http.post(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: { 'some-field': 'value', 'other-field': 'other value' @@ -180,10 +180,10 @@ void main() { test('put', () async { var response = await http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'Content-Type': 'text/plain', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'Content-Type': ['text/plain'], + 'User-Agent': ['Dart'] }); expect(response.statusCode, equals(200)); expect( @@ -205,9 +205,9 @@ void main() { test('put with string', () async { var response = await http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: 'request body'); expect(response.statusCode, equals(200)); @@ -230,9 +230,9 @@ void main() { test('put with bytes', () async { var response = await http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: [ 104, 101, @@ -259,9 +259,9 @@ void main() { test('put with fields', () async { var response = await http.put(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: { 'some-field': 'value', 'other-field': 'other value' @@ -295,10 +295,10 @@ void main() { test('patch', () async { var response = await http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'Content-Type': 'text/plain', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'Content-Type': ['text/plain'], + 'User-Agent': ['Dart'] }); expect(response.statusCode, equals(200)); expect( @@ -320,9 +320,9 @@ void main() { test('patch with string', () async { var response = await http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: 'request body'); expect(response.statusCode, equals(200)); @@ -345,9 +345,9 @@ void main() { test('patch with bytes', () async { var response = await http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: [ 104, 101, @@ -374,9 +374,9 @@ void main() { test('patch with fields', () async { var response = await http.patch(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }, body: { 'some-field': 'value', 'other-field': 'other value' @@ -403,9 +403,9 @@ void main() { test('delete', () async { var response = await http.delete(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }); expect(response.statusCode, equals(200)); expect( @@ -431,9 +431,9 @@ void main() { test('read', () async { var response = await http.read(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }); expect( response, @@ -461,9 +461,9 @@ void main() { test('readBytes', () async { var bytes = await http.readBytes(serverUrl, headers: { - 'X-Random-Header': 'Value', - 'X-Other-Header': 'Other Value', - 'User-Agent': 'Dart' + 'X-Random-Header': ['Value'], + 'X-Other-Header': ['Other Value'], + 'User-Agent': ['Dart'] }); expect( diff --git a/pkgs/http/test/io/request_test.dart b/pkgs/http/test/io/request_test.dart index ac6b44c3fd..d18f75a26e 100644 --- a/pkgs/http/test/io/request_test.dart +++ b/pkgs/http/test/io/request_test.dart @@ -19,7 +19,7 @@ void main() { test('send happy case', () async { final request = http.Request('GET', serverUrl) ..body = 'hello' - ..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..d261d772d8 100644 --- a/pkgs/http/test/mock_client_test.dart +++ b/pkgs/http/test/mock_client_test.dart @@ -15,7 +15,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: http.Headers({'content-type': 'application/json'}))); var response = await client.post(Uri.http('example.com', '/foo'), body: {'field1': 'value1', 'field2': 'value2'}); diff --git a/pkgs/http/test/request_test.dart b/pkgs/http/test/request_test.dart index 59cb0988c5..e49e45c27a 100644 --- a/pkgs/http/test/request_test.dart +++ b/pkgs/http/test/request_test.dart @@ -44,7 +44,7 @@ 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'; + request.headers.set('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,7 +125,7 @@ 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); }); @@ -341,3 +341,9 @@ void main() { }); }); } + +extension on http.Headers { + void operator []=(String name, String value) => set(name, value); + + String? operator [](String name) => get(name); +} diff --git a/pkgs/http/test/response_test.dart b/pkgs/http/test/response_test.dart index 38061c1ef4..d7c939f0f2 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart @@ -24,7 +24,8 @@ 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: + http.Headers({'content-type': 'text/plain; charset=iso-8859-1'})); expect(response.bodyBytes, equals([102, 246, 248, 98, 227, 114])); }); }); @@ -42,7 +43,8 @@ void main() { test('respects the inferred encoding', () { var response = http.Response.bytes([102, 246, 248, 98, 227, 114], 200, - headers: {'content-type': 'text/plain; charset=iso-8859-1'}); + headers: + http.Headers({'content-type': 'text/plain; charset=iso-8859-1'})); expect(response.body, equals('föøbãr')); }); }); 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')