Skip to content

Add the ability to get response headers as a Map<String, List<String>> #1114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 12, 2024
4 changes: 3 additions & 1 deletion pkgs/http/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<String, List<String>` to
`BaseResponse`.

## 1.1.2

Expand Down
2 changes: 1 addition & 1 deletion pkgs/http/lib/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
69 changes: 68 additions & 1 deletion pkgs/http/lib/src/base_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ abstract class BaseResponse {
/// // values = ['Apple', 'Banana', 'Grape']
/// ```
///
/// To retrieve the header values as a `List<String>`, 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<String, String> headers;

final bool isRedirect;
Expand All @@ -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:
/// - <sane-cookie-date> e.g. "Expires=Sun, 06 Nov 1994 08:49:37 GMT"
/// - <path-value> e.g. "Path=somepath,"
/// - <extension-av> 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 ",<valid token>=" is more likely to
/// start a new <cookie-pair> then be part of <path-value> or <extension-av>.
///
/// 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'] ?? <String>[])
/// Cookie.fromSetCookieValue(value)
/// ];
Map<String, List<String>> get headersSplitValues {
var headersWithFieldLists = <String, List<String>>{};
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;
}
}
2 changes: 1 addition & 1 deletion pkgs/http/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand Down
69 changes: 69 additions & 0 deletions pkgs/http/test/response_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 <String, List<String>>{});
});

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'
]
});
});
});
}