diff --git a/pkgs/http/lib/src/base_response.dart b/pkgs/http/lib/src/base_response.dart index a09dcea4ed..ed95f6cdb2 100644 --- a/pkgs/http/lib/src/base_response.dart +++ b/pkgs/http/lib/src/base_response.dart @@ -26,6 +26,26 @@ abstract class BaseResponse { // TODO(nweiz): automatically parse cookies from headers + /// The HTTP headers returned by the server. + /// + /// The header names are converted to lowercase and stored with their + /// associated header values. + /// + /// If the server returns multiple headers with the same name then the header + /// values will be associated with a single key and seperated by commas and + /// possibly whitespace. For example: + /// ```dart + /// // HTTP/1.1 200 OK + /// // Fruit: Apple + /// // Fruit: Banana + /// // Fruit: Grape + /// final values = response.headers['fruit']!.split(RegExp(r'\s*,\s*')); + /// // values = ['Apple', 'Banana', 'Grape'] + /// ``` + /// + /// 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; diff --git a/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart b/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart index 012fa63943..4f920428ae 100644 --- a/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart +++ b/pkgs/http_client_conformance_tests/lib/src/response_headers_tests.dart @@ -30,12 +30,19 @@ void testResponseHeaders(Client client) async { expect(response.headers['foo'], 'bar'); }); + test('UPPERCASE header name', () async { + // RFC 2616 14.44 states that header field names are case-insensitive. + // http.Client canonicalizes field names into lower case. + httpServerChannel.sink.add('FOO: bar\r\n'); + + final response = await client.get(Uri.http(host, '')); + expect(response.headers['foo'], 'bar'); + }); + test('UPPERCASE header value', () async { httpServerChannel.sink.add('foo: BAR\r\n'); final response = await client.get(Uri.http(host, '')); - // RFC 2616 14.44 states that header field names are case-insensitive. - // http.Client canonicalizes field names into lower case. expect(response.headers['foo'], 'BAR'); }); @@ -43,11 +50,27 @@ void testResponseHeaders(Client client) async { httpServerChannel.sink.add('foo: \t BAR \t \r\n'); final response = await client.get(Uri.http(host, '')); - // RFC 2616 14.44 states that header field names are case-insensitive. - // http.Client canonicalizes field names into lower case. expect(response.headers['foo'], 'BAR'); }); + test('space in header value', () async { + httpServerChannel.sink.add('foo: BAR BAZ\r\n'); + + final response = await client.get(Uri.http(host, '')); + expect(response.headers['foo'], 'BAR BAZ'); + }); + + test('multiple spaces in header value', () async { + // RFC 2616 4.2 allows LWS between header values to be replace with a + // single space. + // See https://datatracker.ietf.org/doc/html/rfc2616#section-4.2 + httpServerChannel.sink.add('foo: BAR \t BAZ\r\n'); + + final response = await client.get(Uri.http(host, '')); + expect( + response.headers['foo'], matches(RegExp('BAR {0,2}[ \t] {0,3}BAZ'))); + }); + test('multiple headers', () async { httpServerChannel.sink .add('field1: value1\r\n' 'field2: value2\r\n' 'field3: value3\r\n'); @@ -130,5 +153,30 @@ void testResponseHeaders(Client client) async { client.get(Uri.http(host, '')), throwsA(isA())); }); }); + + group('folded headers', () { + // RFC2616 says that HTTP Headers can be split across multiple lines. + // See https://datatracker.ietf.org/doc/html/rfc2616#section-2.2 + test('leading space', () async { + httpServerChannel.sink.add('foo: BAR\r\n BAZ\r\n'); + + final response = await client.get(Uri.http(host, '')); + expect(response.headers['foo'], 'BAR BAZ'); + }, + skip: 'Enable after https://github.com/dart-lang/sdk/issues/53185 ' + 'is fixed'); + + test('extra whitespace', () async { + httpServerChannel.sink.add('foo: BAR \t \r\n \t BAZ \t \r\n'); + + final response = await client.get(Uri.http(host, '')); + // RFC 2616 4.2 allows LWS between header values to be replace with a + // single space. + expect( + response.headers['foo'], + allOf(matches(RegExp(r'BAR {0,3}[ \t]? {0,7}[ \t]? {0,3}BAZ')), + contains(' '))); + }); + }); }); }