diff --git a/pkgs/http/CHANGELOG.md b/pkgs/http/CHANGELOG.md index 7b8dec4fdd..90eb36aa6b 100644 --- a/pkgs/http/CHANGELOG.md +++ b/pkgs/http/CHANGELOG.md @@ -1,6 +1,8 @@ -## 1.1.3-wip +## 1.2.0-wip * Add `MockClient.pngResponse`, which makes it easier to fake image responses. +* Add the ability to get headers as a `Map` to + `BaseResponse`. ## 1.1.2 diff --git a/pkgs/http/lib/http.dart b/pkgs/http/lib/http.dart index 62004240c7..bd039c8519 100644 --- a/pkgs/http/lib/http.dart +++ b/pkgs/http/lib/http.dart @@ -16,7 +16,7 @@ import 'src/streamed_request.dart'; export 'src/base_client.dart'; export 'src/base_request.dart'; -export 'src/base_response.dart'; +export 'src/base_response.dart' show BaseResponse, HeadersWithSplitValues; export 'src/byte_stream.dart'; export 'src/client.dart' hide zoneClient; export 'src/exception.dart'; diff --git a/pkgs/http/lib/src/base_response.dart b/pkgs/http/lib/src/base_response.dart index ed95f6cdb2..e1796e1b36 100644 --- a/pkgs/http/lib/src/base_response.dart +++ b/pkgs/http/lib/src/base_response.dart @@ -43,10 +43,12 @@ abstract class BaseResponse { /// // values = ['Apple', 'Banana', 'Grape'] /// ``` /// + /// To retrieve the header values as a `List`, use + /// [HeadersWithSplitValues.headersSplitValues]. + /// /// If a header value contains whitespace then that whitespace may be replaced /// 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 bool isRedirect; @@ -68,3 +70,68 @@ abstract class BaseResponse { } } } + +/// "token" as defined in RFC 2616, 2.2 +/// See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 +const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`" + 'abcdefghijklmnopqrstuvwxyz|~'; + +/// Splits comma-seperated header values. +var _headerSplitter = RegExp(r'[ \t]*,[ \t]*'); + +/// Splits comma-seperated "Set-Cookie" header values. +/// +/// Set-Cookie strings can contain commas. In particular, the following +/// productions defined in RFC-6265, section 4.1.1: +/// - e.g. "Expires=Sun, 06 Nov 1994 08:49:37 GMT" +/// - e.g. "Path=somepath," +/// - e.g. "AnyString,Really," +/// +/// Some values are ambiguous e.g. +/// "Set-Cookie: lang=en; Path=/foo/" +/// "Set-Cookie: SID=x23" +/// and: +/// "Set-Cookie: lang=en; Path=/foo/,SID=x23" +/// would both be result in `response.headers` => "lang=en; Path=/foo/,SID=x23" +/// +/// The idea behind this regex is that ",=" is more likely to +/// start a new then be part of or . +/// +/// See https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1 +var _setCookieSplitter = RegExp(r'[ \t]*,[ \t]*(?=[' + _tokenChars + r']+=)'); + +extension HeadersWithSplitValues on BaseResponse { + /// The HTTP headers returned by the server. + /// + /// The header names are converted to lowercase and stored with their + /// associated header values. + /// + /// Cookies can be parsed using the dart:io `Cookie` class: + /// + /// ```dart + /// import "dart:io"; + /// import "package:http/http.dart"; + /// + /// void main() async { + /// final response = await Client().get(Uri.https('example.com', '/')); + /// final cookies = [ + /// for (var value i + /// in response.headersSplitValues['set-cookie'] ?? []) + /// Cookie.fromSetCookieValue(value) + /// ]; + Map> get headersSplitValues { + var headersWithFieldLists = >{}; + headers.forEach((key, value) { + if (!value.contains(',')) { + headersWithFieldLists[key] = [value]; + } else { + if (key == 'set-cookie') { + headersWithFieldLists[key] = value.split(_setCookieSplitter); + } else { + headersWithFieldLists[key] = value.split(_headerSplitter); + } + } + }); + return headersWithFieldLists; + } +} diff --git a/pkgs/http/pubspec.yaml b/pkgs/http/pubspec.yaml index 1645f96048..31746fcb2d 100644 --- a/pkgs/http/pubspec.yaml +++ b/pkgs/http/pubspec.yaml @@ -1,5 +1,5 @@ name: http -version: 1.1.3-wip +version: 1.2.0-wip description: A composable, multi-platform, Future-based API for HTTP requests. repository: https://github.com/dart-lang/http/tree/master/pkgs/http diff --git a/pkgs/http/test/response_test.dart b/pkgs/http/test/response_test.dart index 38061c1ef4..1bd9fd8e38 100644 --- a/pkgs/http/test/response_test.dart +++ b/pkgs/http/test/response_test.dart @@ -70,4 +70,73 @@ void main() { expect(response.bodyBytes, equals([104, 101, 108, 108, 111])); }); }); + + group('.headersSplitValues', () { + test('no headers', () async { + var response = http.Response('Hello, world!', 200); + expect(response.headersSplitValues, const >{}); + }); + + test('one header', () async { + var response = + http.Response('Hello, world!', 200, headers: {'fruit': 'apple'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple'] + }); + }); + + test('two headers', () async { + var response = http.Response('Hello, world!', 200, + headers: {'fruit': 'apple,banana'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple', 'banana'] + }); + }); + + test('two headers with lots of spaces', () async { + var response = http.Response('Hello, world!', 200, + headers: {'fruit': 'apple \t , \tbanana'}); + expect(response.headersSplitValues, const { + 'fruit': ['apple', 'banana'] + }); + }); + + test('one set-cookie', () async { + var response = http.Response('Hello, world!', 200, headers: { + 'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT' + }); + expect(response.headersSplitValues, const { + 'set-cookie': ['id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT'] + }); + }); + + test('two set-cookie, with comma in expires', () async { + var response = http.Response('Hello, world!', 200, headers: { + // ignore: missing_whitespace_between_adjacent_strings + 'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }); + expect(response.headersSplitValues, const { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); + }); + + test('two set-cookie, with lots of commas', () async { + var response = http.Response('Hello, world!', 200, headers: { + 'set-cookie': + // ignore: missing_whitespace_between_adjacent_strings + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,' + 'sessionId=e8bb43229de9; Domain=foo.example.com' + }); + expect(response.headersSplitValues, const { + 'set-cookie': [ + 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO', + 'sessionId=e8bb43229de9; Domain=foo.example.com' + ] + }); + }); + }); }