2.0.0-alpha.15

This commit is contained in:
Tobe O 2018-12-09 10:49:59 -05:00
parent d3c2192042
commit f95de91bf5
15 changed files with 203 additions and 169 deletions

View file

@ -1,3 +1,10 @@
# 2.0.0-alpha.15
* Remove dependency on `body_parser`.
* `RequestContext` now exposes a `Stream<List<int>> get body` getter.
* Calling `RequestContext.parseBody()` parses its contents.
* Added `bodyAsMap`, `bodyAsList`, `bodyAsObject`, and `uploadedFiles` to `RequestContext`.
* Removed `Angel.keepRawRequestBuffers` and anything that had to do with buffering request bodies.
# 2.0.0-alpha.14
* Patch `HttpResponseContext._openStream` to send content-length.

View file

@ -16,7 +16,7 @@ main() async {
app.get('/', (req, res) => res.streamFile(indexHtml));
app.post('/', (req, res) => req.parseBody());
app.post('/', (req, res) => req.parseBody().then((_) => req.bodyAsMap));
var ctx = new SecurityContext()
..useCertificateChain('dev.pem')

View file

@ -16,7 +16,7 @@ main() async {
app.get('/', (req, res) => 'Hello HTTP/2!!!');
app.fallback((req, res) => throw new AngelHttpException.notFound(
message: 'No file exists at ${req.uri.path}'));
message: 'No file exists at ${req.uri}'));
var ctx = new SecurityContext()
..useCertificateChain('dev.pem')

View file

@ -10,7 +10,7 @@ main() async {
var app = new Angel();
app.logger = new Logger('angel')..onRecord.listen(prettyLog);
var publicDir = new Directory('example/public');
var publicDir = new Directory('example/http2/public');
var indexHtml =
const LocalFileSystem().file(publicDir.uri.resolve('index.html'));
var styleCss =

View file

@ -32,7 +32,8 @@ main() async {
serverMain(_) async {
var app = new Angel();
var http = new AngelHttp.custom(app, startShared, useZone: false); // Run a cluster
var http =
new AngelHttp.custom(app, startShared, useZone: false); // Run a cluster
app.get('/', (req, res) {
return res.serialize({

View file

@ -2,12 +2,12 @@ library angel_framework.http.request_context;
import 'dart:async';
import 'dart:convert';
import 'dart:io' show Cookie, HttpHeaders, HttpSession, InternetAddress;
import 'dart:io'
show Cookie, HeaderValue, HttpHeaders, HttpSession, InternetAddress;
import 'package:angel_container/angel_container.dart';
import 'package:http_parser/http_parser.dart';
import 'package:http_server/http_server.dart';
import 'package:meta/meta.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart' as p;
@ -21,11 +21,11 @@ part 'injection.dart';
/// A convenience wrapper around an incoming [RawRequest].
abstract class RequestContext<RawRequest> {
String _acceptHeaderCache, _extensionCache;
bool _acceptsAllCache, _hasParsedBody;
bool _acceptsAllCache, _hasParsedBody = false;
Map<String, dynamic> _bodyFields, _queryParameters;
List _bodyList;
Object _bodyObject;
List<HttpMultipartFormData> _bodyFiles;
List<UploadedFile> _uploadedFiles;
MediaType _contentType;
/// The underlying [RawRequest] provided by the driver.
@ -97,7 +97,7 @@ abstract class RequestContext<RawRequest> {
/// Returns a *mutable* [Map] of the fields parsed from the request [body].
///
/// Note that [parseBody] must be called first.
Map<String, dynamic> get bodyFields {
Map<String, dynamic> get bodyAsMap {
if (!hasParsedBody) {
throw new StateError('The request body has not been parsed yet.');
} else if (_bodyFields == null) {
@ -110,7 +110,7 @@ abstract class RequestContext<RawRequest> {
/// Returns a *mutable* [List] parsed from the request [body].
///
/// Note that [parseBody] must be called first.
List get bodyList {
List get bodyAsList {
if (!hasParsedBody) {
throw new StateError('The request body has not been parsed yet.');
} else if (_bodyList == null) {
@ -123,7 +123,7 @@ abstract class RequestContext<RawRequest> {
/// Returns the parsed request body, whatever it may be (typically a [Map] or [List]).
///
/// Note that [parseBody] must be called first.
Object get bodyObject {
Object get bodyAsObject {
if (!hasParsedBody) {
throw new StateError('The request body has not been parsed yet.');
}
@ -134,12 +134,12 @@ abstract class RequestContext<RawRequest> {
/// Returns a *mutable* map of the files parsed from the request [body].
///
/// Note that [parseBody] must be called first.
List<HttpMultipartFormData> get bodyFiles {
List<UploadedFile> get uploadedFiles {
if (!hasParsedBody) {
throw new StateError('The request body has not been parsed yet.');
}
return _bodyFiles;
return _uploadedFiles;
}
/// Returns a *mutable* map of the fields contained in the query.
@ -189,7 +189,7 @@ abstract class RequestContext<RawRequest> {
_hasParsedBody = true;
if (contentType.type == 'application' && contentType.subtype == 'json') {
_bodyFiles = [];
_uploadedFiles = [];
var parsed = _bodyObject =
await body.transform(encoding.decoder).join().then(json.decode);
@ -199,6 +199,14 @@ abstract class RequestContext<RawRequest> {
} else if (parsed is List) {
_bodyList = parsed;
}
} else if (contentType.type == 'application' &&
contentType.subtype == 'x-www-form-urlencoded') {
_uploadedFiles = [];
var parsed = await body
.transform(encoding.decoder)
.join()
.then((s) => Uri.splitQueryString(s, encoding: encoding));
_bodyFields = new Map<String, dynamic>.from(parsed);
} else if (contentType.type == 'multipart' &&
contentType.subtype == 'form-data' &&
contentType.parameters.containsKey('boundary')) {
@ -207,11 +215,11 @@ abstract class RequestContext<RawRequest> {
var parts = body.transform(transformer).map((part) =>
HttpMultipartFormData.parse(part, defaultEncoding: encoding));
_bodyFields = {};
_bodyFiles = [];
_uploadedFiles = [];
await for (var part in parts) {
if (part.isBinary) {
_bodyFiles.add(part);
_uploadedFiles.add(new UploadedFile(part));
} else if (part.isText &&
part.contentDisposition.parameters.containsKey('name')) {
// If there is no name, then don't parse it.
@ -222,7 +230,7 @@ abstract class RequestContext<RawRequest> {
}
} else {
_bodyFields = {};
_bodyFiles = [];
_uploadedFiles = [];
}
}
}
@ -236,3 +244,35 @@ abstract class RequestContext<RawRequest> {
return new Future.value();
}
}
/// Reads information about a binary chunk uploaded to the server.
class UploadedFile {
/// The underlying `form-data` item.
final HttpMultipartFormData formData;
MediaType _contentType;
UploadedFile(this.formData);
/// Returns the binary stream from [formData].
Stream<List<int>> get data => formData.cast<List<int>>();
/// The filename associated with the data on the user's system.
/// Returns [:null:] if not present.
String get filename => formData.contentDisposition.parameters['filename'];
/// The name of the field associated with this data.
/// Returns [:null:] if not present.
String get name => formData.contentDisposition.parameters['name'];
/// The parsed [:Content-Type:] header of the [:HttpMultipartFormData:].
/// Returns [:null:] if not present.
MediaType get contentType => _contentType ??= (formData.contentType == null
? null
: new MediaType.parse(formData.contentType.toString()));
/// The parsed [:Content-Transfer-Encoding:] header of the
/// [:HttpMultipartFormData:]. This field is used to determine how to decode
/// the data. Returns [:null:] if not present.
HeaderValue get contentTransferEncoding => formData.contentTransferEncoding;
}

View file

@ -114,11 +114,6 @@ class Angel extends Routable {
/// for you.
final Map configuration = {};
/// When set to `true` (default: `false`), the request body will be parsed
/// automatically; otherwise, you must call [RequestContext].parseBody() manually,
/// or use `lazyBody()`.
bool eagerParseRequestBodies = false;
/// A function that renders views.
///
/// Called by [ResponseContext]@`render`.
@ -358,7 +353,6 @@ class Angel extends Routable {
Angel(
{Reflector reflector: const EmptyReflector(),
this.logger,
this.eagerParseRequestBodies: false,
this.allowMethodOverrides: true,
this.serializer,
this.viewGenerator})

View file

@ -200,13 +200,11 @@ class Service<Id, Data> extends Routable {
Middleware indexMiddleware =
getAnnotation(service.index, Middleware, app.container.reflector);
get('/', (req, res) {
return req.parseQuery().then((query) {
return this.index(mergeMap([
{'query': query},
restProvider,
req.serviceParams
]));
});
return this.index(mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: <RequestHandler>[]
..addAll(handlers)
@ -215,20 +213,18 @@ class Service<Id, Data> extends Routable {
Middleware createMiddleware =
getAnnotation(service.create, Middleware, app.container.reflector);
post('/', (req, ResponseContext res) {
return req.parseQuery().then((query) {
return req.parseBody().then((body) {
return this
.create(
body as Data,
mergeMap([
{'query': query},
restProvider,
req.serviceParams
]))
.then((r) {
res.statusCode = 201;
return r;
});
return req.parseBody().then((_) {
return this
.create(
req.bodyAsMap as Data,
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]))
.then((r) {
res.statusCode = 201;
return r;
});
});
},
@ -241,15 +237,13 @@ class Service<Id, Data> extends Routable {
getAnnotation(service.read, Middleware, app.container.reflector);
get('/:id', (req, res) {
return req.parseQuery().then((query) {
return this.read(
parseId<Id>(req.params['id']),
mergeMap([
{'query': query},
restProvider,
req.serviceParams
]));
});
return this.read(
parseId<Id>(req.params['id']),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: []
..addAll(handlers)
@ -257,20 +251,18 @@ class Service<Id, Data> extends Routable {
Middleware modifyMiddleware =
getAnnotation(service.modify, Middleware, app.container.reflector);
patch(
'/:id',
(req, res) => req.parseBody().then((body) {
return req.parseQuery().then((query) {
return this.modify(
parseId<Id>(req.params['id']),
body as Data,
mergeMap([
{'query': query},
restProvider,
req.serviceParams
]));
});
}),
patch('/:id', (req, res) {
return req.parseBody().then((_) {
return this.modify(
parseId<Id>(req.params['id']),
req.bodyAsMap as Data,
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
});
},
middleware: []
..addAll(handlers)
..addAll(
@ -278,38 +270,34 @@ class Service<Id, Data> extends Routable {
Middleware updateMiddleware =
getAnnotation(service.update, Middleware, app.container.reflector);
post(
'/:id',
(req, res) => req.parseBody().then((body) {
return req.parseQuery().then((query) {
return this.update(
parseId<Id>(req.params['id']),
body as Data,
mergeMap([
{'query': query},
restProvider,
req.serviceParams
]));
});
}),
post('/:id', (req, res) {
return req.parseBody().then((_) {
return this.update(
parseId<Id>(req.params['id']),
req.bodyAsMap as Data,
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
});
},
middleware: []
..addAll(handlers)
..addAll(
(updateMiddleware == null) ? [] : updateMiddleware.handlers));
put(
'/:id',
(req, res) => req.parseBody().then((body) {
return req.parseQuery().then((query) {
return this.update(
parseId<Id>(req.params['id']),
body as Data,
mergeMap([
{'query': query},
restProvider,
req.serviceParams
]));
});
}),
put('/:id', (req, res) {
return req.parseBody().then((_) {
return this.update(
parseId<Id>(req.params['id']),
req.bodyAsMap as Data,
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
});
},
middleware: []
..addAll(handlers)
..addAll(
@ -318,30 +306,26 @@ class Service<Id, Data> extends Routable {
Middleware removeMiddleware =
getAnnotation(service.remove, Middleware, app.container.reflector);
delete('/', (req, res) {
return req.parseQuery().then((query) {
return this.remove(
null,
mergeMap([
{'query': query},
restProvider,
req.serviceParams
]));
});
return this.remove(
null,
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: []
..addAll(handlers)
..addAll(
(removeMiddleware == null) ? [] : removeMiddleware.handlers));
delete('/:id', (req, res) {
return req.parseQuery().then((query) {
return this.remove(
parseId<Id>(req.params['id']),
mergeMap([
{'query': query},
restProvider,
req.serviceParams
]));
});
return this.remove(
parseId<Id>(req.params['id']),
mergeMap([
{'query': req.queryParameters},
restProvider,
req.serviceParams
]));
},
middleware: []
..addAll(handlers)

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:io';
import 'package:angel_container/angel_container.dart';
import 'package:body_parser/body_parser.dart';
import 'package:http_parser/http_parser.dart';
import '../core/core.dart';
@ -40,6 +39,9 @@ class HttpRequestContext extends RequestContext<HttpRequest> {
/// The underlying [HttpRequest] instance underneath this context.
HttpRequest get rawRequest => _io;
@override
Stream<List<int>> get body => _io;
@override
String get method {
return _override ?? originalMethod;
@ -120,10 +122,6 @@ class HttpRequestContext extends RequestContext<HttpRequest> {
ctx._path = path;
ctx._io = request;
if (app.eagerParseRequestBodies == true) {
return ctx.parse().then((_) => ctx);
}
return new Future.value(ctx);
}
@ -134,15 +132,4 @@ class HttpRequestContext extends RequestContext<HttpRequest> {
_override = _path = null;
return super.close();
}
@override
Future<BodyParseResult> parseOnce() {
return parseBodyFromStream(
rawRequest,
rawRequest.headers.contentType != null
? new MediaType.parse(rawRequest.headers.contentType.toString())
: null,
rawRequest.uri,
storeOriginalBuffer: app.keepRawRequestBuffers == true);
}
}

View file

@ -3,8 +3,6 @@ import 'dart:convert';
import 'dart:io';
import 'package:angel_container/src/container.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:body_parser/body_parser.dart';
import 'package:http_parser/http_parser.dart';
import 'package:http2/transport.dart';
import 'package:mock_request/mock_request.dart';
import 'package:uuid/uuid.dart';
@ -13,8 +11,8 @@ final RegExp _comma = new RegExp(r',\s*');
final RegExp _straySlashes = new RegExp(r'(^/+)|(/+$)');
class Http2RequestContext extends RequestContext<ServerTransportStream> {
final StreamController<List<int>> _body = new StreamController();
final Container container;
BytesBuilder _buf;
List<Cookie> _cookies;
HttpHeaders _headers;
String _method, _override, _path;
@ -25,27 +23,48 @@ class Http2RequestContext extends RequestContext<ServerTransportStream> {
Http2RequestContext._(this.container);
@override
Stream<List<int>> get body => _body.stream;
static Future<Http2RequestContext> from(
ServerTransportStream stream,
Socket socket,
Angel app,
Map<String, MockHttpSession> sessions,
Uuid uuid) async {
var c = new Completer<Http2RequestContext>();
var req = new Http2RequestContext._(app.container.createChild())
..app = app
.._socket = socket
.._stream = stream;
var buf = req._buf = new BytesBuilder();
var headers = req._headers = new MockHttpHeaders();
String scheme = 'https',
authority = '${socket.address.address}:${socket.port}',
path = '';
String scheme = 'https', host = socket.address.address, path = '';
int port = socket.port;
var cookies = <Cookie>[];
await for (var msg in stream.incomingMessages) {
void finalize() {
req
.._cookies = new List.unmodifiable(cookies)
.._uri = new Uri(scheme: scheme, host: host, port: port, path: path);
if (!c.isCompleted) c.complete(req);
}
void parseHost(String value) {
var uri = Uri.tryParse(value);
if (uri == null || uri.scheme == 'localhost') return;
scheme = uri.hasScheme ? uri.scheme : scheme;
if (uri.hasAuthority) {
host = uri.host;
port = uri.hasPort ? uri.port : null;
}
}
stream.incomingMessages.listen((msg) {
if (msg is DataStreamMessage) {
buf.add(msg.bytes);
if (!c.isCompleted) finalize();
req._body.add(msg.bytes);
} else if (msg is HeadersStreamMessage) {
for (var header in msg.headers) {
var name = ascii.decode(header.name).toLowerCase();
@ -64,7 +83,7 @@ class Http2RequestContext extends RequestContext<ServerTransportStream> {
scheme = value;
break;
case ':authority':
authority = value;
parseHost(value);
break;
case 'cookie':
var cookieStrings = value.split(';').map((s) => s.trim());
@ -78,18 +97,22 @@ class Http2RequestContext extends RequestContext<ServerTransportStream> {
}
break;
default:
headers.add(ascii.decode(header.name), value.split(_comma));
var name = ascii.decode(header.name).toLowerCase();
if (name == 'host') {
parseHost(value);
}
headers.add(name, value.split(_comma));
break;
}
}
if (msg.endStream && !c.isCompleted) finalize();
}
//if (msg.endStream) break;
}
req
.._cookies = new List.unmodifiable(cookies)
.._uri = Uri.parse('$scheme://$authority').replace(path: path);
}, onDone: () {
if (!c.isCompleted) finalize();
}, cancelOnError: true, onError: c.completeError);
// Apply session
var dartSessId =
@ -104,7 +127,7 @@ class Http2RequestContext extends RequestContext<ServerTransportStream> {
() => new MockHttpSession(id: dartSessId.value),
);
return req;
return c.future;
}
@override
@ -147,19 +170,10 @@ class Http2RequestContext extends RequestContext<ServerTransportStream> {
@override
Future close() {
_body.close();
return super.close();
}
@override
Future<BodyParseResult> parseOnce() {
return parseBodyFromStream(
new Stream.fromIterable([_buf.takeBytes()]),
contentType == null ? null : new MediaType.parse(contentType.toString()),
uri,
storeOriginalBuffer: app.keepRawRequestBuffers == true,
);
}
@override
ServerTransportStream get rawRequest => _stream;
}

View file

@ -1,5 +1,5 @@
name: angel_framework
version: 2.0.0-alpha.14
version: 2.0.0-alpha.15
description: A high-powered HTTP server with dependency injection, routing and much more.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/angel_framework

View file

@ -25,9 +25,7 @@ void main() {
Uri serverRoot;
setUp(() async {
app = new Angel()
..keepRawRequestBuffers = true
..encoders['gzip'] = gzip.encoder;
app = new Angel()..encoders['gzip'] = gzip.encoder;
app.get('/', (req, res) async {
res.write('Hello world');
@ -50,13 +48,17 @@ void main() {
await res.close();
});
app.post('/body', (req, res) => req.parseBody());
app.post('/body', (req, res) => req.parseBody().then((_) => req.bodyAsMap));
app.post('/upload', (req, res) async {
var body = await req.parseBody(), files = await req.parseUploadedFiles();
stdout.add(await req.parseRawRequestBuffer());
await req.parseBody();
var body = req.bodyAsMap, files = req.uploadedFiles;
var file = files.firstWhere((f) => f.name == 'file');
return [file.data.length, file.mimeType, body];
return [
await file.data.map((l) => l.length).reduce((a, b) => a + b),
file.contentType.mimeType,
body
];
});
app.get('/push', (req, res) async {

View file

@ -38,7 +38,7 @@ class Http2Client extends BaseClient {
headers.add(new Header.ascii(k, v));
});
var stream = await connection.makeRequest(headers);
var stream = await connection.makeRequest(headers, endStream: body.isEmpty);
if (body.isNotEmpty) {
stream.sendData(body, endStream: true);

View file

@ -75,7 +75,10 @@ main() {
middleware: [interceptor]);
app.get('/hello', (req, res) => 'world');
app.get('/name/:first/last/:last', (req, res) => req.params);
app.post('/lambda', (RequestContext req, res) => req.parseBody());
app.post(
'/lambda',
(RequestContext req, res) =>
req.parseBody().then((_) => req.bodyAsMap));
app.mount('/todos/:id', todos);
app
.get('/greet/:name',
@ -85,7 +88,7 @@ main() {
res.redirectTo('Named routes', {'name': 'tests'});
});
app.get('/log', (RequestContext req, res) async {
print("Query: ${await req.parseQuery()}");
print("Query: ${req.queryParameters}");
return "Logged";
});

View file

@ -77,6 +77,7 @@ main() {
await client.post("$url/todos",
headers: headers as Map<String, String>, body: postData);
postData = json.encode({'text': 'modified'});
var response = await client.patch("$url/todos/0",
headers: headers as Map<String, String>, body: postData);
expect(response.statusCode, 200);
@ -90,6 +91,7 @@ main() {
await client.post("$url/todos",
headers: headers as Map<String, String>, body: postData);
postData = json.encode({'over': 'write'});
var response = await client.post("$url/todos/0",
headers: headers as Map<String, String>, body: postData);
expect(response.statusCode, 200);