diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0699fd3ca2..f89f97a1ca38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ used (see Issue [39627][]). #### `dart:io` +* **Breaking change** [#33501](https://github.com/dart-lang/sdk/issues/33501): +An named parameter is added to `add` and `set` for class `HttpHeaders`. +The signature of has been changed from `void add(String name, Object value)` to +`void add(String name, Object value, {bool preserveHeaderCase: false})`. Same change +is applied to `set`. `preserveHeaderCase` will preserve the case of `name` +instead of converting them to lowercase. `HttpHeader.forEach()` provides the current +case of each header. + ### Dart VM ### Tools diff --git a/DEPS b/DEPS index 505808a3bdc6..c5d3f19a45e4 100644 --- a/DEPS +++ b/DEPS @@ -92,7 +92,7 @@ vars = { "glob_tag": "1.1.7", "html_tag" : "0.14.0+1", "http_io_rev": "2fa188caf7937e313026557713f7feffedd4978b", - "http_multi_server_tag" : "2.0.5", + "http_multi_server_tag" : "98c2789fc1c47544afc8efbec5fa9c1499ecf5e2", "http_parser_tag" : "3.1.3", "http_retry_tag": "0.1.1", "http_tag" : "0.12.0+2", diff --git a/sdk/lib/_http/http.dart b/sdk/lib/_http/http.dart index 855234bf46fe..57c424b74066 100644 --- a/sdk/lib/_http/http.dart +++ b/sdk/lib/_http/http.dart @@ -672,21 +672,36 @@ abstract class HttpHeaders { String value(String name); /** - * Adds a header value. The header named [name] will have the value - * [value] added to its list of values. Some headers are single - * valued, and for these adding a value will replace the previous - * value. If the value is of type DateTime a HTTP date format will be - * applied. If the value is a [:List:] each element of the list will - * be added separately. For all other types the default [:toString:] - * method will be used. + * Adds a header value. + * + * The header named [name] will have a string value derived from [value] + * added to its list of values. + * + * Some headers are single valued, and for these, adding a value will + * replace a previous value. If the [value] is a [DateTime], an + * HTTP date format will be applied. If the value is an [Iterable], + * each element will be added separately. For all other + * types the default [Object.toString] method will be used. + * + * Header names are converted to lower-case unless + * [preserveHeaderCase] is set to true. If two header names are + * the same when converted to lower-case, they are considered to be + * the same header, with one set of values. + * + * The current case of the a header name is that of the name used by + * the last [set] or [add] call for that header. */ - void add(String name, Object value); + void add(String name, Object value, + {@Since("2.8") bool preserveHeaderCase: false}); /** - * Sets a header. The header named [name] will have all its values - * cleared before the value [value] is added as its value. + * Sets the header [name] to [value]. + * + * Removes all existing values for the header named [name] and + * then [add]s [value] to it. */ - void set(String name, Object value); + void set(String name, Object value, + {@Since("2.8") bool preserveHeaderCase: false}); /** * Removes a specific value for a header name. Some headers have @@ -704,11 +719,15 @@ abstract class HttpHeaders { void removeAll(String name); /** - * Enumerates the headers, applying the function [f] to each - * header. The header name passed in [:name:] will be all lower - * case. + * Performs the [action] on each header. + * + * The [action] function is called with each header's name and a list + * of the header's values. The casing of the name string is determined by + * the last [add] or [set] operation for that particular header, + * which defaults to lower-casing the header name unless explicitly + * set to preserve the case. */ - void forEach(void f(String name, List values)); + void forEach(void action(String name, List values)); /** * Disables folding for the header named [name] when sending the HTTP diff --git a/sdk/lib/_http/http_headers.dart b/sdk/lib/_http/http_headers.dart index 30532d5acd0a..5b7853b12510 100644 --- a/sdk/lib/_http/http_headers.dart +++ b/sdk/lib/_http/http_headers.dart @@ -8,6 +8,8 @@ part of dart._http; class _HttpHeaders implements HttpHeaders { final Map> _headers; + // The original header names keyed by the lowercase header names. + Map _originalHeaderNames; final String protocolVersion; bool _mutable = true; // Are the headers currently mutable? @@ -40,10 +42,10 @@ class _HttpHeaders implements HttpHeaders { } } - List operator [](String name) => _headers[name.toLowerCase()]; + List operator [](String name) => _headers[_validateField(name)]; String value(String name) { - name = name.toLowerCase(); + name = _validateField(name); List values = _headers[name]; if (values == null) return null; if (values.length > 1) { @@ -52,13 +54,21 @@ class _HttpHeaders implements HttpHeaders { return values[0]; } - void add(String name, value) { + void add(String name, value, {bool preserveHeaderCase: false}) { _checkMutable(); - _addAll(_validateField(name), value); + String lowercaseName = _validateField(name); + + // TODO(zichangguo): Consider storing all previously cased name for each + // entity instead of replacing name. + if (preserveHeaderCase && name != lowercaseName) { + (_originalHeaderNames ??= {})[lowercaseName] = name; + } else { + _originalHeaderNames?.remove(lowercaseName); + } + _addAll(lowercaseName, value); } void _addAll(String name, value) { - assert(name == _validateField(name)); if (value is Iterable) { for (var v in value) { _add(name, _validateValue(v)); @@ -68,14 +78,20 @@ class _HttpHeaders implements HttpHeaders { } } - void set(String name, Object value) { + void set(String name, Object value, {bool preserveHeaderCase: false}) { _checkMutable(); - name = _validateField(name); - _headers.remove(name); - if (name == HttpHeaders.transferEncodingHeader) { + String lowercaseName = _validateField(name); + _headers.remove(lowercaseName); + _originalHeaderNames?.remove(lowercaseName); + if (lowercaseName == HttpHeaders.transferEncodingHeader) { _chunkedTransferEncoding = false; } - _addAll(name, value); + if (preserveHeaderCase && name != lowercaseName) { + (_originalHeaderNames ??= {})[lowercaseName] = name; + } else { + _originalHeaderNames?.remove(lowercaseName); + } + _addAll(lowercaseName, value); } void remove(String name, Object value) { @@ -88,7 +104,10 @@ class _HttpHeaders implements HttpHeaders { if (index != -1) { values.removeRange(index, index + 1); } - if (values.length == 0) _headers.remove(name); + if (values.length == 0) { + _headers.remove(name); + _originalHeaderNames?.remove(name); + } } if (name == HttpHeaders.transferEncodingHeader && value == "chunked") { _chunkedTransferEncoding = false; @@ -99,10 +118,14 @@ class _HttpHeaders implements HttpHeaders { _checkMutable(); name = _validateField(name); _headers.remove(name); + _originalHeaderNames?.remove(name); } - void forEach(void f(String name, List values)) { - _headers.forEach(f); + void forEach(void action(String name, List values)) { + _headers.forEach((String name, List values) { + String originalName = _originalHeaderName(name); + action(originalName, values); + }); } void noFolding(String name) { @@ -498,14 +521,15 @@ class _HttpHeaders implements HttpHeaders { String toString() { StringBuffer sb = new StringBuffer(); _headers.forEach((String name, List values) { - sb..write(name)..write(": "); + String originalName = _originalHeaderName(name); + sb..write(originalName)..write(": "); bool fold = _foldHeader(name); for (int i = 0; i < values.length; i++) { if (i > 0) { if (fold) { sb.write(", "); } else { - sb..write("\n")..write(name)..write(": "); + sb..write("\n")..write(originalName)..write(": "); } } sb.write(values[i]); @@ -591,7 +615,7 @@ class _HttpHeaders implements HttpHeaders { for (var i = 0; i < field.length; i++) { if (!_HttpParser._isTokenChar(field.codeUnitAt(i))) { throw new FormatException( - "Invalid HTTP header field name: ${json.encode(field)}"); + "Invalid HTTP header field name: ${json.encode(field)}", field, i); } } return field.toLowerCase(); @@ -602,11 +626,16 @@ class _HttpHeaders implements HttpHeaders { for (var i = 0; i < value.length; i++) { if (!_HttpParser._isValueChar(value.codeUnitAt(i))) { throw new FormatException( - "Invalid HTTP header field value: ${json.encode(value)}"); + "Invalid HTTP header field value: ${json.encode(value)}", value, i); } } return value; } + + String _originalHeaderName(String name) { + return (_originalHeaderNames == null ? null : _originalHeaderNames[name]) ?? + name; + } } class _HeaderValue implements HeaderValue { diff --git a/sdk_nnbd/lib/_http/http.dart b/sdk_nnbd/lib/_http/http.dart index 41bc8ded982e..b86139ec0fd0 100644 --- a/sdk_nnbd/lib/_http/http.dart +++ b/sdk_nnbd/lib/_http/http.dart @@ -683,22 +683,31 @@ abstract class HttpHeaders { * The header named [name] will have a string value derived from [value] * added to its list of values. * - * Some headers are single valued, and for these adding a value will replace - * a previous value. - * If the [value] is a [DateTime], an HTTP date format will be - * applied. If the value is a [List], each element of the list will - * be added separately. For all other types the default [Object.toString] - * method will be used. + * Some headers are single valued, and for these, adding a value will + * replace a previous value. If the [value] is a [DateTime], an + * HTTP date format will be applied. If the value is an [Iterable], + * each element will be added separately. For all other + * types the default [Object.toString] method will be used. + * + * Header names are converted to lower-case unless + * [preserveHeaderCase] is set to true. If two header names are + * the same when converted to lower-case, they are considered to be + * the same header, with one set of values. + * + * The current case of the a header name is that of the name used by + * the last [set] or [add] call for that header. */ - void add(String name, Object value); + void add(String name, Object value, + {@Since("2.8") bool preserveHeaderCase: false}); /** - * Sets a header. + * Sets the header [name] to [value]. * - * Removes all existing values for [name], then adds [value] as - * if using [add]. + * Removes all existing values for the header named [name] and + * then [add]s [value] to it. */ - void set(String name, Object value); + void set(String name, Object value, + {@Since("2.8") bool preserveHeaderCase: false}); /** * Removes a specific value for a header name. @@ -723,11 +732,15 @@ abstract class HttpHeaders { void removeAll(String name); /** - * Enumerates the headers, applying the function [f] to each header. + * Performs the [action] on each header. * - * The header name passed in [name] will be all lower case. + * The [action] function is called with each header's name and a list + * of the header's values. The casing of the name string is determined by + * the last [add] or [set] operation for that particular header, + * which defaults to lower-casing the header name unless explicitly + * set to preserve the case. */ - void forEach(void f(String name, List values)); + void forEach(void action(String name, List values)); /** * Disables folding for the header named [name] when sending the HTTP header. diff --git a/sdk_nnbd/lib/_http/http_headers.dart b/sdk_nnbd/lib/_http/http_headers.dart index f23dce2ba00d..1656598d028f 100644 --- a/sdk_nnbd/lib/_http/http_headers.dart +++ b/sdk_nnbd/lib/_http/http_headers.dart @@ -6,6 +6,8 @@ part of dart._http; class _HttpHeaders implements HttpHeaders { final Map> _headers; + // The original header names keyed by the lowercase header names. + Map _originalHeaderNames; final String protocolVersion; bool _mutable = true; // Are the headers currently mutable? @@ -38,10 +40,10 @@ class _HttpHeaders implements HttpHeaders { } } - List? operator [](String name) => _headers[name.toLowerCase()]; + List? operator [](String name) => _headers[_validateField(name)]; String? value(String name) { - name = name.toLowerCase(); + name = _validateField(name); List? values = _headers[name]; if (values == null) return null; assert(values.isNotEmpty); @@ -51,13 +53,21 @@ class _HttpHeaders implements HttpHeaders { return values[0]; } - void add(String name, value) { + void add(String name, value, {bool preserveHeaderCase: false}) { _checkMutable(); - _addAll(_validateField(name), value); + String lowercaseName = _validateField(name); + + // TODO(zichangguo): Consider storing all previously cased name for each + // entity instead of replacing name. + if (preserveHeaderCase && name != lowercaseName) { + (_originalHeaderNames ??= {})[lowercaseName] = name; + } else { + _originalHeaderNames?.remove(lowercaseName); + } + _addAll(lowercaseName, value); } void _addAll(String name, value) { - assert(name == _validateField(name)); if (value is Iterable) { for (var v in value) { _add(name, _validateValue(v)); @@ -67,28 +77,35 @@ class _HttpHeaders implements HttpHeaders { } } - void set(String name, Object value) { + void set(String name, Object value, {bool preserveHeaderCase: false}) { _checkMutable(); - name = _validateField(name); - _headers.remove(name); - if (name == HttpHeaders.transferEncodingHeader) { + String lowercaseName = _validateField(name); + _headers.remove(lowercaseName); + _originalHeaderNames?.remove(lowercaseName); + if (lowercaseName == HttpHeaders.transferEncodingHeader) { _chunkedTransferEncoding = false; } - _addAll(name, value); + if (preserveHeaderCase && name != lowercaseName) { + (_originalHeaderNames ??= {})[lowercaseName] = name; + } + _addAll(lowercaseName, value); } void remove(String name, Object value) { _checkMutable(); name = _validateField(name); value = _validateValue(value); - if (name == HttpHeaders.transferEncodingHeader && value == "chunked") { - _chunkedTransferEncoding = false; - return; - } List? values = _headers[name]; if (values != null) { values.remove(_valueToString(value)); - if (values.length == 0) _headers.remove(name); + if (values.length == 0) { + _headers.remove(name); + _originalHeaderNames?.remove(name); + } + } + if (name == HttpHeaders.transferEncodingHeader && value == "chunked") { + _chunkedTransferEncoding = false; + return; } } @@ -96,10 +113,14 @@ class _HttpHeaders implements HttpHeaders { _checkMutable(); name = _validateField(name); _headers.remove(name); + _originalHeaderNames?.remove(name); } - void forEach(void f(String name, List values)) { - _headers.forEach(f); + void forEach(void action(String name, List values)) { + _headers.forEach((String name, List values) { + String originalName = _originalHeaderName(name); + action(originalName, values); + }); } void noFolding(String name) { @@ -511,14 +532,15 @@ class _HttpHeaders implements HttpHeaders { String toString() { StringBuffer sb = new StringBuffer(); _headers.forEach((String name, List values) { - sb..write(name)..write(": "); + String originalName = _originalHeaderName(name); + sb..write(originalName)..write(": "); bool fold = _foldHeader(name); for (int i = 0; i < values.length; i++) { if (i > 0) { if (fold) { sb.write(", "); } else { - sb..write("\n")..write(name)..write(": "); + sb..write("\n")..write(originalName)..write(": "); } } sb.write(values[i]); @@ -604,7 +626,7 @@ class _HttpHeaders implements HttpHeaders { for (var i = 0; i < field.length; i++) { if (!_HttpParser._isTokenChar(field.codeUnitAt(i))) { throw new FormatException( - "Invalid HTTP header field name: ${json.encode(field)}"); + "Invalid HTTP header field name: ${json.encode(field)}", field, i); } } return field.toLowerCase(); @@ -615,11 +637,15 @@ class _HttpHeaders implements HttpHeaders { for (var i = 0; i < value.length; i++) { if (!_HttpParser._isValueChar(value.codeUnitAt(i))) { throw new FormatException( - "Invalid HTTP header field value: ${json.encode(value)}"); + "Invalid HTTP header field value: ${json.encode(value)}", value, i); } } return value; } + + String _originalHeaderName(String name) { + return _originalHeaderNames?.[name] ?? name; + } } class _HeaderValue implements HeaderValue { diff --git a/tests/standalone_2/io/http_headers_test.dart b/tests/standalone_2/io/http_headers_test.dart index ea53d8af6ecd..d30e390f7516 100644 --- a/tests/standalone_2/io/http_headers_test.dart +++ b/tests/standalone_2/io/http_headers_test.dart @@ -580,6 +580,76 @@ void testFolding() { Expect.isTrue(str.contains(': d')); } +void testLowercaseAdd() { + _HttpHeaders headers = new _HttpHeaders("1.1"); + headers.add('A', 'a'); + Expect.equals(headers['a'][0], headers['A'][0]); + Expect.equals(headers['A'][0], 'a'); + headers.add('Foo', 'Foo', preserveHeaderCase: true); + Expect.equals(headers['Foo'][0], 'Foo'); + // Header field is Foo. + Expect.isTrue(headers.toString().contains('Foo:')); + + headers.add('FOo', 'FOo', preserveHeaderCase: true); + // Header field changes to FOo. + Expect.isTrue(headers.toString().contains('FOo:')); + + headers.add('FOO', 'FOO', preserveHeaderCase: false); + // Header field + Expect.isTrue(!headers.toString().contains('Foo:')); + Expect.isTrue(!headers.toString().contains('FOo:')); + Expect.isTrue(headers.toString().contains('FOO')); +} + +void testLowercaseSet() { + _HttpHeaders headers = new _HttpHeaders("1.1"); + headers.add('test', 'lower cases'); + // 'Test' should override 'test' entity + headers.set('TEST', 'upper cases', preserveHeaderCase: true); + Expect.isTrue(headers.toString().contains('TEST: upper cases')); + Expect.equals(1, headers['test'].length); + Expect.equals(headers['test'][0], 'upper cases'); + + // Latest header will be stored. + headers.set('Test', 'mixed cases', preserveHeaderCase: true); + Expect.isTrue(headers.toString().contains('Test: mixed cases')); + Expect.equals(1, headers['test'].length); + Expect.equals(headers['test'][0], 'mixed cases'); +} + +void testForEach() { + _HttpHeaders headers = new _HttpHeaders("1.1"); + headers.add('header1', 'value1'); + headers.add('header2', 'value2'); + headers.add('HEADER1', 'value3', preserveHeaderCase: true); + headers.add('HEADER3', 'value4', preserveHeaderCase: true); + + bool myHeader1 = false; + bool myHeader2 = false; + bool myHeader3 = false; + int totalValues = 0; + headers.forEach((String name, List values) { + totalValues += values.length; + if (name == "HEADER1") { + myHeader1 = true; + Expect.isTrue(values.indexOf("value 1") != -1); + Expect.isTrue(values.indexOf("value 3") != -1); + } + if (name == "header2") { + myHeader2 = true; + Expect.isTrue(values.indexOf("value 2") != -1); + } + if (name == "HEADER3") { + myHeader3 = true; + Expect.isTrue(values.indexOf("value 4") != -1); + } + }); + Expect.isTrue(myHeader1); + Expect.isTrue(myHeader2); + Expect.isTrue(myHeader3); + Expect.equals(4, totalValues); +} + main() { testMultiValue(); testDate(); @@ -599,4 +669,6 @@ main() { testInvalidFieldValue(); testClear(); testFolding(); + testLowercaseAdd(); + testLowercaseSet(); }