From fac842442be8ab6b1e7a55d4187a0940603e3106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Wed, 18 Dec 2024 14:32:50 +0100 Subject: [PATCH 01/10] fix: Correct transfer encoding and body content length handling --- lib/src/body/body.dart | 66 ++++++++++- lib/src/body/types/mime_type.dart | 8 ++ lib/src/headers/headers.dart | 15 +-- lib/src/message/response.dart | 45 +------- test/message/body_test.dart | 186 ++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 56 deletions(-) create mode 100644 test/message/body_test.dart diff --git a/lib/src/body/body.dart b/lib/src/body/body.dart index 11ab6b3..728a99c 100644 --- a/lib/src/body/body.dart +++ b/lib/src/body/body.dart @@ -3,7 +3,8 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:relic/src/body/types/body_type.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:relic/relic.dart'; import 'package:relic/src/body/types/mime_type.dart'; /// The body of a request or response. @@ -124,10 +125,71 @@ class Body { return stream; } + /// Applies the headers to the response and encodes the body if transfer encoding is chunked. + void applyHeadersAndEncodeBody( + HttpResponse response, { + TransferEncodingHeader? transferEncoding, + }) { + // If the body is empty (contentLength == 0), explicitly set Content-Length to 0 + // and remove any Transfer-Encoding header since no encoding is needed. + if (contentLength == 0) { + response.headers.contentLength = 0; + response.headers.removeAll(Headers.transferEncodingHeader); + return; + } + + // Set the Content-Type header based on the MIME type of the body, if available. + response.headers.contentType = _getContentType(); + + // Retrieve the status code for further validation. + int statusCode = response.statusCode; + + // Determine if chunked encoding should be applied. + // Chunked encoding is enabled if: + // - The status code is in the 200 range but not 204 (No Content) or 304 (Not Modified). + // - The content length is unknown (contentLength == null). + // - The content type is not "multipart/byteranges" (excluded as per HTTP spec). + bool shouldEnableChunkedEncoding = statusCode >= 200 && + statusCode != 204 && + statusCode != 304 && + contentLength == null && + (contentType?.mimeType.isNotMultipartByteranges ?? false); + + // Check if chunked encoding is already enabled by inspecting the Transfer-Encoding header. + bool isChunked = transferEncoding?.isChunked ?? false; + + var encodings = transferEncoding?.encodings ?? []; + // If chunked encoding should be enabled but is not already, update the Transfer-Encoding header. + if (shouldEnableChunkedEncoding && !isChunked) { + // Add 'chunked' to the Transfer-Encoding header. + encodings.add(TransferEncoding.chunked); + + // Apply chunked encoding to the response stream to encode the body in chunks. + _stream = chunkedCoding.encoder.bind(_stream!).cast(); + + // Mark the response as chunked for further processing. + isChunked = true; + } + + if (isChunked) { + // Remove any existing 'identity' transfer encoding, as it conflicts with 'chunked'. + encodings.removeWhere((e) => e.name == TransferEncoding.identity.name); + // Set the Transfer-Encoding header with the updated encodings. + response.headers.set(Headers.transferEncodingHeader, encodings); + + // If the response is already chunked, remove the Content-Length header. + // Chunked encoding does not require Content-Length because chunk sizes define the body length. + response.headers.removeAll(Headers.contentLengthHeader); + } else { + // If chunked encoding is not enabled, set the Content-Length header to the known body length. + response.headers.contentLength = contentLength ?? 0; + } + } + /// Returns the content type of the body as a [ContentType]. /// /// This is a convenience method that combines [mimeType] and [encoding]. - ContentType? getContentType() { + ContentType? _getContentType() { var mContentType = contentType; if (mContentType == null) return null; return ContentType( diff --git a/lib/src/body/types/mime_type.dart b/lib/src/body/types/mime_type.dart index 4f13e78..20e3669 100644 --- a/lib/src/body/types/mime_type.dart +++ b/lib/src/body/types/mime_type.dart @@ -74,3 +74,11 @@ extension ContentTypeExtension on ContentType { /// We are calling this method 'toMimeType' to avoid conflict with the 'mimeType' property. MimeType get toMimeType => MimeType(primaryType, subType); } + +/// Extension to check if a [MimeType] is not multipart/byteranges. +extension MimeTypeExtensions on MimeType { + /// Checks if the mime type is not multipart/byteranges. + bool get isNotMultipartByteranges { + return primaryType != 'multipart' || subType != 'byteranges'; + } +} diff --git a/lib/src/headers/headers.dart b/lib/src/headers/headers.dart index 14bb64b..580858f 100644 --- a/lib/src/headers/headers.dart +++ b/lib/src/headers/headers.dart @@ -10,8 +10,6 @@ import 'package:relic/src/method/request_method.dart'; import 'typed/typed_headers.dart'; -import '../body/body.dart'; - abstract base class Headers { /// Request Headers static const acceptHeader = "accept"; @@ -910,7 +908,7 @@ abstract base class Headers { }); /// Apply headers to the response - void applyHeaders(io.HttpResponse response, Body body); + void applyHeaders(io.HttpResponse response); /// Convert headers to a map Map toMap(); @@ -1237,10 +1235,7 @@ final class _HeadersImpl extends Headers { /// Apply headers to the response @override - void applyHeaders( - io.HttpResponse response, - Body body, - ) { + void applyHeaders(io.HttpResponse response) { var headers = response.headers; headers.clear(); @@ -1308,12 +1303,6 @@ final class _HeadersImpl extends Headers { for (var entry in custom.entries) { headers.set(entry.key, entry.value); } - - // Set the content length from the Body - headers.contentLength = body.contentLength ?? 0; - - // Set the content type from the Body - headers.contentType = body.getContentType(); } /// Convert headers to a map diff --git a/lib/src/message/response.dart b/lib/src/message/response.dart index 8876158..d731416 100644 --- a/lib/src/message/response.dart +++ b/lib/src/message/response.dart @@ -1,9 +1,5 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; - -import 'package:http_parser/http_parser.dart'; -import 'package:relic/src/headers/typed/headers/transfer_encoding_header.dart'; import '../body/body.dart'; import '../headers/headers.dart'; @@ -329,48 +325,15 @@ class Response extends Message { httpResponse.statusCode = statusCode; - headers.applyHeaders( - httpResponse, - body, - ); + headers.applyHeaders(httpResponse); - var mBody = _handleTransferEncoding( + body.applyHeadersAndEncodeBody( httpResponse, - headers.transferEncoding, - statusCode, - body, + transferEncoding: headers.transferEncoding, ); return httpResponse - .addStream(mBody.read()) + .addStream(body.read()) .then((_) => httpResponse.close()); } } - -Body _handleTransferEncoding( - HttpResponse httpResponse, - TransferEncodingHeader? transferEncoding, - int statusCode, - Body body, -) { - if (transferEncoding?.isChunked == true) { - // If the response is already chunked, decode it to avoid double chunking. - body = Body.fromDataStream( - chunkedCoding.decoder.bind(body.read()).cast(), - ); - httpResponse.headers.set(HttpHeaders.transferEncodingHeader, 'chunked'); - return body; - } else if (_shouldEnableChunkedEncoding(statusCode, body)) { - // If content length is unknown and chunking is needed, set chunked encoding. - httpResponse.headers.set(HttpHeaders.transferEncodingHeader, 'chunked'); - } - return body; -} - -bool _shouldEnableChunkedEncoding(int statusCode, Body body) { - return statusCode >= 200 && - statusCode != 204 && - statusCode != 304 && - body.contentLength == null && - body.contentType?.mimeType.toString() != 'multipart/byteranges'; -} diff --git a/test/message/body_test.dart b/test/message/body_test.dart new file mode 100644 index 0000000..fbff5bf --- /dev/null +++ b/test/message/body_test.dart @@ -0,0 +1,186 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; +import 'package:relic/relic.dart'; +import 'package:relic/src/method/request_method.dart'; +import 'package:relic/src/relic_server_serve.dart' as relic_server; +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + var server = _server; + if (server != null) { + try { + await server.close().timeout(Duration(seconds: 5)); + } catch (e) { + await server.close(force: true); + } finally { + _server = null; + } + } + }); + + group('Given a response', () { + test( + 'with an empty body and "chunked" transfer encoding ' + 'when applying headers to the response then the "chunked" transfer ' + 'encoding is removed', () async { + await _scheduleServer( + (_) => Response.ok( + body: Body.empty(), + headers: Headers.response( + transferEncoding: TransferEncodingHeader( + encodings: [TransferEncoding.chunked], + ), + ), + ), + ); + + var response = await _get(); + expect(response.body, isEmpty); + expect(response.headers['transfer-encoding'], isNull); + }); + + test( + 'with unknown content length when applying headers ' + 'to the response then "chunked" transfer encoding is added', () async { + await _scheduleServer( + (_) => Response.ok( + body: Body.fromDataStream( + Stream.fromIterable([ + Uint8List.fromList([1, 2, 3, 4]) + ]), + ), + ), + ); + + var response = await _get(); + expect(response.headers['transfer-encoding'], contains('chunked')); + expect(response.bodyBytes, equals([1, 2, 3, 4])); + }); + + test( + 'with a known content length when applying headers ' + 'to the response then "chunked" transfer encoding is not added', + () async { + await _scheduleServer( + (_) => Response.ok( + body: Body.fromData( + Uint8List.fromList([1, 2, 3, 4]), + ), + ), + ); + + var response = await _get(); + expect(response.headers['transfer-encoding'], isNull); + expect(response.headers['content-length'], equals('4')); + expect(response.bodyBytes, equals([1, 2, 3, 4])); + }); + + test( + 'with "identity" transfer encoding and known content length when ' + 'applying headers to the response then "chunked" transfer encoding ' + 'is added and "identity" is removed', () async { + await _scheduleServer( + (_) => Response.ok( + body: Body.fromDataStream( + Stream.fromIterable([ + Uint8List.fromList([1, 2, 3, 4]) + ]), + ), + headers: Headers.response( + transferEncoding: TransferEncodingHeader( + encodings: [TransferEncoding.identity], + ), + ), + ), + ); + + var response = await _get(); + expect(response.headers['transfer-encoding'], contains('chunked')); + expect( + response.headers['transfer-encoding'], + isNot(contains('identity')), + ); + expect(response.bodyBytes, equals([1, 2, 3, 4])); + }); + + test( + 'with "chunked" transfer encoding already applied when applying headers ' + 'to the response then "chunked" is retained', () async { + await _scheduleServer( + (_) => Response.ok( + body: Body.fromDataStream( + Stream.fromIterable([ + Uint8List.fromList('5\r\nRelic\r\n0\r\n\r\n'.codeUnits), + ]), + ), + headers: Headers.response( + transferEncoding: TransferEncodingHeader( + encodings: [TransferEncoding.chunked], + ), + ), + ), + ); + + var response = await _get(); + expect(response.headers['transfer-encoding'], contains('chunked')); + expect(response.body, equals('Relic')); + }); + + test( + 'with a valid content length when applying headers ' + 'to the response then Content-Length is used instead of chunked encoding', + () async { + await _scheduleServer( + (_) => Response.ok( + body: Body.fromDataStream( + Stream.fromIterable([ + Uint8List.fromList([1, 2, 3, 4]) + ]), + contentLength: 4, + ), + headers: Headers.response(), + ), + ); + + var response = await _get(); + expect(response.headers['content-length'], equals('4')); + expect(response.headers['transfer-encoding'], isNull); + expect(response.bodyBytes, equals([1, 2, 3, 4])); + }); + }); +} + +int get _serverPort => _server!.port; + +HttpServer? _server; + +Future _scheduleServer( + Handler handler, { + SecurityContext? securityContext, +}) async { + assert(_server == null); + _server = await relic_server.serve( + handler, + 'localhost', + 0, + securityContext: securityContext, + ); +} + +Future _get({ + Map? headers, + String path = '', +}) async { + var request = http.Request( + RequestMethod.get.value, + Uri.http('localhost:$_serverPort', path), + ); + + if (headers != null) request.headers.addAll(headers); + + var response = await request.send(); + return await http.Response.fromStream(response); +} From 51b2187215dbb0279b90b2d1e83be95c8578f875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Wed, 18 Dec 2024 14:33:38 +0100 Subject: [PATCH 02/10] fix: Fix transfer encodings order to always have 'chunked' last if present --- .../headers/transfer_encoding_header.dart | 36 +++++++++++++++++-- test/headers/headers_test_utils.dart | 5 ++- .../transfer_encoding_header_test.dart | 29 ++++++++++++--- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/lib/src/headers/typed/headers/transfer_encoding_header.dart b/lib/src/headers/typed/headers/transfer_encoding_header.dart index 477f7c8..c0332df 100644 --- a/lib/src/headers/typed/headers/transfer_encoding_header.dart +++ b/lib/src/headers/typed/headers/transfer_encoding_header.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:relic/src/headers/extension/string_list_extensions.dart'; import 'package:relic/src/headers/typed/typed_header_interface.dart'; @@ -10,9 +11,9 @@ class TransferEncodingHeader implements TypedHeader { final List encodings; /// Constructs a [TransferEncodingHeader] instance with the specified transfer encodings. - const TransferEncodingHeader({ - required this.encodings, - }); + TransferEncodingHeader({ + required List encodings, + }) : encodings = _reorderEncodings(encodings); /// Parses the Transfer-Encoding header value and returns a [TransferEncodingHeader] instance. /// @@ -42,6 +43,35 @@ class TransferEncodingHeader implements TypedHeader { String toString() { return 'TransferEncodingHeader(encodings: $encodings)'; } + + /// Ensures that the 'chunked' transfer encoding is always the last in the list. + /// + /// According to the HTTP/1.1 specification (RFC 9112), the 'chunked' transfer + /// encoding must be the final encoding applied to the response body. This is + /// because 'chunked' signals the end of the response message, and any + /// encoding after 'chunked' would cause ambiguity or violate the standard. + /// + /// Example of valid ordering: + /// Transfer-Encoding: gzip, chunked + /// + /// Example of invalid ordering: + /// Transfer-Encoding: chunked, gzip + /// + /// This function reorders the encodings to comply with the standard and + /// ensures compatibility with HTTP clients and intermediaries. + static List _reorderEncodings( + List encodings, + ) { + final TransferEncoding? chunked = encodings.firstWhereOrNull( + (e) => e.name == TransferEncoding.chunked.name, + ); + if (chunked == null) return encodings; + + var reordered = List.from(encodings); + reordered.removeWhere((e) => e.name == TransferEncoding.chunked.name); + reordered.add(chunked); + return reordered; + } } /// A class representing valid transfer encodings. diff --git a/test/headers/headers_test_utils.dart b/test/headers/headers_test_utils.dart index 5f89439..4002cf5 100644 --- a/test/headers/headers_test_utils.dart +++ b/test/headers/headers_test_utils.dart @@ -39,13 +39,16 @@ Future createServer({ Future getServerRequestHeaders({ required RelicServer server, required Map headers, + bool echoHeaders = true, }) async { Headers? parsedHeaders; server.mountAndStart( (Request request) { parsedHeaders = request.headers; - return Response.ok(); + return Response.ok( + headers: echoHeaders ? parsedHeaders : null, + ); }, ); diff --git a/test/headers/typed_headers/transfer_encoding_header_test.dart b/test/headers/typed_headers/transfer_encoding_header_test.dart index ef007c1..fcf08cd 100644 --- a/test/headers/typed_headers/transfer_encoding_header_test.dart +++ b/test/headers/typed_headers/transfer_encoding_header_test.dart @@ -53,8 +53,12 @@ void main() { }, ); + /// According to the HTTP/1.1 specification (RFC 9112), the 'chunked' transfer + /// encoding must be the final encoding applied to the response body. test( - 'when a valid Transfer-Encoding header is passed then it should parse the encodings correctly', + 'when a valid Transfer-Encoding header is passed with "chunked" as not the last ' + 'encoding then it should parse the encodings correctly and reorder them sot the ' + 'chunked encoding is the last encoding', () async { Headers headers = await getServerRequestHeaders( server: server, @@ -63,7 +67,22 @@ void main() { expect( headers.transferEncoding?.encodings.map((e) => e.name), - equals(['chunked', 'gzip']), + equals(['gzip', 'chunked']), + ); + }, + ); + + test( + 'when a valid Transfer-Encoding header is passed then it should parse the encodings correctly', + () async { + Headers headers = await getServerRequestHeaders( + server: server, + headers: {'transfer-encoding': 'gzip, chunked'}, + ); + + expect( + headers.transferEncoding?.encodings.map((e) => e.name), + equals(['gzip', 'chunked']), ); }, ); @@ -74,12 +93,12 @@ void main() { () async { Headers headers = await getServerRequestHeaders( server: server, - headers: {'transfer-encoding': 'chunked, gzip, chunked'}, + headers: {'transfer-encoding': 'gzip, chunked, chunked'}, ); expect( headers.transferEncoding?.encodings.map((e) => e.name), - equals(['chunked', 'gzip']), + equals(['gzip', 'chunked']), ); }, ); @@ -89,7 +108,7 @@ void main() { () async { Headers headers = await getServerRequestHeaders( server: server, - headers: {'transfer-encoding': 'chunked, gzip'}, + headers: {'transfer-encoding': 'gzip, chunked'}, ); expect(headers.transferEncoding?.isChunked, isTrue); From 24f45c8811c8c94c2d4156ff1f7a5045c9107b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Fri, 20 Dec 2024 11:06:08 +0100 Subject: [PATCH 03/10] fix: Remove manual body encoding and remove transfer encoding when content length is set --- lib/src/body/body.dart | 67 +++++++++++++------------------ lib/src/message/response.dart | 5 ++- test/message/body_test.dart | 2 +- test/relic_server_serve_test.dart | 3 +- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/lib/src/body/body.dart b/lib/src/body/body.dart index 728a99c..10d2076 100644 --- a/lib/src/body/body.dart +++ b/lib/src/body/body.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:http_parser/http_parser.dart'; import 'package:relic/relic.dart'; import 'package:relic/src/body/types/mime_type.dart'; @@ -125,64 +124,54 @@ class Body { return stream; } - /// Applies the headers to the response and encodes the body if transfer encoding is chunked. - void applyHeadersAndEncodeBody( + /// Applies transfer encoding headers and content length to the response. + void applyHeaders( HttpResponse response, { TransferEncodingHeader? transferEncoding, }) { - // If the body is empty (contentLength == 0), explicitly set Content-Length to 0 - // and remove any Transfer-Encoding header since no encoding is needed. - if (contentLength == 0) { - response.headers.contentLength = 0; - response.headers.removeAll(Headers.transferEncodingHeader); - return; - } - - // Set the Content-Type header based on the MIME type of the body, if available. + // Set the Content-Type header based on the MIME type of the body. response.headers.contentType = _getContentType(); - // Retrieve the status code for further validation. - int statusCode = response.statusCode; + // If the content length is known, set it and remove the Transfer-Encoding header. + if (contentLength != null) { + response.headers + ..contentLength = contentLength! + ..removeAll(Headers.transferEncodingHeader); + return; + } // Determine if chunked encoding should be applied. - // Chunked encoding is enabled if: - // - The status code is in the 200 range but not 204 (No Content) or 304 (Not Modified). - // - The content length is unknown (contentLength == null). - // - The content type is not "multipart/byteranges" (excluded as per HTTP spec). - bool shouldEnableChunkedEncoding = statusCode >= 200 && - statusCode != 204 && - statusCode != 304 && + bool shouldEnableChunkedEncoding = response.statusCode >= 200 && + // 204 is no content + response.statusCode != 204 && + // 304 is not modified + response.statusCode != 304 && + // If the content length is not known, chunked encoding is applied. contentLength == null && - (contentType?.mimeType.isNotMultipartByteranges ?? false); + // If the content type is not multipart/byteranges, chunked encoding is applied. + (contentType?.mimeType.isNotMultipartByteranges ?? true); - // Check if chunked encoding is already enabled by inspecting the Transfer-Encoding header. + // Prepare transfer encodings. + var encodings = transferEncoding?.encodings ?? []; bool isChunked = transferEncoding?.isChunked ?? false; - var encodings = transferEncoding?.encodings ?? []; - // If chunked encoding should be enabled but is not already, update the Transfer-Encoding header. if (shouldEnableChunkedEncoding && !isChunked) { - // Add 'chunked' to the Transfer-Encoding header. encodings.add(TransferEncoding.chunked); - - // Apply chunked encoding to the response stream to encode the body in chunks. - _stream = chunkedCoding.encoder.bind(_stream!).cast(); - - // Mark the response as chunked for further processing. isChunked = true; } if (isChunked) { - // Remove any existing 'identity' transfer encoding, as it conflicts with 'chunked'. + // Remove conflicting 'identity' encoding if present. encodings.removeWhere((e) => e.name == TransferEncoding.identity.name); - // Set the Transfer-Encoding header with the updated encodings. - response.headers.set(Headers.transferEncodingHeader, encodings); - // If the response is already chunked, remove the Content-Length header. - // Chunked encoding does not require Content-Length because chunk sizes define the body length. - response.headers.removeAll(Headers.contentLengthHeader); + // Set Transfer-Encoding header and remove Content-Length as it is not needed for chunked encoding. + response.headers + ..set(Headers.transferEncodingHeader, + encodings.map((e) => e.name).toList()) + ..removeAll(Headers.contentLengthHeader); } else { - // If chunked encoding is not enabled, set the Content-Length header to the known body length. - response.headers.contentLength = contentLength ?? 0; + // Set Content-Length to 0 if chunked encoding is not enabled. + response.headers.contentLength = 0; } } diff --git a/lib/src/message/response.dart b/lib/src/message/response.dart index d731416..9049088 100644 --- a/lib/src/message/response.dart +++ b/lib/src/message/response.dart @@ -323,11 +323,14 @@ class Response extends Message { httpResponse.bufferOutput = context['relic_server.buffer_output'] as bool; } + // Set the status code. httpResponse.statusCode = statusCode; + // Apply all headers to the response. headers.applyHeaders(httpResponse); - body.applyHeadersAndEncodeBody( + // Apply transfer encoding headers and content length to the response. + body.applyHeaders( httpResponse, transferEncoding: headers.transferEncoding, ); diff --git a/test/message/body_test.dart b/test/message/body_test.dart index fbff5bf..50a4ce1 100644 --- a/test/message/body_test.dart +++ b/test/message/body_test.dart @@ -126,7 +126,7 @@ void main() { var response = await _get(); expect(response.headers['transfer-encoding'], contains('chunked')); - expect(response.body, equals('Relic')); + expect(response.body, equals('5\r\nRelic\r\n0\r\n\r\n')); }); test( diff --git a/test/relic_server_serve_test.dart b/test/relic_server_serve_test.dart index 715a5eb..feab5b6 100644 --- a/test/relic_server_serve_test.dart +++ b/test/relic_server_serve_test.dart @@ -508,7 +508,7 @@ void main() { response.headers, containsPair(HttpHeaders.transferEncodingHeader, 'chunked'), ); - expect(response.body, equals('hi')); + expect(response.body, equals('2\r\nhi\r\n0\r\n\r\n')); }); group('is not added when', () { @@ -517,6 +517,7 @@ void main() { return Response.ok( body: Body.fromDataStream( Stream.value(Uint8List.fromList([1, 2, 3, 4])), + contentLength: 4, ), ); }); From baf36947b77c306156c36ebc28c8dc54fca6c582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Thu, 2 Jan 2025 14:53:46 +0100 Subject: [PATCH 04/10] test: Unit test improvements --- test/message/body_test.dart | 17 +++++++++++++---- test/relic_server_serve_test.dart | 26 ++++++++++++++++++++------ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/test/message/body_test.dart b/test/message/body_test.dart index 50a4ce1..db47ebf 100644 --- a/test/message/body_test.dart +++ b/test/message/body_test.dart @@ -56,7 +56,10 @@ void main() { ); var response = await _get(); - expect(response.headers['transfer-encoding'], contains('chunked')); + expect( + response.headers['transfer-encoding'], + contains(TransferEncoding.chunked.name), + ); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); @@ -98,10 +101,13 @@ void main() { ); var response = await _get(); - expect(response.headers['transfer-encoding'], contains('chunked')); expect( response.headers['transfer-encoding'], - isNot(contains('identity')), + contains(TransferEncoding.chunked.name), + ); + expect( + response.headers['transfer-encoding'], + isNot(contains(TransferEncoding.identity.name)), ); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); @@ -125,7 +131,10 @@ void main() { ); var response = await _get(); - expect(response.headers['transfer-encoding'], contains('chunked')); + expect( + response.headers['transfer-encoding'], + contains(TransferEncoding.chunked.name), + ); expect(response.body, equals('5\r\nRelic\r\n0\r\n\r\n')); }); diff --git a/test/relic_server_serve_test.dart b/test/relic_server_serve_test.dart index feab5b6..0b20d2d 100644 --- a/test/relic_server_serve_test.dart +++ b/test/relic_server_serve_test.dart @@ -458,8 +458,13 @@ void main() { }); var response = await _get(); - expect(response.headers, - containsPair(HttpHeaders.transferEncodingHeader, 'chunked')); + expect( + response.headers, + containsPair( + HttpHeaders.transferEncodingHeader, + TransferEncoding.chunked.name, + ), + ); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); @@ -478,13 +483,19 @@ void main() { }); var response = await _get(); - expect(response.headers, - containsPair(HttpHeaders.transferEncodingHeader, 'chunked')); + expect( + response.headers, + containsPair( + HttpHeaders.transferEncodingHeader, + TransferEncoding.chunked.name, + ), + ); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); }); - test('is preserved when the transfer-encoding header is "chunked"', + test( + 'is preserved and body is not modified when the transfer-encoding header is "chunked"', () async { await _scheduleServer((request) { return Response.ok( @@ -506,7 +517,10 @@ void main() { var response = await _get(); expect( response.headers, - containsPair(HttpHeaders.transferEncodingHeader, 'chunked'), + containsPair( + HttpHeaders.transferEncodingHeader, + TransferEncoding.chunked.name, + ), ); expect(response.body, equals('2\r\nhi\r\n0\r\n\r\n')); }); From f4da065e5fa07af6461548974db4a794933c080b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Thu, 2 Jan 2025 14:54:30 +0100 Subject: [PATCH 05/10] refactor: Mime type improvements for multipart/bytesrange type --- lib/src/body/types/mime_type.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/body/types/mime_type.dart b/lib/src/body/types/mime_type.dart index 20e3669..9ed39cc 100644 --- a/lib/src/body/types/mime_type.dart +++ b/lib/src/body/types/mime_type.dart @@ -33,6 +33,12 @@ class MimeType { /// RTF mime type. static const rtf = MimeType('application', 'rtf'); + /// Multipart mime type. + static const multipart = MimeType('multipart', 'form-data'); + + /// Multipart mime type. + static const multipartByteranges = MimeType('multipart', 'byteranges'); + /// The primary type of the mime type. final String primaryType; @@ -78,7 +84,9 @@ extension ContentTypeExtension on ContentType { /// Extension to check if a [MimeType] is not multipart/byteranges. extension MimeTypeExtensions on MimeType { /// Checks if the mime type is not multipart/byteranges. - bool get isNotMultipartByteranges { - return primaryType != 'multipart' || subType != 'byteranges'; + bool get isMultipartByteranges { + var multipartByteranges = MimeType.multipartByteranges; + return primaryType == multipartByteranges.primaryType && + subType == multipartByteranges.subType; } } From f02c19a5dbedc49dadac6a43c158b2f6b7884c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Thu, 2 Jan 2025 14:59:26 +0100 Subject: [PATCH 06/10] refactor: Combine 'applyHeaders' into an extension on 'HttpResponse' --- lib/src/body/body.dart | 53 +------------ .../extensions/http_response_extension.dart | 68 ++++++++++++++++ lib/src/headers/headers.dart | 79 +------------------ lib/src/message/response.dart | 10 +-- 4 files changed, 73 insertions(+), 137 deletions(-) create mode 100644 lib/src/extensions/http_response_extension.dart diff --git a/lib/src/body/body.dart b/lib/src/body/body.dart index 10d2076..233b06e 100644 --- a/lib/src/body/body.dart +++ b/lib/src/body/body.dart @@ -124,61 +124,10 @@ class Body { return stream; } - /// Applies transfer encoding headers and content length to the response. - void applyHeaders( - HttpResponse response, { - TransferEncodingHeader? transferEncoding, - }) { - // Set the Content-Type header based on the MIME type of the body. - response.headers.contentType = _getContentType(); - - // If the content length is known, set it and remove the Transfer-Encoding header. - if (contentLength != null) { - response.headers - ..contentLength = contentLength! - ..removeAll(Headers.transferEncodingHeader); - return; - } - - // Determine if chunked encoding should be applied. - bool shouldEnableChunkedEncoding = response.statusCode >= 200 && - // 204 is no content - response.statusCode != 204 && - // 304 is not modified - response.statusCode != 304 && - // If the content length is not known, chunked encoding is applied. - contentLength == null && - // If the content type is not multipart/byteranges, chunked encoding is applied. - (contentType?.mimeType.isNotMultipartByteranges ?? true); - - // Prepare transfer encodings. - var encodings = transferEncoding?.encodings ?? []; - bool isChunked = transferEncoding?.isChunked ?? false; - - if (shouldEnableChunkedEncoding && !isChunked) { - encodings.add(TransferEncoding.chunked); - isChunked = true; - } - - if (isChunked) { - // Remove conflicting 'identity' encoding if present. - encodings.removeWhere((e) => e.name == TransferEncoding.identity.name); - - // Set Transfer-Encoding header and remove Content-Length as it is not needed for chunked encoding. - response.headers - ..set(Headers.transferEncodingHeader, - encodings.map((e) => e.name).toList()) - ..removeAll(Headers.contentLengthHeader); - } else { - // Set Content-Length to 0 if chunked encoding is not enabled. - response.headers.contentLength = 0; - } - } - /// Returns the content type of the body as a [ContentType]. /// /// This is a convenience method that combines [mimeType] and [encoding]. - ContentType? _getContentType() { + ContentType? getContentType() { var mContentType = contentType; if (mContentType == null) return null; return ContentType( diff --git a/lib/src/extensions/http_response_extension.dart b/lib/src/extensions/http_response_extension.dart new file mode 100644 index 0000000..b2e1633 --- /dev/null +++ b/lib/src/extensions/http_response_extension.dart @@ -0,0 +1,68 @@ +import 'package:relic/relic.dart'; +import 'dart:io' as io; + +import 'package:relic/src/body/types/mime_type.dart'; + +/// Extension for [io.HttpResponse] to apply headers and body. +extension HttpResponseExtension on io.HttpResponse { + /// Apply headers and body to the response. + void applyHeaders(Headers headers, Body body) { + var httpHeaders = this.headers; + httpHeaders.clear(); + + // Apply headers + var mappedHeaders = headers.toMap(); + for (var entry in mappedHeaders.entries) { + var key = entry.key; + var value = entry.value; + httpHeaders.set(key, value); + } + + // Set the Content-Type header based on the MIME type of the body. + httpHeaders.contentType = body.getContentType(); + + // If the content length is known, set it and remove the Transfer-Encoding header. + if (body.contentLength != null) { + httpHeaders + ..contentLength = body.contentLength! + ..removeAll(Headers.transferEncodingHeader); + return; + } + + // Determine if chunked encoding should be applied. + bool shouldEnableChunkedEncoding = statusCode >= 200 && + // 204 is no content + statusCode != 204 && + // 304 is not modified + statusCode != 304 && + // If the content length is not known, chunked encoding is applied. + body.contentLength == null && + // If the content type is not multipart/byteranges, chunked encoding is applied. + (body.contentType?.mimeType.isMultipartByteranges == false); + + // Prepare transfer encodings. + var encodings = headers.transferEncoding?.encodings ?? []; + bool isChunked = headers.transferEncoding?.isChunked ?? false; + + if (shouldEnableChunkedEncoding && !isChunked) { + encodings.add(TransferEncoding.chunked); + isChunked = true; + } + + if (isChunked) { + // Remove conflicting 'identity' encoding if present. + encodings.removeWhere((e) => e.name == TransferEncoding.identity.name); + + // Set Transfer-Encoding header and remove Content-Length as it is not needed for chunked encoding. + httpHeaders + ..set( + Headers.transferEncodingHeader, + encodings.map((e) => e.name).toList(), + ) + ..removeAll(Headers.contentLengthHeader); + } else { + // Set Content-Length to 0 if chunked encoding is not enabled. + httpHeaders.contentLength = 0; + } + } +} diff --git a/lib/src/headers/headers.dart b/lib/src/headers/headers.dart index 580858f..db898ca 100644 --- a/lib/src/headers/headers.dart +++ b/lib/src/headers/headers.dart @@ -1,15 +1,13 @@ import 'dart:io' as io; import 'package:http_parser/http_parser.dart'; -import 'package:relic/src/headers/custom/custom_headers.dart'; +import 'package:relic/relic.dart'; import 'package:relic/src/headers/extension/string_list_extensions.dart'; import 'package:relic/src/headers/parser/headers_parser.dart'; import 'package:relic/src/headers/parser/common_types_parser.dart'; import 'package:relic/src/headers/typed/typed_header_interface.dart'; import 'package:relic/src/method/request_method.dart'; -import 'typed/typed_headers.dart'; - abstract base class Headers { /// Request Headers static const acceptHeader = "accept"; @@ -907,9 +905,6 @@ abstract base class Headers { CrossOriginOpenerPolicyHeader? crossOriginOpenerPolicy, }); - /// Apply headers to the response - void applyHeaders(io.HttpResponse response); - /// Convert headers to a map Map toMap(); } @@ -1233,78 +1228,6 @@ final class _HeadersImpl extends Headers { ); } - /// Apply headers to the response - @override - void applyHeaders(io.HttpResponse response) { - var headers = response.headers; - headers.clear(); - - // Date-related headers - var dateHeaders = _dateHeadersMap; - for (var entry in dateHeaders.entries) { - var key = entry.key; - var value = entry.value; - if (value != null) { - headers.set(key, formatHttpDate(value)); - } - } - - // Number-related headers - var numberHeaders = _numberHeadersMap; - for (var entry in numberHeaders.entries) { - var key = entry.key; - var value = entry.value; - if (value != null) { - headers.set(key, value); - } - } - - // String-related headers - var stringHeaders = _stringHeadersMap; - for (var entry in stringHeaders.entries) { - var key = entry.key; - var value = entry.value; - if (value != null) { - headers.set(key, value); - } - } - - // List-related headers - var listStringHeaders = _listStringHeadersMap; - for (var entry in listStringHeaders.entries) { - var key = entry.key; - var value = entry.value; - if (value != null) { - headers.set(key, value); - } - } - - // Uri-related headers - var uriHeaders = _uriHeadersMap; - for (var entry in uriHeaders.entries) { - var key = entry.key; - var value = entry.value; - if (value != null) { - headers.set(key, value.toString()); - } - } - - // TypedHeader-related headers - var typedHeaders = _typedHeadersMap; - for (var entry in typedHeaders.entries) { - var key = entry.key; - var value = entry.value; - if (value != null) { - headers.set(key, value.toHeaderString()); - } - } - - // Set custom headers - for (var entry in custom.entries) { - headers.set(entry.key, entry.value); - } - } - /// Convert headers to a map @override Map toMap() { diff --git a/lib/src/message/response.dart b/lib/src/message/response.dart index 9049088..f6d307d 100644 --- a/lib/src/message/response.dart +++ b/lib/src/message/response.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:relic/src/extensions/http_response_extension.dart'; + import '../body/body.dart'; import '../headers/headers.dart'; import 'message.dart'; @@ -327,13 +329,7 @@ class Response extends Message { httpResponse.statusCode = statusCode; // Apply all headers to the response. - headers.applyHeaders(httpResponse); - - // Apply transfer encoding headers and content length to the response. - body.applyHeaders( - httpResponse, - transferEncoding: headers.transferEncoding, - ); + httpResponse.applyHeaders(headers, body); return httpResponse .addStream(body.read()) From f8c761928cae83c7bfa903057fa7c14d5a0c0b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Thu, 2 Jan 2025 17:00:46 +0100 Subject: [PATCH 07/10] refactor: MimeType constants and body check improvements --- lib/src/body/body.dart | 2 +- lib/src/body/types/body_type.dart | 25 +++++++++++--- lib/src/body/types/mime_type.dart | 23 +++++-------- .../extensions/http_response_extension.dart | 33 ++++++++++--------- lib/src/static/static_handler.dart | 2 +- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/lib/src/body/body.dart b/lib/src/body/body.dart index 233b06e..2f78a24 100644 --- a/lib/src/body/body.dart +++ b/lib/src/body/body.dart @@ -99,7 +99,7 @@ class Body { factory Body.fromData( Uint8List body, { Encoding? encoding, - MimeType mimeType = MimeType.binary, + MimeType mimeType = MimeType.octetStream, }) { return Body._( Stream.value(body), diff --git a/lib/src/body/types/body_type.dart b/lib/src/body/types/body_type.dart index e703156..6511dc8 100644 --- a/lib/src/body/types/body_type.dart +++ b/lib/src/body/types/body_type.dart @@ -29,8 +29,8 @@ class BodyType { ); /// A body type for JavaScript. - static const javaScript = BodyType( - mimeType: MimeType.javaScript, + static const javascript = BodyType( + mimeType: MimeType.javascript, encoding: utf8, ); @@ -46,9 +46,9 @@ class BodyType { encoding: utf8, ); - /// A body type for binary data. - static const binary = BodyType( - mimeType: MimeType.binary, + /// A body type for octet stream data. + static const octetStream = BodyType( + mimeType: MimeType.octetStream, ); /// A body type for PDF. @@ -61,6 +61,21 @@ class BodyType { mimeType: MimeType.rtf, ); + /// A body type for multipart form data. + static const multipartFormData = BodyType( + mimeType: MimeType.multipartFormData, + ); + + /// A body type for multipart byteranges. + static const multipartByteranges = BodyType( + mimeType: MimeType.multipartByteranges, + ); + + /// A body type for URL-encoded form data. + static const urlEncoded = BodyType( + mimeType: MimeType.urlEncoded, + ); + /// The mime type of the body. final MimeType mimeType; diff --git a/lib/src/body/types/mime_type.dart b/lib/src/body/types/mime_type.dart index 9ed39cc..537d56a 100644 --- a/lib/src/body/types/mime_type.dart +++ b/lib/src/body/types/mime_type.dart @@ -15,7 +15,7 @@ class MimeType { static const csv = MimeType('text', 'csv'); /// JavaScript mime type. - static const javaScript = MimeType('text', 'javascript'); + static const javascript = MimeType('text', 'javascript'); /// JSON mime type. static const json = MimeType('application', 'json'); @@ -25,7 +25,7 @@ class MimeType { /// Binary mime type. - static const binary = MimeType('application', 'octet-stream'); + static const octetStream = MimeType('application', 'octet-stream'); /// PDF mime type. static const pdf = MimeType('application', 'pdf'); @@ -33,12 +33,15 @@ class MimeType { /// RTF mime type. static const rtf = MimeType('application', 'rtf'); - /// Multipart mime type. - static const multipart = MimeType('multipart', 'form-data'); + /// Multipart form data mime type. + static const multipartFormData = MimeType('multipart', 'form-data'); - /// Multipart mime type. + /// Multipart byteranges mime type. static const multipartByteranges = MimeType('multipart', 'byteranges'); + /// URL-encoded form MIME type. + static const urlEncoded = MimeType('application', 'x-www-form-urlencoded'); + /// The primary type of the mime type. final String primaryType; @@ -80,13 +83,3 @@ extension ContentTypeExtension on ContentType { /// We are calling this method 'toMimeType' to avoid conflict with the 'mimeType' property. MimeType get toMimeType => MimeType(primaryType, subType); } - -/// Extension to check if a [MimeType] is not multipart/byteranges. -extension MimeTypeExtensions on MimeType { - /// Checks if the mime type is not multipart/byteranges. - bool get isMultipartByteranges { - var multipartByteranges = MimeType.multipartByteranges; - return primaryType == multipartByteranges.primaryType && - subType == multipartByteranges.subType; - } -} diff --git a/lib/src/extensions/http_response_extension.dart b/lib/src/extensions/http_response_extension.dart index b2e1633..438a0c6 100644 --- a/lib/src/extensions/http_response_extension.dart +++ b/lib/src/extensions/http_response_extension.dart @@ -1,34 +1,37 @@ import 'package:relic/relic.dart'; import 'dart:io' as io; -import 'package:relic/src/body/types/mime_type.dart'; - /// Extension for [io.HttpResponse] to apply headers and body. extension HttpResponseExtension on io.HttpResponse { /// Apply headers and body to the response. void applyHeaders(Headers headers, Body body) { - var httpHeaders = this.headers; - httpHeaders.clear(); + var responseHeaders = this.headers; + responseHeaders.clear(); // Apply headers var mappedHeaders = headers.toMap(); for (var entry in mappedHeaders.entries) { - var key = entry.key; - var value = entry.value; - httpHeaders.set(key, value); + responseHeaders.set(entry.key, entry.value); } // Set the Content-Type header based on the MIME type of the body. - httpHeaders.contentType = body.getContentType(); + responseHeaders.contentType = body.getContentType(); // If the content length is known, set it and remove the Transfer-Encoding header. - if (body.contentLength != null) { - httpHeaders - ..contentLength = body.contentLength! + var contentLength = body.contentLength; + if (contentLength != null) { + responseHeaders + ..contentLength = contentLength ..removeAll(Headers.transferEncodingHeader); return; } + // Check if the content type is multipart/byteranges. + var bodyMimeType = body.contentType?.mimeType; + bool isMultipartByteranges = + bodyMimeType?.primaryType == MimeType.multipartByteranges.primaryType && + bodyMimeType?.subType == MimeType.multipartByteranges.subType; + // Determine if chunked encoding should be applied. bool shouldEnableChunkedEncoding = statusCode >= 200 && // 204 is no content @@ -38,7 +41,7 @@ extension HttpResponseExtension on io.HttpResponse { // If the content length is not known, chunked encoding is applied. body.contentLength == null && // If the content type is not multipart/byteranges, chunked encoding is applied. - (body.contentType?.mimeType.isMultipartByteranges == false); + !isMultipartByteranges; // Prepare transfer encodings. var encodings = headers.transferEncoding?.encodings ?? []; @@ -54,15 +57,15 @@ extension HttpResponseExtension on io.HttpResponse { encodings.removeWhere((e) => e.name == TransferEncoding.identity.name); // Set Transfer-Encoding header and remove Content-Length as it is not needed for chunked encoding. - httpHeaders + responseHeaders ..set( Headers.transferEncodingHeader, encodings.map((e) => e.name).toList(), ) ..removeAll(Headers.contentLengthHeader); } else { - // Set Content-Length to 0 if chunked encoding is not enabled. - httpHeaders.contentLength = 0; + // Set Content-Length if chunked encoding is not enabled. + responseHeaders.contentLength = contentLength ?? 0; } } } diff --git a/lib/src/static/static_handler.dart b/lib/src/static/static_handler.dart index e77ed51..8f6d3bf 100644 --- a/lib/src/static/static_handler.dart +++ b/lib/src/static/static_handler.dart @@ -309,7 +309,7 @@ Response? _fileRangeResponse( file.openRead(start, end + 1).cast(), encoding: null, contentLength: (end - start) + 1, - mimeType: MimeType.binary, + mimeType: MimeType.octetStream, ); } From f4a527b565b23b077c1da648ce9a5ea58e500f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Fri, 3 Jan 2025 09:35:39 +0100 Subject: [PATCH 08/10] revert: Reverting body class to main, there no need to change it --- lib/src/body/body.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/body/body.dart b/lib/src/body/body.dart index 2f78a24..afa0f93 100644 --- a/lib/src/body/body.dart +++ b/lib/src/body/body.dart @@ -3,7 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:relic/relic.dart'; +import 'package:relic/src/body/types/body_type.dart'; import 'package:relic/src/body/types/mime_type.dart'; /// The body of a request or response. From 5f667125b29fa888b756b0567616c880ccd003a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Fri, 3 Jan 2025 09:35:58 +0100 Subject: [PATCH 09/10] test: Small test improvement --- test/relic_server_serve_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/relic_server_serve_test.dart b/test/relic_server_serve_test.dart index 0b20d2d..fcee81e 100644 --- a/test/relic_server_serve_test.dart +++ b/test/relic_server_serve_test.dart @@ -537,6 +537,8 @@ void main() { }); var response = await _get(); + expect(response.headers, + isNot(contains(HttpHeaders.transferEncodingHeader))); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); From 135bea55f842671c457da9fb328a62dd72c1f099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaudio=20Ku=C3=A7aj?= Date: Fri, 3 Jan 2025 13:23:31 +0100 Subject: [PATCH 10/10] refactor: Small improvement on how content-length and transfer encoding are set to the response headers --- .../extensions/http_response_extension.dart | 31 +++++++------- test/message/body_test.dart | 42 ++++++++++++++++++- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/lib/src/extensions/http_response_extension.dart b/lib/src/extensions/http_response_extension.dart index 438a0c6..1aa93ee 100644 --- a/lib/src/extensions/http_response_extension.dart +++ b/lib/src/extensions/http_response_extension.dart @@ -38,8 +38,6 @@ extension HttpResponseExtension on io.HttpResponse { statusCode != 204 && // 304 is not modified statusCode != 304 && - // If the content length is not known, chunked encoding is applied. - body.contentLength == null && // If the content type is not multipart/byteranges, chunked encoding is applied. !isMultipartByteranges; @@ -52,20 +50,21 @@ extension HttpResponseExtension on io.HttpResponse { isChunked = true; } - if (isChunked) { - // Remove conflicting 'identity' encoding if present. - encodings.removeWhere((e) => e.name == TransferEncoding.identity.name); - - // Set Transfer-Encoding header and remove Content-Length as it is not needed for chunked encoding. - responseHeaders - ..set( - Headers.transferEncodingHeader, - encodings.map((e) => e.name).toList(), - ) - ..removeAll(Headers.contentLengthHeader); - } else { - // Set Content-Length if chunked encoding is not enabled. - responseHeaders.contentLength = contentLength ?? 0; + if (!isChunked) { + // Set Content-Length to 0 if chunked encoding is not enabled. + responseHeaders.contentLength = 0; + return; } + + // Remove conflicting 'identity' encoding if present. + encodings.removeWhere((e) => e.name == TransferEncoding.identity.name); + + // Set Transfer-Encoding header and remove Content-Length as it is not needed for chunked encoding. + responseHeaders + ..set( + Headers.transferEncodingHeader, + encodings.map((e) => e.name).toList(), + ) + ..removeAll(Headers.contentLengthHeader); } } diff --git a/test/message/body_test.dart b/test/message/body_test.dart index db47ebf..00953b5 100644 --- a/test/message/body_test.dart +++ b/test/message/body_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; @@ -159,6 +160,45 @@ void main() { expect(response.headers['transfer-encoding'], isNull); expect(response.bodyBytes, equals([1, 2, 3, 4])); }); + + test( + 'with an unknown content length and chunked encoding ' + 'then content-length should be set to 0 and it should throw an HttpException ' + 'with a message that states the content size exceeds the specified contentLength', + () async { + Completer completer = Completer(); + + unawaited(runZonedGuarded( + () async { + await _scheduleServer( + (_) => Response.ok( + body: Body.fromDataStream( + Stream.fromIterable([ + Uint8List.fromList([1, 2, 3, 4]) + ]), + mimeType: MimeType.multipartByteranges, + ), + headers: Headers.response(), + ), + ); + + await _get(); + }, + (error, stackTrace) { + if (completer.isCompleted) return; + completer.complete(error); + }, + )); + + var error = await completer.future; + + expect(error, isA()); + expect( + error.toString(), + contains('Content size exceeds specified contentLength'), + ); + }, + ); }); } @@ -173,7 +213,7 @@ Future _scheduleServer( assert(_server == null); _server = await relic_server.serve( handler, - 'localhost', + RelicAddress.fromHostname('localhost'), 0, securityContext: securityContext, );