1.0.9
This commit is contained in:
parent
917713bbc3
commit
3d59ec0771
12 changed files with 800 additions and 406 deletions
File diff suppressed because it is too large
Load diff
11
CHANGELOG.md
11
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<List<int>`, 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).
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<String, String> _headers = {HttpHeaders.SERVER: 'angel'};
|
||||
class ResponseContext implements StreamSink<List<int>>, StringSink {
|
||||
final Map properties = {};
|
||||
bool _isOpen = true, _isClosed = false;
|
||||
final BytesBuilder _buffer = new _LockableBytesBuilder();
|
||||
final Map<String, String> _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<Cookie> 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<String, Converter<List<int>, List<int>>> 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<String, String> 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<int>, List<int>> codec}) async {
|
||||
{@deprecated int chunkSize,
|
||||
@deprecated int sleepMs: 0,
|
||||
@deprecated bool resumable: true,
|
||||
|
||||
/// Use [encoders] instead of manually specifying a codec.
|
||||
@deprecated Codec<List<int>, List<int>> 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<int> 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<List<int>> stream) {
|
||||
if (_isClosed && !_useStream) throw _closed();
|
||||
bool firstStream = _useStream == false;
|
||||
willCloseItself = _useStream = _isClosed = true;
|
||||
|
||||
Stream<List<int>> 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<int>, List<int>> 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<int>)
|
||||
io.add(value);
|
||||
else
|
||||
io.add(encoding.encode(value.toString()));
|
||||
} else {
|
||||
if (value is List<int>)
|
||||
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<int> bytes) {
|
||||
if (_closed)
|
||||
|
|
|
@ -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<HttpServer> 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<HttpRequest> _afterProcessed = new SafeCtrl<HttpRequest>.broadcast();
|
||||
SafeCtrl<HttpRequest> _beforeProcessed =
|
||||
new SafeCtrl<HttpRequest>.broadcast();
|
||||
SafeCtrl<AngelFatalError> _fatalErrorStream =
|
||||
new SafeCtrl<AngelFatalError>.broadcast();
|
||||
SafeCtrl<Controller> _onController = new SafeCtrl<Controller>.broadcast();
|
||||
final SafeCtrl<HttpRequest> _afterProcessed =
|
||||
new SafeCtrl<HttpRequest>.broadcast();
|
||||
final SafeCtrl<HttpRequest> _beforeProcessed =
|
||||
new SafeCtrl<HttpRequest>.broadcast();
|
||||
final SafeCtrl<AngelFatalError> _fatalErrorStream =
|
||||
new SafeCtrl<AngelFatalError>.broadcast();
|
||||
final SafeCtrl<Controller> _onController =
|
||||
new SafeCtrl<Controller>.broadcast();
|
||||
|
||||
final List<Angel> _children = [];
|
||||
final Map<String, List> _handlerCache = {};
|
||||
|
||||
Router _flattened;
|
||||
bool _isProduction;
|
||||
Angel _parent;
|
||||
final Map<String, List> _handlerCache = {};
|
||||
ServerGenerator _serverGenerator = HttpServer.bind;
|
||||
|
||||
/// A global Map of converters that can transform responses bodies.
|
||||
final Map<String, Converter<List<int>, List<int>>> encoders = {};
|
||||
|
||||
/// A global Map of manual injections. You usually will not want to touch this.
|
||||
final Map injections = {};
|
||||
|
||||
final Map<dynamic, InjectionRequest> _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<String, Converter<List<int>, List<int>>> 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<bool> executeHandler(
|
||||
handler, RequestContext req, ResponseContext res) async {
|
||||
Future<bool> 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<ResponseContext> createResponseContext(HttpResponse response) =>
|
||||
new Future<ResponseContext>.value(new ResponseContext(response, this)
|
||||
..serializer = (_serializer ?? god.serialize));
|
||||
Future<ResponseContext> createResponseContext(HttpResponse response,
|
||||
[RequestContext correspondingRequest]) =>
|
||||
new Future<ResponseContext>.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<Future>(
|
||||
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<int> 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<int>, List<int>> 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);
|
||||
|
|
|
@ -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 <thosakwe@gmail.com>
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -16,7 +16,7 @@ main() {
|
|||
String url;
|
||||
|
||||
setUp(() async {
|
||||
app = new Angel(debug: true);
|
||||
app = new Angel();
|
||||
client = new http.Client();
|
||||
|
||||
// Inject some todos
|
||||
|
|
94
test/encoders_buffer_test.dart
Normal file
94
test/encoders_buffer_test.dart
Normal file
|
@ -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<List<int>>([], (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<List<int>>([], (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<List<int>>([], (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<List<int>>([], (out, list) => []..addAll(list));
|
||||
expect(rs.headers.value(HttpHeaders.CONTENT_ENCODING), 'gzip');
|
||||
expect(body, GZIP.encode(UTF8.encode('Hello, world!')));
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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) {
|
||||
|
|
91
test/streaming_test.dart
Normal file
91
test/streaming_test.dart
Normal file
|
@ -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<List<int>>.fromIterable(['Hello, world!'.codeUnits]).pipe(res);
|
||||
});
|
||||
|
||||
app.get('/write', (res) async {
|
||||
await res.addStream(
|
||||
new Stream<List<int>>.fromIterable(['Hello, world!'.codeUnits]));
|
||||
res.write('bye');
|
||||
await res.close();
|
||||
});
|
||||
|
||||
app.get('/multiple', (res) async {
|
||||
await res.addStream(
|
||||
new Stream<List<int>>.fromIterable(['Hello, world!'.codeUnits]));
|
||||
await res.addStream(
|
||||
new Stream<List<int>>.fromIterable(['bye'.codeUnits]));
|
||||
await res.close();
|
||||
});
|
||||
|
||||
app.get('/overwrite', (res) async {
|
||||
res.statusCode = 32;
|
||||
await new Stream<List<int>>.fromIterable(['Hello, world!'.codeUnits])
|
||||
.pipe(res);
|
||||
await new Stream<List<int>>.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);
|
||||
}
|
Loading…
Reference in a new issue