Add Message.isEmpty. (#62)
This also fixes some logic bugs with the previous release. In
particular, it doesn't send Content-Length headers along with chunked
transfer encodings, and it allows users to override the automatic
Content-Length.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3964e15..a980cf5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+## 0.6.7
+
+* Add `Request.isEmpty` and `Response.isEmpty` getters which indicate whether a
+ message has an empty body.
+
+* Don't automatically generate `Content-Length` headers on messages where they
+ may not be allowed.
+
+* User-specified `Content-Length` headers now always take precedence over
+ automatically-generated headers.
+
## 0.6.6
* Allow `List<int>`s to be passed as request or response bodies.
diff --git a/lib/shelf_io.dart b/lib/shelf_io.dart
index fa120b3..7019d2e 100644
--- a/lib/shelf_io.dart
+++ b/lib/shelf_io.dart
@@ -150,9 +150,7 @@
}
// Work around sdk#27660.
- if (response.contentLength == 0) {
- httpResponse.headers.chunkedTransferEncoding = false;
- }
+ if (response.isEmpty) httpResponse.headers.chunkedTransferEncoding = false;
return httpResponse
.addStream(response.read())
diff --git a/lib/src/body.dart b/lib/src/body.dart
index 77c3d1b..448cab8 100644
--- a/lib/src/body.dart
+++ b/lib/src/body.dart
@@ -23,11 +23,12 @@
/// encoding was used.
final Encoding encoding;
- /// The length of the stream returned by [read], or `null` if that can't be
- /// determined efficiently.
- final int contentLength;
+ /// If `true`, the stream returned by [read] will be empty.
+ ///
+ /// This may have false negatives, but it won't have false positives.
+ final bool isEmpty;
- Body._(this._stream, this.encoding, this.contentLength);
+ Body._(this._stream, this.encoding, this.isEmpty);
/// Converts [body] to a byte stream and wraps it in a [Body].
///
@@ -38,25 +39,23 @@
if (body is Body) return body;
Stream<List<int>> stream;
- int contentLength;
+ var isEmpty = false;
if (body == null) {
- contentLength = 0;
+ isEmpty = true;
stream = new Stream.fromIterable([]);
} else if (body is String) {
+ isEmpty = body.isEmpty;
if (encoding == null) {
var encoded = UTF8.encode(body);
// If the text is plain ASCII, don't modify the encoding. This means
- // that an encoding of "text/plain" will stay put.
+ // that an encoding of "text/plain" without a charset will stay put.
if (!_isPlainAscii(encoded, body.length)) encoding = UTF8;
- contentLength = encoded.length;
stream = new Stream.fromIterable([encoded]);
} else {
- var encoded = encoding.encode(body);
- contentLength = encoded.length;
- stream = new Stream.fromIterable([encoded]);
+ stream = new Stream.fromIterable([encoding.encode(body)]);
}
} else if (body is List) {
- contentLength = body.length;
+ isEmpty = body.isEmpty;
stream = new Stream.fromIterable([DelegatingList.typed(body)]);
} else if (body is Stream) {
stream = DelegatingStream.typed(body);
@@ -65,7 +64,7 @@
'Stream.');
}
- return new Body._(stream, encoding, contentLength);
+ return new Body._(stream, encoding, isEmpty);
}
/// Returns whether [bytes] is plain ASCII.
diff --git a/lib/src/message.dart b/lib/src/message.dart
index 2059b9a..df770fe 100644
--- a/lib/src/message.dart
+++ b/lib/src/message.dart
@@ -43,6 +43,11 @@
/// This can be read via [read] or [readAsString].
final Body _body;
+ /// If `true`, the stream returned by [read] won't emit any bytes.
+ ///
+ /// This may have false negatives, but it won't have false positives.
+ bool get isEmpty => _body.isEmpty;
+
/// Creates a new [Message].
///
/// [body] is the response body. It may be either a [String], a [List<int>], a
@@ -71,7 +76,6 @@
/// If not set, `null`.
int get contentLength {
if (_contentLengthCache != null) return _contentLengthCache;
- if (_body.contentLength != null) return _body.contentLength;
if (!headers.containsKey('content-length')) return null;
_contentLengthCache = int.parse(headers['content-length']);
return _contentLengthCache;
@@ -146,12 +150,9 @@
Map<String, String> headers, Body body) {
var sameEncoding = _sameEncoding(headers, body);
if (sameEncoding) {
- if (body.contentLength == null ||
- getHeader(headers, 'content-length') ==
- body.contentLength.toString()) {
+ if (!body.isEmpty || hasHeader(headers, 'content-length')) {
return headers ?? const ShelfUnmodifiableMap.empty();
- } else if (body.contentLength == 0 &&
- (headers == null || headers.isEmpty)) {
+ } else if (body.isEmpty && (headers == null || headers.isEmpty)) {
return _defaultHeaders;
}
}
@@ -171,8 +172,14 @@
}
}
- if (body.contentLength != null) {
- newHeaders['content-length'] = body.contentLength.toString();
+ // Only add a content-length header if the length is 0 and there isn't already
+ // a content-length set. Content-length may not be sent with a chunked
+ // transfer encoding, and HEAD requests may send a non-zero content-length
+ // along with an empty body.
+ //
+ // See https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.4.
+ if (body.isEmpty) {
+ newHeaders.putIfAbsent('content-length', () => '0');
}
return newHeaders;
diff --git a/lib/src/util.dart b/lib/src/util.dart
index a59e912..fcf66d8 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -62,3 +62,17 @@
}
return null;
}
+
+/// Returns whether [headers] contains a header with the given [name].
+///
+/// This works even if [headers] is `null`, or if it's not yet a
+/// case-insensitive map.
+bool hasHeader(Map<String, String> headers, String name) {
+ if (headers == null) return false;
+ if (headers is ShelfUnmodifiableMap) return headers.containsKey(name);
+
+ for (var key in headers.keys) {
+ if (equalsIgnoreAsciiCase(key, name)) return true;
+ }
+ return false;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 0f48cb5..72b0877 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: shelf
-version: 0.6.6
+version: 0.6.7
author: Dart Team <misc@dartlang.org>
description: Web Server Middleware for Dart
homepage: https://github.com/dart-lang/shelf
diff --git a/test/message_test.dart b/test/message_test.dart
index adc7f63..4175455 100644
--- a/test/message_test.dart
+++ b/test/message_test.dart
@@ -164,39 +164,40 @@
expect(request.contentLength, 0);
});
- test("comes from a byte body", () {
- var request = _createMessage(body: [1, 2, 3]);
- expect(request.contentLength, 3);
+ test("is 0 with an empty byte body", () {
+ var request = _createMessage(body: []);
+ expect(request.contentLength, 0);
});
- test("comes from a string body", () {
- var request = _createMessage(body: 'foobar');
- expect(request.contentLength, 6);
+ test("is 0 with an empty string body", () {
+ var request = _createMessage(body: '');
+ expect(request.contentLength, 0);
});
- test("is set based on byte length for a string body", () {
- var request = _createMessage(body: 'fööbär');
- expect(request.contentLength, 9);
-
- request = _createMessage(body: 'fööbär', encoding: LATIN1);
- expect(request.contentLength, 6);
- });
-
- test("is null for a stream body", () {
+ test("is null for an empty stream body", () {
var request = _createMessage(body: new Stream.empty());
expect(request.contentLength, isNull);
});
+ test("is null for a non-empty byte body", () {
+ var request = _createMessage(body: [1, 2, 3]);
+ expect(request.contentLength, isNull);
+ });
+
+ test("is null for a non-empty string body", () {
+ var request = _createMessage(body: "foo");
+ expect(request.contentLength, isNull);
+ });
+
test("uses the content-length header for a stream body", () {
var request = _createMessage(
body: new Stream.empty(), headers: {'content-length': '42'});
expect(request.contentLength, 42);
});
- test("real body length takes precedence over content-length header", () {
- var request = _createMessage(
- body: [1, 2, 3], headers: {'content-length': '42'});
- expect(request.contentLength, 3);
+ test("content-length header takes precedence over an empty body", () {
+ var request = _createMessage(headers: {'content-length': '42'});
+ expect(request.contentLength, 42);
});
});
diff --git a/test/shelf_io_test.dart b/test/shelf_io_test.dart
index 3e644e5..c555b19 100644
--- a/test/shelf_io_test.dart
+++ b/test/shelf_io_test.dart
@@ -335,6 +335,29 @@
});
});
+ group("doesn't use a chunked transfer-encoding", () {
+ test("for a response with an empty body", () {
+ _scheduleServer((request) => new Response.notModified());
+
+ return _scheduleGet().then((response) {
+ expect(response.headers, isNot(contains('transfer-encoding')));
+ expect(response.headers, containsPair('content-length', '0'));
+ });
+ });
+
+ test("for a response with an empty body and a non-empty content-length",
+ () {
+ _scheduleServer((request) {
+ return new Response.ok(null, headers: {'content-length': '42'});
+ });
+
+ return _scheduleHead().then((response) {
+ expect(response.headers, isNot(contains('transfer-encoding')));
+ expect(response.headers, containsPair('content-length', '42'));
+ });
+ });
+ });
+
test('respects the "shelf.io.buffer_output" context parameter', () {
var controller = new StreamController<String>();
_scheduleServer((request) {
@@ -385,6 +408,13 @@
() => http.get('http://localhost:$_serverPort/', headers: headers));
}
+Future<http.Response> _scheduleHead({Map<String, String> headers}) {
+ if (headers == null) headers = {};
+
+ return schedule/*<Future<http.Response>>*/(
+ () => http.head('http://localhost:$_serverPort/', headers: headers));
+}
+
Future<http.StreamedResponse> _schedulePost(
{Map<String, String> headers, String body}) {
return schedule/*<Future<http.StreamedResponse>>*/(() {