+9
This commit is contained in:
parent
2c846ae449
commit
2d87631e8d
19 changed files with 808 additions and 586 deletions
|
@ -26,14 +26,14 @@
|
|||
<entry key="angel_route">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-1.0.8/lib" />
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-2.0.3+2/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="args">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.0.2/lib" />
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.1.0/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
|
@ -86,6 +86,13 @@
|
|||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="combinator">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/combinator-1.0.0-beta+7/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="container">
|
||||
<value>
|
||||
<list>
|
||||
|
@ -114,13 +121,6 @@
|
|||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="flatten">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/flatten-1.0.0/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="front_end">
|
||||
<value>
|
||||
<list>
|
||||
|
@ -271,7 +271,7 @@
|
|||
<entry key="path">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.0/lib" />
|
||||
<option value="$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.1/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
|
@ -442,8 +442,8 @@
|
|||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/analyzer-0.30.0+4/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_http_exception-1.0.0/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_model-1.0.0/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-1.0.8/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.0.2/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/angel_route-2.0.3+2/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/args-1.1.0/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/async-1.13.3/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/barback-0.15.2+13/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/body_parser-1.0.3/lib" />
|
||||
|
@ -451,11 +451,11 @@
|
|||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/charcode-1.1.1/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/cli_util-0.1.2+1/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/collection-1.14.3/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/combinator-1.0.0-beta+7/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/container-0.1.2/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/convert-2.0.1/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/crypto-2.0.2+1/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/csslib-0.14.1/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/flatten-1.0.0/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/front_end-0.1.0-alpha.4.1/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/glob-1.1.5/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/html-0.13.2/lib" />
|
||||
|
@ -477,7 +477,7 @@
|
|||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/node_preamble-1.4.0/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_config-1.0.3/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/package_resolver-1.0.2/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.0/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/path-1.5.1/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/plugin-0.2.0+2/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pool-1.3.3/lib" />
|
||||
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.dartlang.org/pub_semver-1.3.2/lib" />
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="only match route with matching method in routing_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||
<option name="envs">
|
||||
<entry key="ANGEL_ENV" value="production" />
|
||||
</option>
|
||||
<option name="filePath" value="$PROJECT_DIR$/test/routing_test.dart" />
|
||||
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
|
||||
<option name="testName" value="only match route with matching method" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
8
.idea/runConfigurations/tests_in_framework.xml
Normal file
8
.idea/runConfigurations/tests_in_framework.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="tests in framework" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$" />
|
||||
<option name="scope" value="FOLDER" />
|
||||
<option name="testRunnerOptions" value="-j 4" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
File diff suppressed because it is too large
Load diff
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -1,3 +1,15 @@
|
|||
# 1.1.0-alpha+9
|
||||
* Fixed a bug that prevented `isProduction` from ever returning `true`.
|
||||
* This enabled caching, which greatly improved performance.
|
||||
* Requests no longer have independent zones, which greatly improved performance.
|
||||
* `FormatException`, when caught, is automatically transformed in a `400` error response.
|
||||
* Added `extension` to `RequestContext`.
|
||||
* Added `strict` to `RequestContext#accepts`.
|
||||
* Added a `toString` override for the `Providers` class.
|
||||
* Returned to `RegExp` for stripping stray slashes.
|
||||
* The request path is now only parsed once.
|
||||
* Optimized the parsing of the `ACCEPT_ENCODING` header.
|
||||
|
||||
# 1.1.0-alpha+8
|
||||
* Added an `autoIdAndDateFields` flag to `MapService`. Finally.
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
import 'dart:mirrors';
|
||||
import 'package:body_parser/body_parser.dart';
|
||||
import 'package:charcode/charcode.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'metadata.dart';
|
||||
import 'response_context.dart';
|
||||
import 'routable.dart';
|
||||
|
@ -13,7 +13,7 @@ part 'injection.dart';
|
|||
|
||||
/// A convenience wrapper around an incoming HTTP request.
|
||||
class RequestContext {
|
||||
String _acceptHeaderCache;
|
||||
String _acceptHeaderCache, _extensionCache;
|
||||
bool _acceptsAllCache;
|
||||
BodyParseResult _body;
|
||||
ContentType _contentType;
|
||||
|
@ -39,9 +39,10 @@ class RequestContext {
|
|||
String get hostname => io.headers.value(HttpHeaders.HOST);
|
||||
|
||||
final Map _injections = {};
|
||||
Map _injectionsCache;
|
||||
|
||||
/// A [Map] of singletons injected via [inject]. *Read-only*.
|
||||
Map get injections => new Map.unmodifiable(_injections);
|
||||
Map get injections => _injectionsCache ??= new Map.unmodifiable(_injections);
|
||||
|
||||
/// The underlying [HttpRequest] instance underneath this context.
|
||||
HttpRequest get io => _io;
|
||||
|
@ -135,8 +136,13 @@ class RequestContext {
|
|||
io.headers.value("X-Requested-With")?.trim()?.toLowerCase() ==
|
||||
'xmlhttprequest';
|
||||
|
||||
/// Returns the file extension of the requested path, if any.
|
||||
///
|
||||
/// Includes the leading `.`, if there is one.
|
||||
String get extension => _extensionCache ??= p.extension(uri.path);
|
||||
|
||||
/// Magically transforms an [HttpRequest] into a [RequestContext].
|
||||
static Future<RequestContext> from(HttpRequest request, Angel app) async {
|
||||
static Future<RequestContext> from(HttpRequest request, Angel app, String path) async {
|
||||
RequestContext ctx = new RequestContext();
|
||||
|
||||
String override = request.method;
|
||||
|
@ -150,6 +156,7 @@ class RequestContext {
|
|||
ctx._contentType = request.headers.contentType;
|
||||
ctx._override = override;
|
||||
|
||||
/*
|
||||
// Faster way to get path
|
||||
List<int> _path = [];
|
||||
|
||||
|
@ -175,6 +182,9 @@ class RequestContext {
|
|||
ctx._path = new String.fromCharCodes(_path.take(lastSlash));
|
||||
else
|
||||
ctx._path = new String.fromCharCodes(_path);
|
||||
*/
|
||||
|
||||
ctx._path = path;
|
||||
ctx._io = request;
|
||||
|
||||
if (app.lazyParseBodies != true) {
|
||||
|
@ -219,17 +229,19 @@ class RequestContext {
|
|||
}
|
||||
|
||||
_injections[type] = value;
|
||||
_injectionsCache = null;
|
||||
}
|
||||
|
||||
/// Returns `true` if the client's `Accept` header indicates that the given [contentType] is considered a valid response.
|
||||
///
|
||||
/// You cannot provide a `null` [contentType].
|
||||
/// If the `Accept` header's value is `*/*`, this method will always return `true`.
|
||||
/// To ignore the wildcard (`*/*`), pass [strict] as `true`.
|
||||
///
|
||||
/// [contentType] can be either of the following:
|
||||
/// * A [ContentType], in which case the `Accept` header will be compared against its `mimeType` property.
|
||||
/// * Any other Dart value, in which case the `Accept` header will be compared against the result of a `toString()` call.
|
||||
bool accepts(contentType) {
|
||||
bool accepts(contentType, {bool strict: false}) {
|
||||
var contentTypeString = contentType is ContentType
|
||||
? contentType.mimeType
|
||||
: contentType?.toString();
|
||||
|
@ -242,7 +254,7 @@ class RequestContext {
|
|||
|
||||
if (_acceptHeaderCache == null)
|
||||
return false;
|
||||
else if (_acceptHeaderCache.contains('*/*'))
|
||||
else if (strict != true && _acceptHeaderCache.contains('*/*'))
|
||||
return true;
|
||||
else
|
||||
return _acceptHeaderCache.contains(contentTypeString);
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'dart:io';
|
|||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:json_god/json_god.dart' as god;
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:pool/pool.dart';
|
||||
import 'server.dart' show Angel;
|
||||
import 'controller.dart';
|
||||
import 'request_context.dart';
|
||||
|
@ -337,6 +338,31 @@ class ResponseContext implements StreamSink<List<int>>, StringSink {
|
|||
buffer.add(data);
|
||||
}
|
||||
|
||||
/// Configure the response to write directly to the output stream, instead of buffering.
|
||||
bool useStream() {
|
||||
if (!_useStream) {
|
||||
// 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);
|
||||
willCloseItself = _useStream = _isClosed = true;
|
||||
|
||||
if (_correspondingRequest?.injections?.containsKey(Stopwatch) == true) {
|
||||
(_correspondingRequest.injections[Stopwatch] as Stopwatch).stop();
|
||||
}
|
||||
|
||||
if (_correspondingRequest?.injections?.containsKey(PoolResource) == true) {
|
||||
(_correspondingRequest.injections[PoolResource] as PoolResource).release();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Adds a stream directly the underlying dart:[io] response.
|
||||
///
|
||||
/// This will also set [willCloseItself] to `true`, thus canceling out response finalizers.
|
||||
|
@ -346,24 +372,10 @@ class ResponseContext implements StreamSink<List<int>>, StringSink {
|
|||
@override
|
||||
Future addStream(Stream<List<int>> stream) {
|
||||
if (_isClosed && !_useStream) throw _closed();
|
||||
bool firstStream = _useStream == false;
|
||||
willCloseItself = _useStream = _isClosed = true;
|
||||
|
||||
if (_correspondingRequest?.injections?.containsKey(Stopwatch) == true) {
|
||||
(_correspondingRequest.injections[Stopwatch] as Stopwatch).stop();
|
||||
}
|
||||
var firstStream = useStream();
|
||||
|
||||
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] ?? [])
|
||||
|
|
|
@ -64,6 +64,7 @@ class Routable extends Router {
|
|||
/// Assigns a middleware to a name for convenience.
|
||||
@override
|
||||
registerMiddleware(String name, @checked RequestHandler middleware) =>
|
||||
// ignore: deprecated_member_use
|
||||
super.registerMiddleware(name, middleware);
|
||||
|
||||
/// Retrieves the service assigned to the given path.
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
library angel_framework.http.server;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:collection' show HashMap;
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_http_exception/angel_http_exception.dart';
|
||||
import 'package:angel_route/angel_route.dart' hide Extensible;
|
||||
import 'package:charcode/charcode.dart';
|
||||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:combinator/combinator.dart';
|
||||
export 'package:container/container.dart';
|
||||
import 'package:flatten/flatten.dart';
|
||||
import 'package:json_god/json_god.dart' as god;
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:pool/pool.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'angel_base.dart';
|
||||
import 'controller.dart';
|
||||
|
@ -30,11 +31,13 @@ typedef Future AngelConfigurer(Angel app);
|
|||
/// A powerful real-time/REST/MVC server class.
|
||||
class Angel extends AngelBase {
|
||||
final List<Angel> _children = [];
|
||||
final Map<String, Tuple3<List, Map, Match>> handlerCache = {};
|
||||
final Map<String, Tuple3<List, Map, ParseResult<Map<String, String>>>>
|
||||
handlerCache = new HashMap();
|
||||
|
||||
Router _flattened;
|
||||
bool _isProduction = false;
|
||||
bool _isProduction;
|
||||
Angel _parent;
|
||||
Pool _pool;
|
||||
StreamSubscription<HttpRequest> _sub;
|
||||
ServerGenerator _serverGenerator = HttpServer.bind;
|
||||
|
||||
|
@ -75,7 +78,8 @@ class Angel extends AngelBase {
|
|||
/// This value is memoized the first time you call it, so do not change environment
|
||||
/// configuration at runtime!
|
||||
bool get isProduction {
|
||||
return _isProduction ??= (Platform.environment['ANGEL_ENV'] == 'production');
|
||||
return _isProduction ??=
|
||||
(Platform.environment['ANGEL_ENV'] == 'production');
|
||||
}
|
||||
|
||||
/// The function used to bind this instance to an HTTP server.
|
||||
|
@ -237,7 +241,7 @@ class Angel extends AngelBase {
|
|||
String tab: ' ',
|
||||
bool showMatchers: false}) {
|
||||
if (isProduction) {
|
||||
if (_flattened == null) _flattened = flatten(this);
|
||||
_flattened ??= flatten(this);
|
||||
|
||||
_flattened.dumpTree(
|
||||
callback: callback,
|
||||
|
@ -246,8 +250,7 @@ class Angel extends AngelBase {
|
|||
: (isProduction
|
||||
? 'Dumping flattened route tree:'
|
||||
: 'Dumping route tree:'),
|
||||
tab: tab ?? ' ',
|
||||
showMatchers: showMatchers == true);
|
||||
tab: tab ?? ' ');
|
||||
} else {
|
||||
super.dumpTree(
|
||||
callback: callback,
|
||||
|
@ -256,8 +259,7 @@ class Angel extends AngelBase {
|
|||
: (isProduction
|
||||
? 'Dumping flattened route tree:'
|
||||
: 'Dumping route tree:'),
|
||||
tab: tab ?? ' ',
|
||||
showMatchers: showMatchers == true);
|
||||
tab: tab ?? ' ');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -308,7 +310,6 @@ class Angel extends AngelBase {
|
|||
/// Runs some [handler]. Returns `true` if request execution should continue.
|
||||
Future<bool> executeHandler(
|
||||
handler, RequestContext req, ResponseContext res) async {
|
||||
if (handler == null) return false;
|
||||
var result = await getHandlerResult(handler, req, res);
|
||||
|
||||
if (result == null)
|
||||
|
@ -325,8 +326,11 @@ class Angel extends AngelBase {
|
|||
}
|
||||
|
||||
Future<RequestContext> createRequestContext(HttpRequest request) {
|
||||
return RequestContext.from(request, this).then((req) {
|
||||
_injections.forEach(req.inject);
|
||||
var path = request.uri.path.replaceAll(_straySlashes, '');
|
||||
if (path.length == 0) path = '/';
|
||||
return RequestContext.from(request, this, path).then((req) async {
|
||||
if (_pool != null) req.inject(PoolResource, await _pool.request());
|
||||
if (_injections.isNotEmpty) _injections.forEach(req.inject);
|
||||
return req;
|
||||
});
|
||||
}
|
||||
|
@ -378,53 +382,32 @@ class Angel extends AngelBase {
|
|||
Future handleRequest(HttpRequest request) async {
|
||||
var req = await createRequestContext(request);
|
||||
var res = await createResponseContext(request.response, req);
|
||||
var zoneSpec = await createZoneForRequest(request, req, res);
|
||||
var zone = Zone.current.fork(specification: zoneSpec);
|
||||
|
||||
return zone.runGuarded(() async {
|
||||
String requestedUrl;
|
||||
try {
|
||||
var path = req.path;
|
||||
if (path == '/') path = '';
|
||||
|
||||
// Faster way to get path
|
||||
List<int> _path = request.uri.path.codeUnits;
|
||||
|
||||
// Remove trailing slashes
|
||||
int lastSlash = -1;
|
||||
|
||||
for (int i = _path.length - 1; i >= 0; i--) {
|
||||
if (_path[i] == $slash)
|
||||
lastSlash = i;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (lastSlash > -1)
|
||||
requestedUrl = new String.fromCharCodes(_path.take(lastSlash));
|
||||
else
|
||||
requestedUrl = new String.fromCharCodes(_path);
|
||||
|
||||
if (requestedUrl.isEmpty) requestedUrl = '/';
|
||||
|
||||
Tuple3<List, Map, Match> resolveTuple() {
|
||||
Router r = isProduction ? (_flattened ??= flatten(this)) : this;
|
||||
Tuple3<List, Map, ParseResult<Map<String, String>>> resolveTuple() {
|
||||
Router r = _flattened ?? this;
|
||||
var resolved =
|
||||
r.resolveAll(requestedUrl, requestedUrl, method: req.method);
|
||||
r.resolveAbsolute(path, method: req.method, strip: false);
|
||||
|
||||
return new Tuple3(
|
||||
new MiddlewarePipeline(resolved).handlers,
|
||||
resolved.fold<Map>({}, (out, r) => out..addAll(r.allParams)),
|
||||
resolved.isEmpty ? null : resolved.first.route.match(requestedUrl),
|
||||
resolved.isEmpty ? null : resolved.first.parseResult,
|
||||
);
|
||||
}
|
||||
|
||||
var cacheKey = req.method + path;
|
||||
var tuple = isProduction
|
||||
? handlerCache.putIfAbsent(
|
||||
'${req.method}:$requestedUrl', resolveTuple)
|
||||
? handlerCache.putIfAbsent(cacheKey, resolveTuple)
|
||||
: resolveTuple();
|
||||
|
||||
req.inject(Zone, zone);
|
||||
req.inject(ZoneSpecification, zoneSpec);
|
||||
//req.inject(Zone, zone);
|
||||
//req.inject(ZoneSpecification, zoneSpec);
|
||||
req.params.addAll(tuple.item2);
|
||||
req.inject(Match, tuple.item3);
|
||||
req.inject(ParseResult, tuple.item3);
|
||||
|
||||
if (logger != null) req.inject(Stopwatch, new Stopwatch()..start());
|
||||
|
||||
|
@ -453,7 +436,15 @@ class Angel extends AngelBase {
|
|||
ignoreFinalizers: true,
|
||||
);
|
||||
}
|
||||
}).catchError((error, stackTrace) {
|
||||
} on FormatException catch (error, stackTrace) {
|
||||
var e = new AngelHttpException.badRequest(message: error.message);
|
||||
|
||||
if (logger != null) {
|
||||
logger.severe(e.message ?? e.toString(), error, stackTrace);
|
||||
}
|
||||
|
||||
return await handleAngelHttpException(e, stackTrace, req, res, request);
|
||||
} catch (error, stackTrace) {
|
||||
var e = new AngelHttpException(error,
|
||||
stackTrace: stackTrace, message: error?.toString());
|
||||
|
||||
|
@ -461,13 +452,10 @@ class Angel extends AngelBase {
|
|||
logger.severe(e.message ?? e.toString(), error, stackTrace);
|
||||
}
|
||||
|
||||
return handleAngelHttpException(e, stackTrace, req, res, request);
|
||||
}).whenComplete(() {
|
||||
scheduleMicrotask(() {
|
||||
req.close();
|
||||
res.dispose();
|
||||
});
|
||||
});
|
||||
return await handleAngelHttpException(e, stackTrace, req, res, request);
|
||||
} finally {
|
||||
res.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs several optimizations, *if* [isProduction] is `true`.
|
||||
|
@ -495,7 +483,7 @@ class Angel extends AngelBase {
|
|||
});
|
||||
}
|
||||
|
||||
if (_flattened == null) _flattened = flatten(this);
|
||||
_flattened ??= flatten(this);
|
||||
|
||||
_walk(_flattened);
|
||||
|
||||
|
@ -528,34 +516,34 @@ class Angel extends AngelBase {
|
|||
Future sendResponse(
|
||||
HttpRequest request, RequestContext req, ResponseContext res,
|
||||
{bool ignoreFinalizers: false}) {
|
||||
if (res.willCloseItself) {
|
||||
return new Future.value();
|
||||
} else {
|
||||
Future finalizers = ignoreFinalizers == true
|
||||
? new Future.value()
|
||||
: responseFinalizers.fold<Future>(
|
||||
new Future.value(), (out, f) => out.then((_) => f(req, res)));
|
||||
if (res.willCloseItself) return new Future.value();
|
||||
|
||||
if (res.isOpen) res.end();
|
||||
Future finalizers = ignoreFinalizers == true
|
||||
? new Future.value()
|
||||
: responseFinalizers.fold<Future>(
|
||||
new Future.value(), (out, f) => out.then((_) => f(req, res)));
|
||||
|
||||
for (var key in res.headers.keys) {
|
||||
request.response.headers.add(key, res.headers[key]);
|
||||
}
|
||||
if (res.isOpen) res.end();
|
||||
|
||||
request.response.contentLength = res.buffer.length;
|
||||
request.response.headers.chunkedTransferEncoding = res.chunked ?? true;
|
||||
for (var key in res.headers.keys) {
|
||||
request.response.headers.add(key, res.headers[key]);
|
||||
}
|
||||
|
||||
List<int> outputBuffer = res.buffer.toBytes();
|
||||
request.response.contentLength = res.buffer.length;
|
||||
request.response.headers.chunkedTransferEncoding = res.chunked ?? true;
|
||||
|
||||
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];
|
||||
});
|
||||
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];
|
||||
});
|
||||
|
||||
if (allowedEncodings != null) {
|
||||
for (var encodingName in allowedEncodings) {
|
||||
Converter<List<int>, List<int>> encoder;
|
||||
String key = encodingName;
|
||||
|
@ -574,24 +562,38 @@ class Angel extends AngelBase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.response
|
||||
..statusCode = res.statusCode
|
||||
..cookies.addAll(res.cookies)
|
||||
..add(outputBuffer);
|
||||
|
||||
return finalizers.then((_) => request.response.close()).then((_) {
|
||||
if (logger != null) {
|
||||
var sw = req.grab<Stopwatch>(Stopwatch);
|
||||
|
||||
if (sw.isRunning) {
|
||||
sw?.stop();
|
||||
logger.info("${res.statusCode} ${req.method} ${req.uri} (${sw
|
||||
?.elapsedMilliseconds ?? 'unknown'} ms)");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
request.response
|
||||
..statusCode = res.statusCode
|
||||
..cookies.addAll(res.cookies)
|
||||
..add(outputBuffer);
|
||||
|
||||
return finalizers.then((_) async {
|
||||
request.response.close();
|
||||
|
||||
if (req.injections.containsKey(PoolResource)) {
|
||||
req.injections[PoolResource].release();
|
||||
}
|
||||
|
||||
if (logger != null) {
|
||||
var sw = req.grab<Stopwatch>(Stopwatch);
|
||||
|
||||
if (sw.isRunning) {
|
||||
sw?.stop();
|
||||
logger.info("${res.statusCode} ${req.method} ${req.uri} (${sw
|
||||
?.elapsedMilliseconds ?? 'unknown'} ms)");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Limits the maximum number of requests to be handled concurrently by this instance.
|
||||
///
|
||||
/// You can optionally provide a [timeout] to limit the amount of time a request can be
|
||||
/// handled before.
|
||||
void throttle(int maxConcurrentRequests, {Duration timeout}) {
|
||||
_pool = new Pool(maxConcurrentRequests, timeout: timeout);
|
||||
}
|
||||
|
||||
/// Applies an [AngelConfigurer] to this instance.
|
||||
|
@ -651,18 +653,19 @@ class Angel extends AngelBase {
|
|||
/// Default constructor. ;)
|
||||
Angel() : super() {
|
||||
bootstrapContainer();
|
||||
createZoneForRequest = defaultZoneCreator;
|
||||
}
|
||||
|
||||
createZoneForRequest = (request, req, res) async {
|
||||
return new ZoneSpecification(
|
||||
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
|
||||
if (logger != null) {
|
||||
logger.info(line);
|
||||
} else {
|
||||
return parent.print(zone, line);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
Future<ZoneSpecification> defaultZoneCreator(request, req, res) async {
|
||||
return new ZoneSpecification(
|
||||
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
|
||||
if (logger != null) {
|
||||
logger.info(line);
|
||||
} else {
|
||||
return parent.print(zone, line);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// An instance mounted on a server started by the [serverGenerator].
|
||||
|
@ -696,4 +699,4 @@ class Angel extends AngelBase {
|
|||
|
||||
return new Angel.fromSecurityContext(serverContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,11 @@ class Providers {
|
|||
|
||||
@override
|
||||
bool operator ==(other) => other is Providers && other.via == via;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'via:$via';
|
||||
}
|
||||
}
|
||||
|
||||
/// A front-facing interface that can present data to and operate on data on behalf of the user.
|
||||
|
|
|
@ -22,7 +22,8 @@ class AngelMetrics extends Angel {
|
|||
res.contentType = ContentType.HTML;
|
||||
|
||||
var rows = stats.all.map((stat) {
|
||||
return '''<tr>
|
||||
return '''
|
||||
<tr>
|
||||
<td>${stat.name}</td>
|
||||
<td>${stat.iterations}</td>
|
||||
<td>${stat.sum}ms</td>
|
||||
|
@ -107,6 +108,13 @@ class AngelMetrics extends Angel {
|
|||
() => super.createResponseContext(response, correspondingRequest));
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<RoutingResult> resolveAll(String absolute, String relative,
|
||||
{String method: 'GET', bool strip: true}) {
|
||||
return stats.resolveAll
|
||||
.run(() => super.resolveAll(absolute, relative, method: method, strip: strip));
|
||||
}
|
||||
|
||||
@override
|
||||
Future handleRequest(HttpRequest request) {
|
||||
return stats.handleRequest.run(() async {
|
||||
|
@ -146,6 +154,7 @@ class AngelMetricsStats {
|
|||
all = [
|
||||
createRequestContext,
|
||||
createResponseContext,
|
||||
resolveAll,
|
||||
executeHandler,
|
||||
getHandlerResult,
|
||||
runContained,
|
||||
|
@ -156,6 +165,7 @@ class AngelMetricsStats {
|
|||
|
||||
final Stats createRequestContext = new Stats('createRequestContext');
|
||||
final Stats createResponseContext = new Stats('createResponseContext');
|
||||
final Stats resolveAll = new Stats('resolveAll');
|
||||
final Stats handleRequest = new Stats('handleRequest');
|
||||
final Stats executeHandler = new Stats('executeHandler');
|
||||
final Stats getHandlerResult = new Stats('getHandlerResult');
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'dart:mirrors';
|
||||
|
||||
final RegExp straySlashes = new RegExp(r'(^/+)|(/+$)');
|
||||
|
||||
matchingAnnotation(List<InstanceMirror> metadata, Type T) {
|
||||
for (InstanceMirror metaDatum in metadata) {
|
||||
if (metaDatum.hasReflectee) {
|
||||
|
|
|
@ -5,20 +5,39 @@ import 'dart:async';
|
|||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/metrics.dart';
|
||||
|
||||
main() {
|
||||
main() async {
|
||||
var isolates = <Isolate>[];
|
||||
|
||||
for (int i = 0; i < Platform.numberOfProcessors; i++) {
|
||||
Isolate.spawn(start, i + 1);
|
||||
isolates.add(await Isolate.spawn(start, i + 1));
|
||||
}
|
||||
|
||||
start(0);
|
||||
await Future.wait(isolates.map((i) {
|
||||
var rcv = new ReceivePort();
|
||||
i.addOnExitListener(rcv.sendPort);
|
||||
return rcv.first;
|
||||
}));
|
||||
//start(0);
|
||||
}
|
||||
|
||||
void start(int id) {
|
||||
var app = new AngelMetrics.custom(startShared)
|
||||
..lazyParseBodies = true
|
||||
..get('/', (req, res) => res.write('Hello, world!'));
|
||||
var app = new Angel.custom(startShared)..lazyParseBodies = true;
|
||||
|
||||
if (true) {
|
||||
app.get('/', (req, ResponseContext res) {
|
||||
res.willCloseItself = true;
|
||||
res.io
|
||||
..write('Hello, world!')
|
||||
..close();
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
app.get('/', (req, ResponseContext res) {
|
||||
res.useStream();
|
||||
res.write('Hello, world!');
|
||||
});
|
||||
}
|
||||
|
||||
var oldHandler = app.errorHandler;
|
||||
app.errorHandler = (e, req, res) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: angel_framework
|
||||
version: 1.1.0-alpha+8
|
||||
version: 1.1.0-alpha+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
|
||||
|
@ -8,17 +8,17 @@ environment:
|
|||
dependencies:
|
||||
angel_http_exception: ^1.0.0
|
||||
angel_model: ^1.0.0
|
||||
angel_route: ">=1.0.5 <2.0.0"
|
||||
angel_route: ^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
|
||||
logging: ">=0.11.3 <1.0.0"
|
||||
matcher: ^0.12.0
|
||||
merge_map: ^1.0.0
|
||||
meta: ^1.0.0
|
||||
mime: ^0.9.3
|
||||
pool: ^1.0.0
|
||||
random_string: ^0.0.1
|
||||
tuple: ^1.0.0
|
||||
dev_dependencies:
|
||||
|
|
|
@ -34,6 +34,12 @@ main() {
|
|||
expect(req.accepts('text/html'), isTrue);
|
||||
});
|
||||
|
||||
test('strict', () async {
|
||||
var req = await acceptContentTypes(['text/html', "*/*"]);
|
||||
expect(req.accepts(ContentType.HTML), isTrue);
|
||||
expect(req.accepts(ContentType.JSON, strict: true), isFalse);
|
||||
});
|
||||
|
||||
group('disallow null', () {
|
||||
RequestContext req;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ 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 'extension_test.dart' as extension;
|
||||
import 'general_test.dart' as general;
|
||||
import 'hooked_test.dart' as hooked;
|
||||
import 'parameter_meta_test.dart' as parameter_meta;
|
||||
|
@ -27,6 +28,7 @@ main() {
|
|||
group('di', di.main);
|
||||
group('encoders_buffer', encoders_buffer.main);
|
||||
group('exception', exception.main);
|
||||
group('extension', extension.main);
|
||||
group('general', general.main);
|
||||
group('hooked', hooked.main);
|
||||
group('parameter_meta', parameter_meta.main);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_route/angel_route.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:test/test.dart';
|
||||
import 'common.dart';
|
||||
|
|
29
test/extension_test.dart
Normal file
29
test/extension_test.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
import 'dart:async';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:mock_request/mock_request.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final Uri ENDPOINT = Uri.parse('http://example.com');
|
||||
|
||||
main() {
|
||||
test('single extension', () async {
|
||||
var req = await makeRequest('foo.js');
|
||||
expect(req.extension, '.js');
|
||||
});
|
||||
|
||||
test('multiple extensions', () async {
|
||||
var req = await makeRequest('foo.min.js');
|
||||
expect(req.extension, '.js');
|
||||
});
|
||||
|
||||
test('no extension', () async {
|
||||
var req = await makeRequest('foo');
|
||||
expect(req.extension, '');
|
||||
});
|
||||
}
|
||||
|
||||
Future<RequestContext> makeRequest(String path) {
|
||||
var rq = new MockHttpRequest('GET', ENDPOINT.replace(path: path))..close();
|
||||
var app = new Angel();
|
||||
return app.createRequestContext(rq);
|
||||
}
|
|
@ -49,8 +49,8 @@ main() {
|
|||
|
||||
ted = nested.post('/ted/:route', (RequestContext req, res) {
|
||||
print('Params: ${req.params}');
|
||||
print(
|
||||
'Path: ${ted.path}, matcher: ${ted.matcher.pattern}, uri: ${req.path}');
|
||||
print('Path: ${ted.path}, uri: ${req.path}');
|
||||
print('matcher: ${ted.parser}');
|
||||
return req.params;
|
||||
});
|
||||
|
||||
|
@ -65,7 +65,7 @@ main() {
|
|||
app
|
||||
.get('/greet/:name',
|
||||
(RequestContext req, res) async => "Hello ${req.params['name']}")
|
||||
.as('Named routes');
|
||||
.name = 'Named routes';
|
||||
app.get('/named', (req, ResponseContext res) async {
|
||||
res.redirectTo('Named routes', {'name': 'tests'});
|
||||
});
|
||||
|
@ -86,7 +86,9 @@ main() {
|
|||
};
|
||||
}
|
||||
|
||||
app.chain(write('a')).chain([write('b'), write('c')]).get('/chained', () => false);
|
||||
app
|
||||
.chain(write('a'))
|
||||
.chain([write('b'), write('c')]).get('/chained', () => false);
|
||||
|
||||
app.use('MJ');
|
||||
|
||||
|
@ -165,7 +167,7 @@ main() {
|
|||
});
|
||||
|
||||
test('Can name routes', () {
|
||||
Route foo = new Route('/framework/:id', name: 'frm');
|
||||
Route foo = app.get('/framework/:id', [])..name = 'frm';
|
||||
print('Foo: $foo');
|
||||
String uri = foo.makeUri({'id': 'angel'});
|
||||
print(uri);
|
||||
|
|
Loading…
Reference in a new issue