Skip to content

Commit

Permalink
Accept List<int> message bodies.
Browse files Browse the repository at this point in the history
This also automatically sets the content-length header when possible,
and works around dart-lang/sdk#27660.

Closes #60
  • Loading branch information
nex3 committed Oct 24, 2016
1 parent da66394 commit 2fe81e6
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 62 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,16 @@
## 0.6.6

* Allow `List<int>`s to be passed as request or response bodies.

* Requests and responses now automatically set `Content-Length` headers when the
body length is known ahead of time.

* Work around [sdk#27660][] by manually setting
`HttpResponse.chunkedTransferEncoding` to `false` for requests known to have
no content.

[sdk#27660]: https://github.com/dart-lang/sdk/issues/27660

## 0.6.5+3

* Improve the documentation of `logRequests()`.
Expand Down
5 changes: 5 additions & 0 deletions lib/shelf_io.dart
Expand Up @@ -149,6 +149,11 @@ Future _writeResponse(Response response, HttpResponse httpResponse) {
httpResponse.headers.date = new DateTime.now().toUtc();
}

// Work around sdk#27660.
if (response.contentLength == 0) {
httpResponse.headers.chunkedTransferEncoding = false;
}

return httpResponse
.addStream(response.read())
.then((_) => httpResponse.close());
Expand Down
56 changes: 48 additions & 8 deletions lib/src/body.dart
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:async/async.dart';

/// The body of a request or response.
Expand All @@ -18,31 +19,70 @@ class Body {
/// This will be `null` after [read] is called.
Stream<List<int>> _stream;

Body._(this._stream);
/// The encoding used to encode the stream returned by [read], or `null` if no
/// 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;

Body._(this._stream, this.encoding, this.contentLength);

/// Converts [body] to a byte stream and wraps it in a [Body].
///
/// [body] may be either a [Body], a [String], a [Stream<List<int>>], or
/// `null`. If it's a [String], [encoding] will be used to convert it to a
/// [Stream<List<int>>].
/// [body] may be either a [Body], a [String], a [List<int>], a
/// [Stream<List<int>>], or `null`. If it's a [String], [encoding] will be
/// used to convert it to a [Stream<List<int>>].
factory Body(body, [Encoding encoding]) {
if (encoding == null) encoding = UTF8;

if (body is Body) return body;

Stream<List<int>> stream;
int contentLength;
if (body == null) {
contentLength = 0;
stream = new Stream.fromIterable([]);
} else if (body is String) {
stream = new Stream.fromIterable([encoding.encode(body)]);
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.
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]);
}
} else if (body is List) {
contentLength = body.length;
stream = new Stream.fromIterable([DelegatingList.typed(body)]);
} else if (body is Stream) {
stream = DelegatingStream.typed(body);
} else {
throw new ArgumentError('Response body "$body" must be a String or a '
'Stream.');
}

return new Body._(stream);
return new Body._(stream, encoding, contentLength);
}

/// Returns whether [bytes] is plain ASCII.
///
/// [codeUnits] is the number of code units in the original string.
static bool _isPlainAscii(List<int> bytes, int codeUnits) {
// Most non-ASCII code units will produce multiple bytes and make the text
// longer.
if (bytes.length != codeUnits) return false;

for (var byte in bytes) {
// Non-ASCII code units between U+0080 and U+009F produce 8-bit
// characters with the high bit set.
if (byte & 0x80 != 0) return false;
}

return true;
}

/// Returns a [Stream] representing the body.
Expand Down
72 changes: 56 additions & 16 deletions lib/src/message.dart
Expand Up @@ -13,6 +13,11 @@ import 'util.dart';

Body getBody(Message message) => message._body;

/// The default set of headers for a message created with no body and no
/// explicit headers.
final _defaultHeaders = new ShelfUnmodifiableMap<String>(
{"content-length": "0"}, ignoreKeyCase: true);

/// Represents logic shared between [Request] and [Response].
abstract class Message {
/// The HTTP headers.
Expand Down Expand Up @@ -40,7 +45,7 @@ abstract class Message {

/// Creates a new [Message].
///
/// [body] is the response body. It may be either a [String], a
/// [body] is the response body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
Expand All @@ -52,17 +57,21 @@ abstract class Message {
/// Content-Type header, it will be set to "application/octet-stream".
Message(body, {Encoding encoding, Map<String, String> headers,
Map<String, Object> context})
: this._body = new Body(body, encoding),
this.headers = new ShelfUnmodifiableMap<String>(
_adjustHeaders(headers, encoding), ignoreKeyCase: true),
this.context = new ShelfUnmodifiableMap<Object>(context,
: this._(new Body(body, encoding), headers, context);

Message._(Body body, Map<String, String> headers, Map<String, Object> context)
: _body = body,
headers = new ShelfUnmodifiableMap<String>(
_adjustHeaders(headers, body), ignoreKeyCase: true),
context = new ShelfUnmodifiableMap<Object>(context,
ignoreKeyCase: false);

/// The contents of the content-length field in [headers].
///
/// 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;
Expand Down Expand Up @@ -134,17 +143,48 @@ abstract class Message {
///
/// Returns a new map without modifying [headers].
Map<String, String> _adjustHeaders(
Map<String, String> headers, Encoding encoding) {
if (headers == null) headers = const {};
if (encoding == null) return headers;

headers = new CaseInsensitiveMap.from(headers);
if (headers['content-type'] == null) {
return addHeader(headers, 'content-type',
'application/octet-stream; charset=${encoding.name}');
Map<String, String> headers, Body body) {
var sameEncoding = _sameEncoding(headers, body);
if (sameEncoding) {
if (body.contentLength == null ||
getHeader(headers, 'content-length') ==
body.contentLength.toString()) {
return headers ?? const ShelfUnmodifiableMap.empty();
} else if (body.contentLength == 0 &&
(headers == null || headers.isEmpty)) {
return _defaultHeaders;
}
}

var newHeaders = headers == null
? new CaseInsensitiveMap<String>()
: new CaseInsensitiveMap<String>.from(headers);

if (!sameEncoding) {
if (newHeaders['content-type'] == null) {
newHeaders['content-type'] =
'application/octet-stream; charset=${body.encoding.name}';
} else {
var contentType = new MediaType.parse(newHeaders['content-type'])
.change(parameters: {'charset': body.encoding.name});
newHeaders['content-type'] = contentType.toString();
}
}

if (body.contentLength != null) {
newHeaders['content-length'] = body.contentLength.toString();
}

var contentType = new MediaType.parse(headers['content-type']).change(
parameters: {'charset': encoding.name});
return addHeader(headers, 'content-type', contentType.toString());
return newHeaders;
}

/// Returns whether [headers] declares the same encoding as [body].
bool _sameEncoding(Map<String, String> headers, Body body) {
if (body.encoding == null) return true;

var contentType = getHeader(headers, 'content-type');
if (contentType == null) return false;

var charset = new MediaType.parse(contentType).parameters['charset'];
return Encoding.getByName(charset) == body.encoding;
}
12 changes: 6 additions & 6 deletions lib/src/request.dart
Expand Up @@ -96,10 +96,10 @@ class Request extends Message {
/// and [url] to `requestedUri.path` without the initial `/`. If only one is
/// passed, the other will be inferred.
///
/// [body] is the request body. It may be either a [String], a
/// [Stream<List<int>>], or `null` to indicate no body.
/// If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8.
/// [body] is the request body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. The default
/// encoding is UTF-8.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
Expand Down Expand Up @@ -192,8 +192,8 @@ class Request extends Message {
/// [Request]. All other context and header values from the [Request] will be
/// included in the copied [Request] unchanged.
///
/// [body] is the request body. It may be either a [String] or a
/// [Stream<List<int>>].
/// [body] is the request body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body.
///
/// [path] is used to update both [handlerPath] and [url]. It's designed for
/// routing middleware, and represents the path from the current handler to
Expand Down
44 changes: 22 additions & 22 deletions lib/src/response.dart
Expand Up @@ -43,7 +43,7 @@ class Response extends Message {
///
/// This indicates that the request has succeeded.
///
/// [body] is the response body. It may be either a [String], a
/// [body] is the response body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
Expand All @@ -62,7 +62,7 @@ class Response extends Message {
/// URI. [location] is that URI; it can be either a [String] or a [Uri]. It's
/// automatically set as the Location header in [headers].
///
/// [body] is the response body. It may be either a [String], a
/// [body] is the response body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
Expand All @@ -81,7 +81,7 @@ class Response extends Message {
/// URI. [location] is that URI; it can be either a [String] or a [Uri]. It's
/// automatically set as the Location header in [headers].
///
/// [body] is the response body. It may be either a [String], a
/// [body] is the response body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
Expand All @@ -101,7 +101,7 @@ class Response extends Message {
/// [String] or a [Uri]. It's automatically set as the Location header in
/// [headers].
///
/// [body] is the response body. It may be either a [String], a
/// [body] is the response body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
/// UTF-8.
Expand Down Expand Up @@ -141,10 +141,10 @@ class Response extends Message {
///
/// This indicates that the server is refusing to fulfill the request.
///
/// [body] is the response body. It may be a [String], a [Stream<List<int>>],
/// or `null`. If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8. If it's `null` or not
/// passed, a default error message is used.
/// [body] is the response body. It may be a [String], a [List<int>], a
/// [Stream<List<int>>], or `null`. If it's a [String], [encoding] is used to
/// encode it to a [Stream<List<int>>]. The default encoding is UTF-8. If it's
/// `null` or not passed, a default error message is used.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
Expand All @@ -161,10 +161,10 @@ class Response extends Message {
/// This indicates that the server didn't find any resource matching the
/// requested URI.
///
/// [body] is the response body. It may be a [String], a [Stream<List<int>>],
/// or `null`. If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8. If it's `null` or not
/// passed, a default error message is used.
/// [body] is the response body. It may be a [String], a [List<int>], a
/// [Stream<List<int>>], or `null`. If it's a [String], [encoding] is used to
/// encode it to a [Stream<List<int>>]. The default encoding is UTF-8. If it's
/// `null` or not passed, a default error message is used.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
Expand All @@ -181,10 +181,10 @@ class Response extends Message {
/// This indicates that the server had an internal error that prevented it
/// from fulfilling the request.
///
/// [body] is the response body. It may be a [String], a [Stream<List<int>>],
/// or `null`. If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8. If it's `null` or not
/// passed, a default error message is used.
/// [body] is the response body. It may be a [String], a [List<int>], a
/// [Stream<List<int>>], or `null`. If it's a [String], [encoding] is used to
/// encode it to a [Stream<List<int>>]. The default encoding is UTF-8. If it's
/// `null` or not passed, a default error message is used.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
Expand All @@ -200,10 +200,10 @@ class Response extends Message {
///
/// [statusCode] must be greater than or equal to 100.
///
/// [body] is the response body. It may be either a [String], a
/// [Stream<List<int>>], or `null` to indicate no body.
/// If it's a [String], [encoding] is used to encode it to a
/// [Stream<List<int>>]. The default encoding is UTF-8.
/// [body] is the response body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
/// [encoding] is used to encode it to a [Stream<List<int>>]. The default
/// encoding is UTF-8.
///
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
Expand All @@ -229,8 +229,8 @@ class Response extends Message {
/// All other context and header values from the [Response] will be included
/// in the copied [Response] unchanged.
///
/// [body] is the request body. It may be either a [String] or a
/// [Stream<List<int>>].
/// [body] is the request body. It may be either a [String], a [List<int>], a
/// [Stream<List<int>>], or `null` to indicate no body.
Response change(
{Map<String, String> headers, Map<String, Object> context, body}) {
headers = updateMap(this.headers, headers);
Expand Down
4 changes: 4 additions & 0 deletions lib/src/shelf_unmodifiable_map.dart
Expand Up @@ -4,6 +4,7 @@

import 'dart:collection';

import 'package:collection/collection.dart';
import 'package:http_parser/http_parser.dart';

/// A simple wrapper over [UnmodifiableMapView] which avoids re-wrapping itself.
Expand Down Expand Up @@ -42,6 +43,9 @@ class ShelfUnmodifiableMap<V> extends UnmodifiableMapView<String, V> {
return new ShelfUnmodifiableMap<V>._(source, ignoreKeyCase);
}

/// Returns an empty [ShelfUnmodifiableMap].
const factory ShelfUnmodifiableMap.empty() = _EmptyShelfUnmodifiableMap<V>;

ShelfUnmodifiableMap._(Map<String, V> source, this._ignoreKeyCase)
: super(source);
}
Expand Down

0 comments on commit 2fe81e6

Please sign in to comment.