Skip to content

Commit ebd86b9

Browse files
authored
Add the ability to get response headers as a Map<String, List<String>> (#1114)
1 parent 5c75da6 commit ebd86b9

File tree

5 files changed

+142
-4
lines changed

5 files changed

+142
-4
lines changed

pkgs/http/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
## 1.1.3-wip
1+
## 1.2.0-wip
22

33
* Add `MockClient.pngResponse`, which makes it easier to fake image responses.
4+
* Add the ability to get headers as a `Map<String, List<String>` to
5+
`BaseResponse`.
46

57
## 1.1.2
68

pkgs/http/lib/http.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import 'src/streamed_request.dart';
1616

1717
export 'src/base_client.dart';
1818
export 'src/base_request.dart';
19-
export 'src/base_response.dart';
19+
export 'src/base_response.dart' show BaseResponse, HeadersWithSplitValues;
2020
export 'src/byte_stream.dart';
2121
export 'src/client.dart' hide zoneClient;
2222
export 'src/exception.dart';

pkgs/http/lib/src/base_response.dart

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ abstract class BaseResponse {
4343
/// // values = ['Apple', 'Banana', 'Grape']
4444
/// ```
4545
///
46+
/// To retrieve the header values as a `List<String>`, use
47+
/// [HeadersWithSplitValues.headersSplitValues].
48+
///
4649
/// If a header value contains whitespace then that whitespace may be replaced
4750
/// by a single space. Leading and trailing whitespace in header values are
4851
/// always removed.
49-
// TODO(nweiz): make this a HttpHeaders object.
5052
final Map<String, String> headers;
5153

5254
final bool isRedirect;
@@ -68,3 +70,68 @@ abstract class BaseResponse {
6870
}
6971
}
7072
}
73+
74+
/// "token" as defined in RFC 2616, 2.2
75+
/// See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
76+
const _tokenChars = r"!#$%&'*+\-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`"
77+
'abcdefghijklmnopqrstuvwxyz|~';
78+
79+
/// Splits comma-seperated header values.
80+
var _headerSplitter = RegExp(r'[ \t]*,[ \t]*');
81+
82+
/// Splits comma-seperated "Set-Cookie" header values.
83+
///
84+
/// Set-Cookie strings can contain commas. In particular, the following
85+
/// productions defined in RFC-6265, section 4.1.1:
86+
/// - <sane-cookie-date> e.g. "Expires=Sun, 06 Nov 1994 08:49:37 GMT"
87+
/// - <path-value> e.g. "Path=somepath,"
88+
/// - <extension-av> e.g. "AnyString,Really,"
89+
///
90+
/// Some values are ambiguous e.g.
91+
/// "Set-Cookie: lang=en; Path=/foo/"
92+
/// "Set-Cookie: SID=x23"
93+
/// and:
94+
/// "Set-Cookie: lang=en; Path=/foo/,SID=x23"
95+
/// would both be result in `response.headers` => "lang=en; Path=/foo/,SID=x23"
96+
///
97+
/// The idea behind this regex is that ",<valid token>=" is more likely to
98+
/// start a new <cookie-pair> then be part of <path-value> or <extension-av>.
99+
///
100+
/// See https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1
101+
var _setCookieSplitter = RegExp(r'[ \t]*,[ \t]*(?=[' + _tokenChars + r']+=)');
102+
103+
extension HeadersWithSplitValues on BaseResponse {
104+
/// The HTTP headers returned by the server.
105+
///
106+
/// The header names are converted to lowercase and stored with their
107+
/// associated header values.
108+
///
109+
/// Cookies can be parsed using the dart:io `Cookie` class:
110+
///
111+
/// ```dart
112+
/// import "dart:io";
113+
/// import "package:http/http.dart";
114+
///
115+
/// void main() async {
116+
/// final response = await Client().get(Uri.https('example.com', '/'));
117+
/// final cookies = [
118+
/// for (var value i
119+
/// in response.headersSplitValues['set-cookie'] ?? <String>[])
120+
/// Cookie.fromSetCookieValue(value)
121+
/// ];
122+
Map<String, List<String>> get headersSplitValues {
123+
var headersWithFieldLists = <String, List<String>>{};
124+
headers.forEach((key, value) {
125+
if (!value.contains(',')) {
126+
headersWithFieldLists[key] = [value];
127+
} else {
128+
if (key == 'set-cookie') {
129+
headersWithFieldLists[key] = value.split(_setCookieSplitter);
130+
} else {
131+
headersWithFieldLists[key] = value.split(_headerSplitter);
132+
}
133+
}
134+
});
135+
return headersWithFieldLists;
136+
}
137+
}

pkgs/http/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: http
2-
version: 1.1.3-wip
2+
version: 1.2.0-wip
33
description: A composable, multi-platform, Future-based API for HTTP requests.
44
repository: https://github.com/dart-lang/http/tree/master/pkgs/http
55

pkgs/http/test/response_test.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,73 @@ void main() {
7070
expect(response.bodyBytes, equals([104, 101, 108, 108, 111]));
7171
});
7272
});
73+
74+
group('.headersSplitValues', () {
75+
test('no headers', () async {
76+
var response = http.Response('Hello, world!', 200);
77+
expect(response.headersSplitValues, const <String, List<String>>{});
78+
});
79+
80+
test('one header', () async {
81+
var response =
82+
http.Response('Hello, world!', 200, headers: {'fruit': 'apple'});
83+
expect(response.headersSplitValues, const {
84+
'fruit': ['apple']
85+
});
86+
});
87+
88+
test('two headers', () async {
89+
var response = http.Response('Hello, world!', 200,
90+
headers: {'fruit': 'apple,banana'});
91+
expect(response.headersSplitValues, const {
92+
'fruit': ['apple', 'banana']
93+
});
94+
});
95+
96+
test('two headers with lots of spaces', () async {
97+
var response = http.Response('Hello, world!', 200,
98+
headers: {'fruit': 'apple \t , \tbanana'});
99+
expect(response.headersSplitValues, const {
100+
'fruit': ['apple', 'banana']
101+
});
102+
});
103+
104+
test('one set-cookie', () async {
105+
var response = http.Response('Hello, world!', 200, headers: {
106+
'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT'
107+
});
108+
expect(response.headersSplitValues, const {
109+
'set-cookie': ['id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT']
110+
});
111+
});
112+
113+
test('two set-cookie, with comma in expires', () async {
114+
var response = http.Response('Hello, world!', 200, headers: {
115+
// ignore: missing_whitespace_between_adjacent_strings
116+
'set-cookie': 'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT,'
117+
'sessionId=e8bb43229de9; Domain=foo.example.com'
118+
});
119+
expect(response.headersSplitValues, const {
120+
'set-cookie': [
121+
'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT',
122+
'sessionId=e8bb43229de9; Domain=foo.example.com'
123+
]
124+
});
125+
});
126+
127+
test('two set-cookie, with lots of commas', () async {
128+
var response = http.Response('Hello, world!', 200, headers: {
129+
'set-cookie':
130+
// ignore: missing_whitespace_between_adjacent_strings
131+
'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO,'
132+
'sessionId=e8bb43229de9; Domain=foo.example.com'
133+
});
134+
expect(response.headersSplitValues, const {
135+
'set-cookie': [
136+
'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO',
137+
'sessionId=e8bb43229de9; Domain=foo.example.com'
138+
]
139+
});
140+
});
141+
});
73142
}

0 commit comments

Comments
 (0)