Skip to content

Commit

Permalink
Add the ability to get response headers as a `Map<String, List<String…
Browse files Browse the repository at this point in the history
…>>` (#1114)
  • Loading branch information
brianquinlan committed Jan 12, 2024
1 parent 5c75da6 commit ebd86b9
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 4 deletions.
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'
]
});
});
});
}

0 comments on commit ebd86b9

Please sign in to comment.