From b2b7337ad4b05b3197cde407dab98a444e291c2f Mon Sep 17 00:00:00 2001 From: Zichang Guo Date: Fri, 31 Jan 2020 17:08:52 +0000 Subject: [PATCH] [dart:io] Stop forcing lower case on HttpHeaders This is a breaking change. Request: https://github.com/dart-lang/sdk/issues/33501 HttpHeaders use lowercase by default for all headers, since it is supposed to be case insensitive. Some servers incorrectly treat case as significant, however, and expect headers with capitalization or in uppercase. The current implementation forces headers to be lower cases when adding values. Users cannot even manually modify the headers. This change removes this restriction here so that users can modify the headers to whatever form they want. The new behavior is backwards compatible except if class was implemented. All headers inside http.dart are written as lower cases, adding values to HttpHeaders is still receiving lower cases input. The other cl (https://dart-review.googlesource.com/c/http_multi_server/+/121411) migrates multi_headers.dart to be compatible with this change. Bug: https://github.com/dart-lang/sdk/issues/33501 Change-Id: I6f7f2ef907b229773c283140c07f2de4cd500981 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/119100 Commit-Queue: Zichang Guo Reviewed-by: Lasse R.H. Nielsen --- CHANGELOG.md | 8 +++ DEPS | 2 +- sdk/lib/_http/http.dart | 49 +++++++++---- sdk/lib/_http/http_headers.dart | 63 ++++++++++++----- sdk_nnbd/lib/_http/http.dart | 41 +++++++---- sdk_nnbd/lib/_http/http_headers.dart | 68 ++++++++++++------ tests/standalone_2/io/http_headers_test.dart | 72 ++++++++++++++++++++ 7 files changed, 235 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0699fd3ca..f89f97a1ca3 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 505808a3bdc..c5d3f19a45e 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 855234bf46f..57c424b7406 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 30532d5acd0..5b7853b1251 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 41bc8ded982..b86139ec0fd 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 f23dce2ba00..1656598d028 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 ea53d8af6ec..d30e390f751 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(); }