[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 <zichangguo@google.com>
Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
This commit is contained in:
Zichang Guo 2020-01-31 17:08:52 +00:00 committed by commit-bot@chromium.org
parent 0ed9f7179d
commit b2b7337ad4
7 changed files with 235 additions and 68 deletions

View file

@ -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

2
DEPS
View file

@ -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",

View file

@ -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<String> values));
void forEach(void action(String name, List<String> values));
/**
* Disables folding for the header named [name] when sending the HTTP

View file

@ -8,6 +8,8 @@ part of dart._http;
class _HttpHeaders implements HttpHeaders {
final Map<String, List<String>> _headers;
// The original header names keyed by the lowercase header names.
Map<String, String> _originalHeaderNames;
final String protocolVersion;
bool _mutable = true; // Are the headers currently mutable?
@ -40,10 +42,10 @@ class _HttpHeaders implements HttpHeaders {
}
}
List<String> operator [](String name) => _headers[name.toLowerCase()];
List<String> operator [](String name) => _headers[_validateField(name)];
String value(String name) {
name = name.toLowerCase();
name = _validateField(name);
List<String> 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<String> values)) {
_headers.forEach(f);
void forEach(void action(String name, List<String> values)) {
_headers.forEach((String name, List<String> 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<String> 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 {

View file

@ -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<String> values));
void forEach(void action(String name, List<String> values));
/**
* Disables folding for the header named [name] when sending the HTTP header.

View file

@ -6,6 +6,8 @@ part of dart._http;
class _HttpHeaders implements HttpHeaders {
final Map<String, List<String>> _headers;
// The original header names keyed by the lowercase header names.
Map<String, String> _originalHeaderNames;
final String protocolVersion;
bool _mutable = true; // Are the headers currently mutable?
@ -38,10 +40,10 @@ class _HttpHeaders implements HttpHeaders {
}
}
List<String>? operator [](String name) => _headers[name.toLowerCase()];
List<String>? operator [](String name) => _headers[_validateField(name)];
String? value(String name) {
name = name.toLowerCase();
name = _validateField(name);
List<String>? 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<String>? 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<String> values)) {
_headers.forEach(f);
void forEach(void action(String name, List<String> values)) {
_headers.forEach((String name, List<String> 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<String> 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 {

View file

@ -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<String> 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();
}