Skip to content
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

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

Merged
merged 12 commits into from
Jan 12, 2024
Prev Previous commit
Next Next commit
Add Map<String, List<String>> get headersFieldValueList to `BaseReq…
…uest`
  • Loading branch information
brianquinlan committed Jan 12, 2024
commit 7f535d4150c84ae78b1e30f273e106ff48afdf52
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, HeadersWithFieldLists;
export 'src/byte_stream.dart';
export 'src/client.dart' hide zoneClient;
export 'src/exception.dart';
Expand Down
70 changes: 69 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
/// [HeadersWithFieldLists.headersFieldValueList].
///
/// 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,69 @@ 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 headers.
var _headerSplitter = RegExp(r'[ \t]*,[ \t]*');

// Splits comma-seperated "Set-Cookie" headers.
//
// 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 represented with:
// "lang=en; Path=/foo/,SID=x23"
//
// The idea behind this RegExp 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 HeadersWithFieldLists 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.headersFieldValueList['set-cookie'] ?? <String>[])
/// Cookie.fromSetCookieValue(value)
/// ];
Map<String, List<String>> get headersFieldValueList {
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;
}
}
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('.headersFieldValueList', () {
test('no headers', () async {
var response = http.Response('Hello, world!', 200);
expect(response.headersFieldValueList, const <String, List<String>>{});
});

test('one headers', () async {
var response =
http.Response('Hello, world!', 200, headers: {'fruit': 'apple'});
expect(response.headersFieldValueList, const {
'fruit': ['apple']
});
});

test('two headers', () async {
var response = http.Response('Hello, world!', 200,
headers: {'fruit': 'apple,banana'});
expect(response.headersFieldValueList, 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.headersFieldValueList, 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.headersFieldValueList, 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.headersFieldValueList, 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.headersFieldValueList, const {
'set-cookie': [
'id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/,,HE,=L=LO',
'sessionId=e8bb43229de9; Domain=foo.example.com'
]
});
});
});
}