diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 56a70f18..2cf90842 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -1,13 +1,19 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -31,7 +37,7 @@
-
+
@@ -41,61 +47,42 @@
-
-
+
+
-
-
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
@@ -106,7 +93,7 @@
-
+
@@ -115,15 +102,44 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -139,28 +155,6 @@
- dump
- void li
- _onError
- Unhandled exc
- handleAn
- Tom Fo
- transform(
- grab(
- json(
- cons
- const
- call
- use(
- value
- whenI
- before
- afterProcessed
- onco
- StreamC
- onCo
- fatalErrorStream
- handleReq
handleRequest(
_Lockabl
takeBy
@@ -169,6 +163,28 @@
sendRes
replaceAll
noSuch
+ addStre
+ finalizers
+ write
+ deprec
+ addStr
+ addSt
+ cancell
+ corr
+ buffer.
+ if (_isClosed)
+ if (_isClosed
+ createRespo
+ injectSerializer
+ encoders
+ addAll
+ res.enco
+ encodingNa
+ addErr
+ close()
+ .complete(
+ isCompl
+ streamFil`
_isClosed
@@ -182,6 +198,7 @@
after
fatalErrorStream
onController
+ if (_isClosed && !_useStream)
C:\Users\thosa\Source\Angel\framework\lib
@@ -209,36 +226,39 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -284,6 +304,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -356,20 +390,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -402,7 +422,7 @@
-
+
@@ -489,6 +509,16 @@
false
+
+
+
+
+
+
+
+
+
+
@@ -550,29 +580,31 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -615,7 +647,8 @@
-
+
+
1481237183504
@@ -813,43 +846,50 @@
1501778536483
-
+
+ 1501782730507
+
+
+
+ 1501782730507
+
+
-
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
@@ -869,15 +909,14 @@
-
+
-
-
+
-
+
@@ -886,6 +925,7 @@
+
@@ -896,7 +936,6 @@
-
@@ -921,55 +960,14 @@
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1012,13 +1010,6 @@
-
-
-
-
-
-
-
@@ -1030,7 +1021,6 @@
-
@@ -1038,7 +1028,6 @@
-
@@ -1046,7 +1035,6 @@
-
@@ -1054,23 +1042,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1094,9 +1065,6 @@
-
-
-
@@ -1104,9 +1072,6 @@
-
-
-
@@ -1114,7 +1079,6 @@
-
@@ -1122,7 +1086,6 @@
-
@@ -1130,9 +1093,6 @@
-
-
-
@@ -1140,9 +1100,6 @@
-
-
-
@@ -1150,7 +1107,6 @@
-
@@ -1158,7 +1114,6 @@
-
@@ -1167,30 +1122,11 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1203,34 +1139,16 @@
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
@@ -1239,7 +1157,6 @@
-
@@ -1247,7 +1164,6 @@
-
@@ -1255,7 +1171,6 @@
-
@@ -1263,7 +1178,6 @@
-
@@ -1271,63 +1185,19 @@
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -1350,6 +1220,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1358,5 +1314,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0fec777b..b44c2ec5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# 1.0.9
+* Closed [#161](https://github.com/angel-dart/framework/issues/161). `addCreated`/`addUpdatedAt` no longer
+crash when `serialize` is `false`.
+* Added an explicit on `charcode`, `path`, and others. Resolves
+[#160](https://github.com/angel-dart/framework/issues/160).
+* `ResponseContext` now implements `StreamSink`, so data can be streamed directly to the
+underlying response.
+* You can now inject `encoders` into a `ResponseContext`, which takes care of `Accept-Encoding`.
+This will ultimately replace `package:angel_compress`.
+Resolves [#159](https://github.com/angel-dart/framework/issues/159).
+
# 1.0.8
* Changed `req.query` to use a modifiable Map if the body has not parsed. Resolves
[#157](https://github.com/angel-dart/framework/issues/157).
diff --git a/lib/hooks.dart b/lib/hooks.dart
index 4e165c47..04975215 100644
--- a/lib/hooks.dart
+++ b/lib/hooks.dart
@@ -220,11 +220,11 @@ HookedServiceEventListener disable([provider]) {
///
/// Default key: `createdAt`
HookedServiceEventListener addCreatedAt(
- {assign(obj, String now), String key, bool serialize: true}) {
+ {assign(obj, now), String key, bool serialize: true}) {
var name = key?.isNotEmpty == true ? key : 'createdAt';
return (HookedServiceEvent e) async {
- _assign(obj, String now) {
+ _assign(obj, now) {
if (assign != null)
return assign(obj, now);
else if (obj is Map)
@@ -269,11 +269,11 @@ HookedServiceEventListener addUpatedAt({
///
/// Default key: `updatedAt`
HookedServiceEventListener addUpdatedAt(
- {assign(obj, String now), String key, bool serialize: true}) {
+ {assign(obj, now), String key, bool serialize: true}) {
var name = key?.isNotEmpty == true ? key : 'updatedAt';
return (HookedServiceEvent e) async {
- _assign(obj, String now) {
+ _assign(obj, now) {
if (assign != null)
return assign(obj, now);
else if (obj is Map)
diff --git a/lib/src/http/response_context.dart b/lib/src/http/response_context.dart
index 3ce2b4f7..034f1d5c 100644
--- a/lib/src/http/response_context.dart
+++ b/lib/src/http/response_context.dart
@@ -9,6 +9,7 @@ import 'package:json_god/json_god.dart' as god;
import 'package:mime/mime.dart';
import 'server.dart' show Angel;
import 'controller.dart';
+import 'request_context.dart';
final RegExp _contentType =
new RegExp(r'([^/\n]+)\/\s*([^;\n]+)\s*(;\s*charset=([^$;\n]+))?');
@@ -19,11 +20,14 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
typedef String ResponseSerializer(data);
/// A convenience wrapper around an outgoing HTTP request.
-class ResponseContext implements StringSink {
- final _LockableBytesBuilder _buffer = new _LockableBytesBuilder();
- final Map _headers = {HttpHeaders.SERVER: 'angel'};
+class ResponseContext implements StreamSink>, StringSink {
final Map properties = {};
- bool _isOpen = true, _isClosed = false;
+ final BytesBuilder _buffer = new _LockableBytesBuilder();
+ final Map _headers = {HttpHeaders.SERVER: 'angel'};
+ final RequestContext _correspondingRequest;
+
+ Completer _done;
+ bool _isOpen = true, _isClosed = false, _useStream = false;
int _statusCode = 200;
/// The [Angel] instance that is sending a response.
@@ -35,6 +39,17 @@ class ResponseContext implements StringSink {
/// Any and all cookies to be sent to the user.
final List cookies = [];
+ /// A set of [Converter] objects that can be used to encode response data.
+ ///
+ /// At most one encoder will ever be used to convert data.
+ final Map, List>> encoders = {};
+
+ /// Points to the [RequestContext] corresponding to this response.
+ RequestContext get correspondingRequest => _correspondingRequest;
+
+ @override
+ Future get done => (_done ?? new Completer()).future;
+
/// Headers that will be sent to the user.
Map get headers {
/// If the response is closed, then this getter will return an immutable `Map`.
@@ -74,8 +89,8 @@ class ResponseContext implements StringSink {
/// A set of UTF-8 encoded bytes that will be written to the response.
BytesBuilder get buffer => _buffer;
- /// Sets the status code to be sent with this response.
- @Deprecated('Please use `statusCode=` instead.')
+ /// Please use `statusCode=` instead.'
+ @deprecated
void status(int code) {
statusCode = code;
}
@@ -110,7 +125,7 @@ class ResponseContext implements StringSink {
headers[HttpHeaders.CONTENT_TYPE] = contentType.toString();
}
- ResponseContext(this.io, this.app);
+ ResponseContext(this.io, this.app, [this._correspondingRequest]);
/// Set this to true if you will manually close the response.
///
@@ -127,15 +142,32 @@ class ResponseContext implements StringSink {
'attachment; filename="${filename ?? file.path}"';
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
headers[HttpHeaders.CONTENT_LENGTH] = file.lengthSync().toString();
- buffer.add(await file.readAsBytes());
- end();
+
+ if (_useStream) {
+ await file.openRead().pipe(this);
+ } else {
+ buffer.add(await file.readAsBytes());
+ end();
+ }
}
/// Prevents more data from being written to the response, and locks it entire from further editing.
- void close() {
- _buffer._lock();
+ Future close() {
+ var f = new Future.value();
+
+ if (_useStream) {
+ _useStream = false;
+ _buffer?.clear();
+ f = io.close();
+ } else if (_buffer is _LockableBytesBuilder) {
+ (_buffer as _LockableBytesBuilder)._lock();
+ }
+
_isOpen = false;
_isClosed = true;
+
+ if (_done?.isCompleted == false) _done.complete();
+ return f;
}
/// Prevents further request handlers from running on the response, except for response finalizers.
@@ -271,7 +303,9 @@ class ResponseContext implements StringSink {
/// Copies a file's contents into the response buffer.
Future sendFile(File file,
- {int chunkSize, int sleepMs: 0, bool resumable: true}) async {
+ {@deprecated int chunkSize,
+ @deprecated int sleepMs: 0,
+ @deprecated bool resumable: true}) async {
if (_isClosed) throw _closed();
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
@@ -297,29 +331,120 @@ class ResponseContext implements StringSink {
/// Streams a file to this response.
///
+ // ignore: deprecated_member_use
/// You can optionally transform the file stream with a [codec].
Future streamFile(File file,
- {int chunkSize,
- int sleepMs: 0,
- bool resumable: true,
- Codec, List> codec}) async {
+ {@deprecated int chunkSize,
+ @deprecated int sleepMs: 0,
+ @deprecated bool resumable: true,
+
+ /// Use [encoders] instead of manually specifying a codec.
+ @deprecated Codec, List> codec}) async {
if (_isClosed) throw _closed();
headers[HttpHeaders.CONTENT_TYPE] = lookupMimeType(file.path);
- end();
- willCloseItself = true;
- Stream stream = codec != null
- ? file.openRead().transform(codec.encoder)
- : file.openRead();
- await stream.pipe(io);
+ // ignore: deprecated_member_use
+ if (codec != null) {
+ end();
+ willCloseItself = true;
+
+ // ignore: deprecated_member_use
+ Stream stream = codec != null
+ // ignore: deprecated_member_use
+ ? file.openRead().transform(codec.encoder)
+ : file.openRead();
+ await stream.pipe(io);
+ } else {
+ await file.openRead().pipe(this);
+ }
+ }
+
+ @override
+ void add(List data) {
+ if (_isClosed && !_useStream)
+ throw _closed();
+ else if (_useStream)
+ io.add(data);
+ else
+ buffer.add(data);
+ }
+
+ /// Adds a stream directly the underlying dart:[io] response.
+ ///
+ /// This will also set [willCloseItself] to `true`, thus canceling out response finalizers.
+ ///
+ /// If this instance has access to a [correspondingRequest], then it will attempt to transform
+ /// the content using at most one of the response [encoders].
+ @override
+ Future addStream(Stream> stream) {
+ if (_isClosed && !_useStream) throw _closed();
+ bool firstStream = _useStream == false;
+ willCloseItself = _useStream = _isClosed = true;
+
+ Stream> output = stream;
+
+ if (firstStream) {
+ // If this is the first stream added to this response,
+ // then add headers, status code, etc.
+ io
+ ..statusCode = statusCode
+ ..cookies.addAll(cookies);
+ headers.forEach(io.headers.set);
+ }
+
+ if (encoders.isNotEmpty && correspondingRequest != null) {
+ var allowedEncodings =
+ (correspondingRequest.headers[HttpHeaders.ACCEPT_ENCODING] ?? [])
+ .map((str) {
+ // Ignore quality specifications in accept-encoding
+ // ex. gzip;q=0.8
+ if (!str.contains(';')) return str;
+ return str.split(';')[0];
+ });
+
+ for (var encodingName in allowedEncodings) {
+ Converter, List> encoder;
+ String key = encodingName;
+
+ if (encoders.containsKey(encodingName))
+ encoder = encoders[encodingName];
+ else if (encodingName == '*') {
+ encoder = encoders[key = encoders.keys.first];
+ }
+
+ if (encoder != null) {
+ if (firstStream) {
+ io.headers.set(HttpHeaders.CONTENT_ENCODING, key);
+ }
+
+ output = encoders[key].bind(output);
+ break;
+ }
+ }
+ }
+
+ return io.addStream(output);
+ }
+
+ @override
+ void addError(Object error, [StackTrace stackTrace]) {
+ io.addError(error, stackTrace);
+ if (_done?.isCompleted == false) _done.completeError(error, stackTrace);
}
/// Writes data to the response.
- void write(value, {Encoding encoding: UTF8}) {
- if (_isClosed)
+ void write(value, {Encoding encoding}) {
+ encoding ??= UTF8;
+
+ if (_isClosed && !_useStream)
throw _closed();
- else {
+ else if (_useStream) {
+ if (value is List)
+ io.add(value);
+ else
+ io.add(encoding.encode(value.toString()));
+ } else {
if (value is List)
buffer.add(value);
else
@@ -329,8 +454,10 @@ class ResponseContext implements StringSink {
@override
void writeCharCode(int charCode) {
- if (_isClosed)
+ if (_isClosed && !_useStream)
throw _closed();
+ else if (_useStream)
+ io.add([charCode]);
else
buffer.addByte(charCode);
}
@@ -364,6 +491,7 @@ class _LockableBytesBuilderImpl implements _LockableBytesBuilder {
void _lock() {
_closed = true;
}
+
@override
void add(List bytes) {
if (_closed)
diff --git a/lib/src/http/server.dart b/lib/src/http/server.dart
index 8a85cd53..0f6dc170 100644
--- a/lib/src/http/server.dart
+++ b/lib/src/http/server.dart
@@ -1,6 +1,7 @@
library angel_framework.http.server;
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
import 'dart:mirrors';
import 'package:angel_route/angel_route.dart' hide Extensible;
@@ -14,6 +15,7 @@ import 'angel_base.dart';
import 'angel_http_exception.dart';
import 'controller.dart';
import 'fatal_error.dart';
+
//import 'hooked_service.dart';
import 'request_context.dart';
import 'response_context.dart';
@@ -26,30 +28,37 @@ final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
typedef Future ServerGenerator(InternetAddress address, int port);
/// Handles an [AngelHttpException].
-typedef Future AngelErrorHandler(
- AngelHttpException err, RequestContext req, ResponseContext res);
+typedef Future AngelErrorHandler(AngelHttpException err, RequestContext req,
+ ResponseContext res);
/// A function that configures an [Angel] server in some way.
typedef Future AngelConfigurer(Angel app);
/// A powerful real-time/REST/MVC server class.
class Angel extends AngelBase {
- SafeCtrl _afterProcessed = new SafeCtrl.broadcast();
- SafeCtrl _beforeProcessed =
- new SafeCtrl.broadcast();
- SafeCtrl _fatalErrorStream =
- new SafeCtrl.broadcast();
- SafeCtrl _onController = new SafeCtrl.broadcast();
+ final SafeCtrl _afterProcessed =
+ new SafeCtrl.broadcast();
+ final SafeCtrl _beforeProcessed =
+ new SafeCtrl.broadcast();
+ final SafeCtrl _fatalErrorStream =
+ new SafeCtrl.broadcast();
+ final SafeCtrl _onController =
+ new SafeCtrl.broadcast();
final List _children = [];
+ final Map _handlerCache = {};
+
Router _flattened;
bool _isProduction;
Angel _parent;
- final Map _handlerCache = {};
ServerGenerator _serverGenerator = HttpServer.bind;
+ /// A global Map of converters that can transform responses bodies.
+ final Map, List>> encoders = {};
+
/// A global Map of manual injections. You usually will not want to touch this.
final Map injections = {};
+
final Map _preContained = {};
ResponseSerializer _serializer;
@@ -204,17 +213,17 @@ class Angel extends AngelBase {
await service.close();
});
- for (var plugin in justBeforeStop) await plugin(this);
+ for (var plugin in justBeforeStop)
+ await plugin(this);
return server;
}
@override
- void dumpTree(
- {callback(String tree),
- String header: 'Dumping route tree:',
- String tab: ' ',
- bool showMatchers: false}) {
+ void dumpTree({callback(String tree),
+ String header: 'Dumping route tree:',
+ String tab: ' ',
+ bool showMatchers: false}) {
if (isProduction) {
if (_flattened == null) _flattened = flatten(this);
@@ -223,8 +232,8 @@ class Angel extends AngelBase {
header: header?.isNotEmpty == true
? header
: (isProduction
- ? 'Dumping flattened route tree:'
- : 'Dumping route tree:'),
+ ? 'Dumping flattened route tree:'
+ : 'Dumping route tree:'),
tab: tab ?? ' ',
showMatchers: showMatchers == true);
} else {
@@ -233,8 +242,8 @@ class Angel extends AngelBase {
header: header?.isNotEmpty == true
? header
: (isProduction
- ? 'Dumping flattened route tree:'
- : 'Dumping route tree:'),
+ ? 'Dumping flattened route tree:'
+ : 'Dumping route tree:'),
tab: tab ?? ' ',
showMatchers: showMatchers == true);
}
@@ -245,13 +254,18 @@ class Angel extends AngelBase {
injections[key] = value;
}
+ /// Shortcuts for adding converters to transform the response buffer/stream of any request.
+ void injectEncoders(Map, List>> encoders) {
+ this.encoders.addAll(encoders);
+ }
+
/// Shortcut for adding a middleware to inject a serialize on every request.
void injectSerializer(ResponseSerializer serializer) {
_serializer = serializer;
}
- Future getHandlerResult(
- handler, RequestContext req, ResponseContext res) async {
+ Future getHandlerResult(handler, RequestContext req,
+ ResponseContext res) async {
/*if (handler is RequestMiddleware) {
var result = await handler(req, res);
@@ -289,8 +303,8 @@ class Angel extends AngelBase {
}
/// Runs some [handler]. Returns `true` if request execution should continue.
- Future executeHandler(
- handler, RequestContext req, ResponseContext res) async {
+ Future executeHandler(handler, RequestContext req,
+ ResponseContext res) async {
var result = await getHandlerResult(handler, req, res);
if (result is bool) {
@@ -311,9 +325,12 @@ class Angel extends AngelBase {
});
}
- Future createResponseContext(HttpResponse response) =>
- new Future.value(new ResponseContext(response, this)
- ..serializer = (_serializer ?? god.serialize));
+ Future createResponseContext(HttpResponse response,
+ [RequestContext correspondingRequest]) =>
+ new Future.value(
+ new ResponseContext(response, this, correspondingRequest)
+ ..serializer = (_serializer ?? god.serialize)
+ ..encoders.addAll(encoders ?? {}));
/// Attempts to find a middleware by the given name within this application.
findMiddleware(key) {
@@ -363,7 +380,7 @@ class Angel extends AngelBase {
Future handleRequest(HttpRequest request) async {
try {
var req = await createRequestContext(request);
- var res = await createResponseContext(request.response);
+ var res = await createResponseContext(request.response, req);
String requestedUrl;
// Faster way to get path
@@ -388,11 +405,12 @@ class Angel extends AngelBase {
var pipeline = _handlerCache.putIfAbsent(requestedUrl, () {
Router r =
- isProduction ? (_flattened ?? (_flattened = flatten(this))) : this;
+ isProduction ? (_flattened ?? (_flattened = flatten(this))) : this;
var resolved =
- r.resolveAll(requestedUrl, requestedUrl, method: req.method);
+ r.resolveAll(requestedUrl, requestedUrl, method: req.method);
- for (var result in resolved) req.params.addAll(result.allParams);
+ for (var result in resolved)
+ req.params.addAll(result.allParams);
if (resolved.isNotEmpty) {
var route = resolved.first.route;
@@ -402,7 +420,8 @@ class Angel extends AngelBase {
var m = new MiddlewarePipeline(resolved);
req.inject(MiddlewarePipeline, m);
- return new List.from(before)..addAll(m.handlers)..addAll(after);
+ return new List.from(before)
+ ..addAll(m.handlers)..addAll(after);
});
for (var handler in pipeline) {
@@ -445,7 +464,9 @@ class Angel extends AngelBase {
void _walk(Router router) {
if (router is Angel) {
- router..before.forEach(_add)..after.forEach(_add);
+ router
+ ..before.forEach(_add)
+ ..after.forEach(_add);
}
router.requestMiddleware.forEach((k, v) => _add(v));
@@ -467,8 +488,8 @@ class Angel extends AngelBase {
/// Run a function after injecting from service container.
/// If this function has been reflected before, then
/// the execution will be faster, as the injection requirements were stored beforehand.
- Future runContained(
- Function handler, RequestContext req, ResponseContext res) {
+ Future runContained(Function handler, RequestContext req,
+ ResponseContext res) {
if (_preContained.containsKey(handler)) {
return handleContained(handler, _preContained[handler])(req, res);
}
@@ -477,23 +498,23 @@ class Angel extends AngelBase {
}
/// Runs with DI, and *always* reflects. Prefer [runContained].
- Future runReflected(
- Function handler, RequestContext req, ResponseContext res) async {
+ Future runReflected(Function handler, RequestContext req,
+ ResponseContext res) async {
var h =
- handleContained(handler, _preContained[handler] = preInject(handler));
+ handleContained(handler, _preContained[handler] = preInject(handler));
return await h(req, res);
// return await closureMirror.apply(args).reflectee;
}
/// Use [sendResponse] instead.
@deprecated
- Future sendRequest(
- HttpRequest request, RequestContext req, ResponseContext res) =>
+ Future sendRequest(HttpRequest request, RequestContext req,
+ ResponseContext res) =>
sendResponse(request, req, res);
/// Sends a response.
- Future sendResponse(
- HttpRequest request, RequestContext req, ResponseContext res,
+ Future sendResponse(HttpRequest request, RequestContext req,
+ ResponseContext res,
{bool ignoreFinalizers: false}) {
_afterProcessed.add(request);
@@ -503,7 +524,7 @@ class Angel extends AngelBase {
Future finalizers = ignoreFinalizers == true
? new Future.value()
: responseFinalizers.fold(
- new Future.value(), (out, f) => out.then((_) => f(req, res)));
+ new Future.value(), (out, f) => out.then((_) => f(req, res)));
if (res.isOpen) res.end();
@@ -515,10 +536,40 @@ class Angel extends AngelBase {
..chunkedTransferEncoding = res.chunked ?? true
..set(HttpHeaders.CONTENT_LENGTH, res.buffer.length);
+ List outputBuffer = res.buffer.toBytes();
+
+ if (res.encoders.isNotEmpty) {
+ var allowedEncodings =
+ (req.headers[HttpHeaders.ACCEPT_ENCODING] ?? []).map((str) {
+ // Ignore quality specifications in accept-encoding
+ // ex. gzip;q=0.8
+ if (!str.contains(';')) return str;
+ return str.split(';')[0];
+ });
+
+ for (var encodingName in allowedEncodings) {
+ Converter, List> encoder;
+ String key = encodingName;
+
+ if (res.encoders.containsKey(encodingName))
+ encoder = res.encoders[encodingName];
+ else if (encodingName == '*') {
+ encoder = res.encoders[key = res.encoders.keys.first];
+ }
+
+ if (encoder != null) {
+ request.response.headers
+ .set(HttpHeaders.CONTENT_ENCODING, key);
+ outputBuffer = res.encoders[key].convert(outputBuffer);
+ break;
+ }
+ }
+ }
+
request.response
..statusCode = res.statusCode
..cookies.addAll(res.cookies)
- ..add(res.buffer.toBytes());
+ ..add(outputBuffer);
return finalizers.then((_) => request.response.close());
}
@@ -529,7 +580,9 @@ class Angel extends AngelBase {
await configurer(this);
if (configurer is Controller)
- _onController.add(controllers[configurer.findExpose().path] = configurer);
+ _onController.add(controllers[configurer
+ .findExpose()
+ .path] = configurer);
}
/// Starts the server, wrapped in a [runZoned] call.
@@ -633,8 +686,9 @@ class Angel extends AngelBase {
/// An instance mounted on a server started by the [serverGenerator].
factory Angel.custom(ServerGenerator serverGenerator,
- {@deprecated bool debug: false}) =>
- new Angel().._serverGenerator = serverGenerator;
+ {@deprecated bool debug: false}) =>
+ new Angel()
+ .._serverGenerator = serverGenerator;
factory Angel.fromSecurityContext(SecurityContext context,
{@deprecated bool debug: false}) {
@@ -655,7 +709,7 @@ class Angel extends AngelBase {
factory Angel.secure(String certificateChainPath, String serverKeyPath,
{bool debug: false, String password}) {
var certificateChain =
- Platform.script.resolve(certificateChainPath).toFilePath();
+ Platform.script.resolve(certificateChainPath).toFilePath();
var serverKey = Platform.script.resolve(serverKeyPath).toFilePath();
var serverContext = new SecurityContext();
serverContext.useCertificateChain(certificateChain, password: password);
diff --git a/pubspec.yaml b/pubspec.yaml
index 0a336817..0be3cf64 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: angel_framework
-version: 1.0.8
+version: 1.0.9
description: A high-powered HTTP server with DI, routing and more.
author: Tobe O
homepage: https://github.com/angel-dart/angel_framework
@@ -9,6 +9,7 @@ dependencies:
angel_model: ^1.0.0
angel_route: ">=1.0.5 <2.0.0"
body_parser: ^1.0.0-dev
+ charcode: ^1.0.0
container: ^0.1.2
flatten: ^1.0.0
json_god: ^2.0.0-beta
@@ -17,6 +18,7 @@ dependencies:
mime: ^0.9.3
random_string: ^0.0.1
dev_dependencies:
+ matcher: ^0.12.0
mock_request: ^1.0.0
http: ^0.11.3
test: ^0.12.13
diff --git a/test/all.dart b/test/all.dart
index 7cdfd9e4..f2d089d6 100644
--- a/test/all.dart
+++ b/test/all.dart
@@ -2,6 +2,7 @@ import 'accepts_test.dart' as accepts;
import 'anonymous_service_test.dart' as anonymous_service;
import 'controller_test.dart' as controller;
import 'di_test.dart' as di;
+import 'encoders_buffer_test.dart' as encoders_buffer;
import 'exception_test.dart' as exception;
import 'general_test.dart' as general;
import 'hooked_test.dart' as hooked;
@@ -10,6 +11,7 @@ import 'routing_test.dart' as routing;
import 'serialize_test.dart' as serialize;
import 'server_test.dart' as server;
import 'services_test.dart' as services;
+import 'streaming_test.dart' as streaming;
import 'typed_service_test.dart' as typed_service;
import 'util_test.dart' as util;
import 'view_generator_test.dart' as view_generator;
@@ -21,6 +23,7 @@ main() {
group('anonymous service', anonymous_service.main);
group('controller', controller.main);
group('di', di.main);
+ group('encoders_buffer', encoders_buffer.main);
group('exception', exception.main);
group('general', general.main);
group('hooked', hooked.main);
@@ -29,6 +32,7 @@ main() {
group('serialize', serialize.main);
group('server', server.main);
group('services', services.main);
+ group('streaming', streaming.main);
group('typed_service', typed_service.main);
group('util', util.main);
group('view generator', view_generator.main);
diff --git a/test/controller_test.dart b/test/controller_test.dart
index e56000ae..7c371790 100644
--- a/test/controller_test.dart
+++ b/test/controller_test.dart
@@ -44,7 +44,7 @@ main() {
String url;
setUp(() async {
- app = new Angel(debug: true);
+ app = new Angel();
app.registerMiddleware("foo", (req, res) async => res.write("Hello, "));
app.registerMiddleware("bar", (req, res) async => res.write("world!"));
app.get(
diff --git a/test/di_test.dart b/test/di_test.dart
index 9f441c13..3882a750 100644
--- a/test/di_test.dart
+++ b/test/di_test.dart
@@ -16,7 +16,7 @@ main() {
String url;
setUp(() async {
- app = new Angel(debug: true);
+ app = new Angel();
client = new http.Client();
// Inject some todos
diff --git a/test/encoders_buffer_test.dart b/test/encoders_buffer_test.dart
new file mode 100644
index 00000000..47ed8812
--- /dev/null
+++ b/test/encoders_buffer_test.dart
@@ -0,0 +1,94 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:mock_request/mock_request.dart';
+import 'package:test/test.dart';
+
+main() async {
+ Angel app;
+
+ setUp(() {
+ app = new Angel();
+ app.injectEncoders(
+ {
+ 'deflate': ZLIB.encoder,
+ 'gzip': GZIP.encoder,
+ },
+ );
+
+ app.get('/hello', (res) {
+ res.write('Hello, world!');
+ });
+ });
+
+ tearDown(() => app.close());
+
+ encodingTests(() => app);
+}
+
+void encodingTests(Angel getApp()) {
+ group('encoding', () {
+ Angel app;
+
+ setUp(() {
+ app = getApp();
+ });
+
+ test('sends plaintext if no accept-encoding', () async {
+ var rq = new MockHttpRequest('GET', Uri.parse('/hello'))..close();
+ var rs = rq.response;
+ await app.handleRequest(rq);
+
+ var body = await rs.transform(UTF8.decoder).join();
+ expect(body, 'Hello, world!');
+ });
+
+ test('encodes if wildcard', () async {
+ var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
+ ..headers.set(HttpHeaders.ACCEPT_ENCODING, '*')
+ ..close();
+ var rs = rq.response;
+ await app.handleRequest(rq);
+
+ var body = await rs.fold>([], (out, list) => []..addAll(list));
+ expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'deflate');
+ expect(body, ZLIB.encode(UTF8.encode('Hello, world!')));
+ });
+
+ test('encodes if wildcard + multiple', () async {
+ var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
+ ..headers.set(HttpHeaders.ACCEPT_ENCODING, ['foo', 'bar', '*'])
+ ..close();
+ var rs = rq.response;
+ await app.handleRequest(rq);
+
+ var body = await rs.fold>([], (out, list) => []..addAll(list));
+ expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'deflate');
+ expect(body, ZLIB.encode(UTF8.encode('Hello, world!')));
+ });
+
+ test('encodes if explicit', () async {
+ var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
+ ..headers.set(HttpHeaders.ACCEPT_ENCODING, 'gzip')
+ ..close();
+ var rs = rq.response;
+ await app.handleRequest(rq);
+
+ var body = await rs.fold>([], (out, list) => []..addAll(list));
+ expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'gzip');
+ expect(body, GZIP.encode(UTF8.encode('Hello, world!')));
+ });
+
+ test('only uses one encoder', () async {
+ var rq = new MockHttpRequest('GET', Uri.parse('/hello'))
+ ..headers.set(HttpHeaders.ACCEPT_ENCODING, ['gzip', 'deflate'])
+ ..close();
+ var rs = rq.response;
+ await app.handleRequest(rq);
+
+ var body = await rs.fold>([], (out, list) => []..addAll(list));
+ expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'gzip');
+ expect(body, GZIP.encode(UTF8.encode('Hello, world!')));
+ });
+ });
+}
diff --git a/test/routing_test.dart b/test/routing_test.dart
index e6c955de..b0074f69 100644
--- a/test/routing_test.dart
+++ b/test/routing_test.dart
@@ -24,10 +24,9 @@ main() {
http.Client client;
setUp(() async {
- final debug = true;
- app = new Angel(debug: debug);
- nested = new Angel(debug: debug);
- todos = new Angel(debug: debug);
+ app = new Angel();
+ nested = new Angel();
+ todos = new Angel();
// Lazy-parse in production
[app, nested, todos].forEach((Angel app) {
diff --git a/test/streaming_test.dart b/test/streaming_test.dart
new file mode 100644
index 00000000..b59dc91a
--- /dev/null
+++ b/test/streaming_test.dart
@@ -0,0 +1,91 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:mock_request/mock_request.dart';
+import 'package:test/test.dart';
+import 'encoders_buffer_test.dart' show encodingTests;
+
+main() async {
+ Angel app;
+
+ setUp(() {
+ app = new Angel();
+ app.injectEncoders(
+ {
+ 'deflate': ZLIB.encoder,
+ 'gzip': GZIP.encoder,
+ },
+ );
+
+ app.get('/hello', (res) {
+ new Stream>.fromIterable(['Hello, world!'.codeUnits]).pipe(res);
+ });
+
+ app.get('/write', (res) async {
+ await res.addStream(
+ new Stream>.fromIterable(['Hello, world!'.codeUnits]));
+ res.write('bye');
+ await res.close();
+ });
+
+ app.get('/multiple', (res) async {
+ await res.addStream(
+ new Stream>.fromIterable(['Hello, world!'.codeUnits]));
+ await res.addStream(
+ new Stream>.fromIterable(['bye'.codeUnits]));
+ await res.close();
+ });
+
+ app.get('/overwrite', (res) async {
+ res.statusCode = 32;
+ await new Stream>.fromIterable(['Hello, world!'.codeUnits])
+ .pipe(res);
+ await new Stream>.fromIterable(['Hello, world!'.codeUnits])
+ .pipe(res);
+ });
+
+ app.get('/error', (res) => res.addError(new StateError('wtf')));
+
+ app.fatalErrorStream.listen((e) {
+ stderr..writeln(e.error)..writeln(e.stack);
+ });
+ });
+
+ tearDown(() => app.close());
+
+ _expectHelloBye(String path) async {
+ var rq = new MockHttpRequest('GET', Uri.parse(path))..close();
+ await app.handleRequest(rq);
+ var body = await rq.response.transform(UTF8.decoder).join();
+ expect(body, 'Hello, world!bye');
+ }
+
+ test('write after addStream', () => _expectHelloBye('/write'));
+
+ test('multiple addStream', () => _expectHelloBye('/multiple'));
+
+ test('cannot write after close', () async {
+ var rq = new MockHttpRequest('GET', Uri.parse('/overwrite'))
+ ..close();
+ await app.handleRequest(rq);
+ var body = await rq.response.transform(UTF8.decoder).join();
+
+ if (rq.response.statusCode != 32)
+ throw 'overwrite should throw error; response: $body';
+ });
+
+ test('res => addError', () async {
+ try {
+ var rq = new MockHttpRequest('GET', Uri.parse('/error'))
+ ..close();
+ await app.handleRequest(rq);
+ var body = await rq.response.transform(UTF8.decoder).join();
+ throw 'addError should throw error; response: $body';
+ } on StateError {
+ // Should throw error...
+ }
+ });
+
+ encodingTests(() => app);
+}