Init shelf driver

This commit is contained in:
Tobe O 2019-10-14 11:28:50 -04:00
parent aacd91e267
commit b115aae975
11 changed files with 160 additions and 84 deletions

View file

@ -1,3 +1,7 @@
# 2.1.0
* `pedantic` lints.
* Add the `AngelShelf` driver class, which allows you to embed Angel within shelf.
# 2.0.0
* Removed `supportShelf`.

View file

@ -30,8 +30,8 @@ import 'package:shelf/shelf.dart' as shelf;
import 'api/api.dart';
main() async {
var app = new Angel();
var http = new AngelHttp(app);
var app = Angel();
var http = AngelHttp(app);
// Angel routes on top
await app.mountController<ApiController>();
@ -39,7 +39,7 @@ main() async {
// Re-route all other traffic to an
// existing application.
app.fallback(embedShelf(
new shelf.Pipeline()
shelf.Pipeline()
.addMiddleware(shelf.logRequests())
.addHandler(_echoRequest)
));
@ -62,7 +62,7 @@ handleRequest(shelf.Request request) {
var req = request.context['angel_shelf.request'] as RequestContext;
// ... And then interact with it.
req.container.registerNamedSingleton<Foo>('from_shelf', new Foo());
req.container.registerNamedSingleton<Foo>('from_shelf', Foo());
// `req.container` is also available.
var container = request.context['angel_shelf.container'] as Container;

View file

@ -1,3 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false
implicit-casts: false

View file

@ -2,11 +2,17 @@ import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_shelf/angel_shelf.dart';
import 'package:logging/logging.dart';
import 'package:pretty_logging/pretty_logging.dart';
import 'package:shelf_static/shelf_static.dart';
main() async {
var app = new Angel();
var http = new AngelHttp(app);
Logger.root
..level = Level.ALL
..onRecord.listen(prettyLog);
var app = Angel(logger: Logger('angel_shelf_demo'));
var http = AngelHttp(app);
// `shelf` request handler
var shelfHandler = createStaticHandler('.',
@ -21,9 +27,9 @@ main() async {
return false; // End execution of handlers, so we don't proxy to dartlang.org when we don't need to.
});
// Proxy any other request through to the static file handler
// Pass any other request through to the static file handler
app.fallback(wrappedHandler);
await http.startServer(InternetAddress.loopbackIPv4, 8080);
print('Proxying at ${http.uri}');
print('Running at ${http.uri}');
}

View file

@ -1,15 +1,13 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_framework/http2.dart';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart' as shelf;
import 'package:stream_channel/stream_channel.dart';
/// Creates a [shelf.Request] analogous to the input [req].
///
/// The new request's `context` will contain [req.container] as `angel_shelf.container`, as well as
/// The request's `context` will contain [req.container] as `angel_shelf.container`, as well as
/// the provided [context], if any.
///
/// The context will also have the original request available as `angel_shelf.request`.
@ -30,47 +28,26 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
String protocolVersion;
Uri requestedUri;
if (req is HttpRequestContext && res is HttpResponseContext) {
protocolVersion = req.rawRequest.protocolVersion;
requestedUri = req.rawRequest.requestedUri;
protocolVersion = '1.1';
requestedUri = Uri.parse('http://${req.hostname}');
requestedUri = requestedUri.replace(path: req.uri.path);
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
new Future(() async {
var rs = res.detach();
var socket = await rs.detachSocket(writeHeaders: false);
var ctrl = new StreamChannelController<List<int>>();
var body = await req.parseRawRequestBuffer() ?? [];
ctrl.local.sink.add(body ?? []);
socket.listen(ctrl.local.sink.add,
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
Future.sync(res.detach).then((_) {
var ctrl = StreamChannelController<List<int>>();
if (req.hasParsedBody) {
req.body.listen(ctrl.local.sink.add,
onError: ctrl.local.sink.addError, onDone: ctrl.local.sink.close);
ctrl.local.stream.pipe(socket);
hijack(ctrl.foreign);
}).catchError((e, st) {
app.logger?.severe('An error occurred while hijacking a shelf request',
e, st as StackTrace);
});
};
} else if (req is Http2RequestContext && res is Http2ResponseContext) {
protocolVersion = '2.0';
requestedUri = req.uri;
onHijack = (void hijack(StreamChannel<List<int>> channel)) {
new Future(() async {
var rs = await res.detach();
var ctrl = new StreamChannelController<List<int>>();
var body = await req.parseRawRequestBuffer() ?? [];
ctrl.local.sink.add(body ?? []);
ctrl.local.stream.listen(rs.sendData, onDone: rs.terminate);
hijack(ctrl.foreign);
}).catchError((e, st) {
stderr.writeln('An error occurred while hijacking a shelf request: $e');
stderr.writeln(st);
});
};
} else {
throw new UnsupportedError(
'`embedShelf` is only supported for HTTP and HTTP2 requests in Angel.');
}
} else {
ctrl.local.sink.close();
}
ctrl.local.stream.pipe(res);
hijack(ctrl.foreign);
}).catchError((e, st) {
app.logger?.severe('An error occurred while hijacking a shelf request', e,
st as StackTrace);
});
};
var url = req.uri;
@ -78,12 +55,12 @@ Future<shelf.Request> convertRequest(RequestContext req, ResponseContext res,
url = url.replace(path: url.path.substring(1));
}
return new shelf.Request(req.method, requestedUri,
return shelf.Request(req.method, requestedUri,
protocolVersion: protocolVersion,
headers: headers,
handlerPath: handlerPath,
url: url,
body: (await req.parseRawRequestBuffer()) ?? [],
body: req.body,
context: {'angel_shelf.request': req}
..addAll({'angel_shelf.container': req.container})
..addAll(context ?? {}),

View file

@ -13,19 +13,21 @@ import 'convert.dart';
RequestHandler embedShelf(shelf.Handler handler,
{String handlerPath,
Map<String, Object> context,
bool throwOnNullResponse: false}) {
bool throwOnNullResponse = false}) {
return (RequestContext req, ResponseContext res) async {
var shelfRequest = await convertRequest(req, res,
handlerPath: handlerPath, context: context);
try {
var result = await handler(shelfRequest);
if (result is! shelf.Response && result != null) return result;
if (result == null && throwOnNullResponse == true)
throw new AngelHttpException('Internal Server Error');
if (result == null && throwOnNullResponse == true) {
throw AngelHttpException('Internal Server Error');
}
await mergeShelfResponse(result, res);
return false;
} on shelf.HijackException {
// On hijack, do nothing, because the hijack handlers already call res.detach();
return null;
} catch (e) {
rethrow;
}

View file

View file

@ -0,0 +1,77 @@
import 'dart:async';
import 'dart:io';
import 'package:angel_container/angel_container.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:mock_request/mock_request.dart';
import 'package:shelf/shelf.dart' as shelf;
class ShelfRequestContext extends RequestContext {
@override
final Angel app;
@override
final Container container;
@override
final shelf.Request rawRequest;
@override
final String path;
List<Cookie> _cookies;
@override
final MockHttpHeaders headers = MockHttpHeaders();
ShelfRequestContext(this.app, this.container, this.rawRequest, this.path) {
rawRequest.headers.forEach(headers.add);
}
@override
Stream<List<int>> get body => rawRequest.read();
@override
List<Cookie> get cookies {
if (_cookies == null) {
// Parse cookies
_cookies = [];
var value = headers.value('cookie');
if (value != null) {
var cookieStrings = value.split(';').map((s) => s.trim());
for (var cookieString in cookieStrings) {
try {
_cookies.add(Cookie.fromSetCookieValue(cookieString));
} catch (_) {
// Ignore malformed cookies, and just don't add them to the container.
}
}
}
}
return _cookies;
}
@override
String get hostname => rawRequest.headers['host'];
@override
String get method {
if (!app.allowMethodOverrides) {
return originalMethod;
} else {
return headers.value('x-http-method-override')?.toUpperCase() ??
originalMethod;
}
}
@override
String get originalMethod => rawRequest.method;
@override
// TODO: implement remoteAddress
InternetAddress get remoteAddress => null;
@override
// TODO: implement session
HttpSession get session => null;
@override
Uri get uri => rawRequest.url;
}

View file

View file

@ -2,7 +2,7 @@ author: Tobe O <thosakwe@gmail.com>
description: Shelf interop with Angel. Use this to wrap existing server code.
homepage: https://github.com/angel-dart/shelf
name: angel_shelf
version: 2.0.0
version: 2.1.0
dependencies:
angel_framework: ^2.0.0-alpha
path: ^1.0.0
@ -10,6 +10,8 @@ dependencies:
stream_channel: ^1.0.0
dev_dependencies:
angel_test: ^2.0.0-alpha
pedantic: ^1.0.0
pretty_logging: ^1.0.0
shelf_static: ^0.2.8
test: ^1.0.0
environment:

View file

@ -7,6 +7,7 @@ import 'package:angel_shelf/angel_shelf.dart';
import 'package:angel_test/angel_test.dart';
import 'package:charcode/charcode.dart';
import 'package:logging/logging.dart';
import 'package:pretty_logging/pretty_logging.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:stream_channel/stream_channel.dart';
import 'package:test/test.dart';
@ -16,15 +17,24 @@ main() {
HttpServer server;
String url;
String _path(String p) {
return Uri(
scheme: 'http',
host: server.address.address,
port: server.port,
path: p)
.toString();
}
setUp(() async {
var handler = new shelf.Pipeline().addHandler((shelf.Request request) {
if (request.url.path == 'two')
return new shelf.Response(200, body: json.encode(2));
else if (request.url.path == 'error')
throw new AngelHttpException.notFound();
else if (request.url.path == 'status')
return new shelf.Response.notModified(headers: {'foo': 'bar'});
else if (request.url.path == 'hijack') {
var handler = shelf.Pipeline().addHandler((shelf.Request request) {
if (request.url.path == 'two') {
return shelf.Response(200, body: json.encode(2));
} else if (request.url.path == 'error') {
throw AngelHttpException.notFound();
} else if (request.url.path == 'status') {
return shelf.Response.notModified(headers: {'foo': 'bar'});
} else if (request.url.path == 'hijack') {
request.hijack((StreamChannel<List<int>> channel) {
var sink = channel.sink;
sink.add(utf8.encode('HTTP/1.1 200 OK\r\n'));
@ -32,25 +42,22 @@ main() {
sink.add(utf8.encode(json.encode({'error': 'crime'})));
sink.close();
});
} else if (request.url.path == 'throw')
return null;
else
return new shelf.Response.ok('Request for "${request.url}"');
} else if (request.url.path == 'throw') {
return null;
} else {
return shelf.Response.ok('Request for "${request.url}"');
}
});
var app = new Angel();
var http = new AngelHttp(app);
var logger = Logger.detached('angel_shelf')..onRecord.listen(prettyLog);
var app = Angel(logger: logger);
var http = AngelHttp(app);
app.get('/angel', (req, res) => 'Angel');
app.fallback(embedShelf(handler, throwOnNullResponse: true));
app.logger = new Logger.detached('angel_shelf')
..onRecord.listen((rec) {
stdout.writeln(rec);
if (rec.error != null) stdout.writeln(rec.error);
if (rec.stackTrace != null) stdout.writeln(rec.stackTrace);
});
server = await http.startServer(InternetAddress.loopbackIPv4, 0);
client = new c.Rest(url = http.uri.toString());
client = c.Rest(url = http.uri.toString());
});
tearDown(() async {
@ -59,24 +66,24 @@ main() {
});
test('expose angel side', () async {
var response = await client.get('/angel');
var response = await client.get(_path('/angel'));
expect(json.decode(response.body), equals('Angel'));
});
test('expose shelf side', () async {
var response = await client.get('/foo');
var response = await client.get(_path('/foo'));
expect(response, hasStatus(200));
expect(response.body, equals('Request for "foo"'));
});
test('shelf can return arbitrary values', () async {
var response = await client.get('/two');
var response = await client.get(_path('/two'));
expect(response, isJson(2));
});
test('shelf can hijack', () async {
try {
var client = new HttpClient();
var client = HttpClient();
var rq = await client.openUrl('GET', Uri.parse('$url/hijack'));
var rs = await rq.close();
var body = await rs.cast<List<int>>().transform(utf8.decoder).join();
@ -90,17 +97,17 @@ main() {
}, skip: '');
test('shelf can set status code', () async {
var response = await client.get('/status');
var response = await client.get(_path('/status'));
expect(response, allOf(hasStatus(304), hasHeader('foo', 'bar')));
});
test('shelf can throw error', () async {
var response = await client.get('/error');
var response = await client.get(_path('/error'));
expect(response, hasStatus(404));
});
test('throw on null', () async {
var response = await client.get('/throw');
var response = await client.get(_path('/throw'));
expect(response, hasStatus(500));
});
}