diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bda5bfdaac..1f6ecc4d90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,14 @@ ### Libraries +#### `dart:io` + +- **Breaking change** [#53005][]: The headers returned by + `HttpClientResponse.headers` and `HttpRequest.headers` no longer include + trailing whitespace in their values. + +[#52334]: https://dartbug.com/53005 + #### `dart:js_interop` - **JSNumber.toDart and Object.toJS**: diff --git a/sdk/lib/_http/http_parser.dart b/sdk/lib/_http/http_parser.dart index 0a8bba4c042..9ea44e75a48 100644 --- a/sdk/lib/_http/http_parser.dart +++ b/sdk/lib/_http/http_parser.dart @@ -683,6 +683,9 @@ class _HttpParser extends Stream<_HttpIncoming> { _state = _State.HEADER_VALUE_START; } else { String headerField = String.fromCharCodes(_headerField); + // The field value does not include any leading or trailing whitespace. + // See https://www.rfc-editor.org/rfc/rfc7230#section-3.2.4 + _removeTrailingSpaces(_headerValue); String headerValue = String.fromCharCodes(_headerValue); const errorIfBothText = "Both Content-Length and Transfer-Encoding " "are specified, at most one is allowed"; @@ -1002,6 +1005,17 @@ class _HttpParser extends Stream<_HttpIncoming> { return (byte > 31 && byte < 128) || (byte == _CharCode.HT); } + static void _removeTrailingSpaces(List value) { + var length = value.length; + while (length > 0 && + (value[length - 1] == _CharCode.SP || + value[length - 1] == _CharCode.HT)) { + --length; + } + + value.length = length; + } + static List _tokenizeFieldValue(String headerValue) { List tokens = []; int start = 0; diff --git a/tests/standalone/io/http_parser_test.dart b/tests/standalone/io/http_parser_test.dart index af618a4ebac..af375beb399 100644 --- a/tests/standalone/io/http_parser_test.dart +++ b/tests/standalone/io/http_parser_test.dart @@ -381,13 +381,24 @@ POST /test HTTP/1.1\r request = """ POST /test HTTP/1.1\r -Header-A: AAA\r -X-Header-B: bbb\r +Header-A: AAA aaa\r +X-Header-B: bbb BBB\r \r """; headers = new Map(); - headers["header-a"] = "AAA"; - headers["x-header-b"] = "bbb"; + headers["header-a"] = "AAA aaa"; + headers["x-header-b"] = "bbb BBB"; + _testParseRequestLean(request, "POST", "/test", expectedHeaders: headers); + + request = """ +POST /test HTTP/1.1\r +Header-A: \t AAA aaa \t \r +X-Header-B: \t bbb BBB \t \r +\r +"""; + headers = new Map(); + headers["header-a"] = "AAA aaa"; + headers["x-header-b"] = "bbb BBB"; _testParseRequestLean(request, "POST", "/test", expectedHeaders: headers); request = """ @@ -404,22 +415,23 @@ Empty-Header-2:\r request = """ POST /test HTTP/1.1\r -Header-A: AAA\r -X-Header-B:\t \t bbb\r +Empty-Header-1:\t \t \r +Empty-Header-2:\t \t \r + \r \r """; headers = new Map(); - headers["header-a"] = "AAA"; - headers["x-header-b"] = "bbb"; + headers["empty-header-1"] = ""; + headers["empty-header-2"] = ""; _testParseRequestLean(request, "POST", "/test", expectedHeaders: headers); request = """ POST /test HTTP/1.1\r -Header-A: AA\r - A\r +Header-A: \t AA\r + A \t \r X-Header-B: b\r b\r -\t b\r +\t b \t \r \r """; @@ -461,6 +473,19 @@ Transfer-Encoding: chunked\r 01234\r 5\r 56789\r +0\r\n\r\n"""; + _testParseRequest(request, "POST", "/test", + expectedTransferLength: -1, expectedBytesReceived: 10, chunked: true); + + // Test LWS around chunked encoding header value. + request = """ +POST /test HTTP/1.1\r +Transfer-Encoding: \t chunked \t \r +\r +5\r +01234\r +5\r +56789\r 0\r\n\r\n"""; _testParseRequest(request, "POST", "/test", expectedTransferLength: -1, expectedBytesReceived: 10, chunked: true); @@ -569,6 +594,10 @@ Sec-WebSocket-Version: 13\r _testParseResponse(response, 100, "Continue", expectedTransferLength: 10, expectedBytesReceived: 0); + response = "HTTP/1.1 100 Continue\r\nContent-Length: \t 10 \t \r\n\r\n"; + _testParseResponse(response, 100, "Continue", + expectedTransferLength: 10, expectedBytesReceived: 0); + response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n" "Connection: Close\r\n\r\n"; _testParseResponse(response, 200, "OK", connectionClose: true);