diff --git a/packages/proxy/.gitignore b/packages/proxy/.gitignore
new file mode 100644
index 00000000..fffcccb7
--- /dev/null
+++ b/packages/proxy/.gitignore
@@ -0,0 +1,71 @@
+# See https://www.dartlang.org/tools/private-files.html
+
+# Files and directories created by pub
+.buildlog
+.packages
+.project
+.pub/
+build/
+**/packages/
+
+# Files created by dart2js
+# (Most Dart developers will use pub build to compile Dart, use/modify these
+# rules if you intend to use dart2js directly
+# Convention is to use extension '.dart.js' for Dart compiled to Javascript to
+# differentiate from explicit Javascript files)
+*.dart.js
+*.part.js
+*.js.deps
+*.js.map
+*.info.json
+
+# Directory created by dartdoc
+doc/api/
+
+# Don't commit pubspec lock file
+# (Library packages only! Remove pattern if developing an application package)
+pubspec.lock
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/workspace.xml
+.idea/tasks.xml
+
+# Sensitive or high-churn files:
+.idea/dataSources/
+.idea/dataSources.ids
+.idea/dataSources.xml
+.idea/dataSources.local.xml
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+
+# Gradle:
+.idea/gradle.xml
+.idea/libraries
+
+# Mongo Explorer plugin:
+.idea/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+.dart_tool
diff --git a/packages/proxy/.idea/angel_proxy.iml b/packages/proxy/.idea/angel_proxy.iml
new file mode 100644
index 00000000..eae13016
--- /dev/null
+++ b/packages/proxy/.idea/angel_proxy.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/jsLibraryMappings.xml b/packages/proxy/.idea/jsLibraryMappings.xml
new file mode 100644
index 00000000..f3e502d1
--- /dev/null
+++ b/packages/proxy/.idea/jsLibraryMappings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/misc.xml b/packages/proxy/.idea/misc.xml
new file mode 100644
index 00000000..c65900a0
--- /dev/null
+++ b/packages/proxy/.idea/misc.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ General
+
+
+ XPath
+
+
+
+
+ AngularJS
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/modules.xml b/packages/proxy/.idea/modules.xml
new file mode 100644
index 00000000..a68a6387
--- /dev/null
+++ b/packages/proxy/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/runConfigurations/All_Tests.xml b/packages/proxy/.idea/runConfigurations/All_Tests.xml
new file mode 100644
index 00000000..44742797
--- /dev/null
+++ b/packages/proxy/.idea/runConfigurations/All_Tests.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/runConfigurations/Basic_Tests.xml b/packages/proxy/.idea/runConfigurations/Basic_Tests.xml
new file mode 100644
index 00000000..87d3c79c
--- /dev/null
+++ b/packages/proxy/.idea/runConfigurations/Basic_Tests.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/runConfigurations/Pub_Serve_Tests.xml b/packages/proxy/.idea/runConfigurations/Pub_Serve_Tests.xml
new file mode 100644
index 00000000..74e5219a
--- /dev/null
+++ b/packages/proxy/.idea/runConfigurations/Pub_Serve_Tests.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/runConfigurations/multiple_dart.xml b/packages/proxy/.idea/runConfigurations/multiple_dart.xml
new file mode 100644
index 00000000..36b7b1d6
--- /dev/null
+++ b/packages/proxy/.idea/runConfigurations/multiple_dart.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.idea/vcs.xml b/packages/proxy/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/packages/proxy/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/proxy/.travis.yml b/packages/proxy/.travis.yml
new file mode 100644
index 00000000..de2210c9
--- /dev/null
+++ b/packages/proxy/.travis.yml
@@ -0,0 +1 @@
+language: dart
\ No newline at end of file
diff --git a/packages/proxy/.vscode/launch.json b/packages/proxy/.vscode/launch.json
new file mode 100644
index 00000000..b78e04ca
--- /dev/null
+++ b/packages/proxy/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Multiple Proxies",
+ "type": "dart-cli",
+ "request": "launch",
+ "program": "${workspaceRoot}/example/multiple.dart"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/proxy/CHANGELOG.md b/packages/proxy/CHANGELOG.md
new file mode 100644
index 00000000..94651d01
--- /dev/null
+++ b/packages/proxy/CHANGELOG.md
@@ -0,0 +1,26 @@
+# 2.2.0
+* Use `http.Client` instead of `http.BaseClient`, and make it an
+optional parameter.
+* Allow `baseUrl` to accept `Uri` or `String`.
+* Add `Proxy.pushState`.
+
+# 2.1.2
+* Apply lints.
+
+# 2.1.1
+* Update for framework@2.0.0-alpha.15
+
+# 2.1.0
+
+- Use `Uri` instead of archaic `host`, `port`, and `mapTo`. Also cleaner + safer + easier.
+
+* Enable WebSocket proxying.
+
+# 2.0.0
+
+- Updates for Angel 2. Big thanks to @denkuy!
+- Use `package:path` for better path resolution.
+
+# 1.1.1
+
+- Removed reference to `io`; now works with HTTP/2. Thanks to @daniel-v!
diff --git a/packages/proxy/LICENSE b/packages/proxy/LICENSE
new file mode 100644
index 00000000..15fe44bd
--- /dev/null
+++ b/packages/proxy/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 The Angel Framework
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/proxy/README.md b/packages/proxy/README.md
new file mode 100644
index 00000000..7e4400c9
--- /dev/null
+++ b/packages/proxy/README.md
@@ -0,0 +1,34 @@
+# proxy
+[![Pub](https://img.shields.io/pub/v/angel_proxy.svg)](https://pub.dartlang.org/packages/angel_proxy)
+[![build status](https://travis-ci.org/angel-dart/proxy.svg)](https://travis-ci.org/angel-dart/proxy)
+
+Angel middleware to forward requests to another server (i.e. `webdev serve`).
+Also supports WebSockets.
+
+```dart
+import 'package:angel_proxy/angel_proxy.dart';
+import 'package:http/http.dart' as http;
+
+main() async {
+ // Forward requests instead of serving statically.
+ // You can also pass a URI, instead of a string.
+ var proxy1 = Proxy('http://localhost:3000');
+
+ // handle all methods (GET, POST, ...)
+ app.fallback(proxy.handleRequest);
+}
+```
+
+You can also restrict the proxy to serving only from a specific root:
+```dart
+Proxy(baseUrl, publicPath: '/remote');
+```
+
+Also, you can map requests to a root path on the remote server:
+```dart
+Proxy(baseUrl.replace(path: '/path'));
+```
+
+Request bodies will be forwarded as well, if they are not empty. This allows things like POST requests to function.
+
+For a request body to be forwarded, the body must not have already been parsed.
\ No newline at end of file
diff --git a/packages/proxy/analysis_options.yaml b/packages/proxy/analysis_options.yaml
new file mode 100644
index 00000000..085be64d
--- /dev/null
+++ b/packages/proxy/analysis_options.yaml
@@ -0,0 +1,8 @@
+include: package:pedantic/analysis_options.yaml
+analyzer:
+ strong-mode:
+ implicit-casts: false
+linter:
+ rules:
+ - unnecessary_const
+ - unnecessary_new
\ No newline at end of file
diff --git a/packages/proxy/example/main.dart b/packages/proxy/example/main.dart
new file mode 100644
index 00000000..f4998ef6
--- /dev/null
+++ b/packages/proxy/example/main.dart
@@ -0,0 +1,63 @@
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:angel_proxy/angel_proxy.dart';
+import 'package:logging/logging.dart';
+
+final Duration timeout = Duration(seconds: 5);
+
+main() async {
+ var app = Angel();
+
+ // Forward any /api requests to pub.
+ // By default, if the host throws a 404, the request will fall through to the next handler.
+ var pubProxy = Proxy(
+ 'https://pub.dartlang.org',
+ publicPath: '/pub',
+ timeout: timeout,
+ );
+ app.all("/pub/*", pubProxy.handleRequest);
+
+ // Surprise! We can also proxy WebSockets.
+ //
+ // Play around with this at http://www.websocket.org/echo.html.
+ var echoProxy = Proxy(
+ 'http://echo.websocket.org',
+ publicPath: '/echo',
+ timeout: timeout,
+ );
+ app.get('/echo', echoProxy.handleRequest);
+
+ // Pub's HTML assumes that the site's styles, etc. are on the absolute path `/static`.
+ // This is not the case here. Let's patch that up:
+ app.get('/static/*', (RequestContext req, res) {
+ return pubProxy.servePath(req.path, req, res);
+ });
+
+ // Anything else should fall through to dartlang.org.
+ var dartlangProxy = Proxy(
+ 'https://dartlang.org',
+ timeout: timeout,
+ recoverFrom404: false,
+ );
+ app.all('*', dartlangProxy.handleRequest);
+
+ // In case we can't connect to dartlang.org, show an error.
+ app.fallback(
+ (req, res) => res.write('Couldn\'t connect to Pub or dartlang.'));
+
+ app.logger = Logger('angel')
+ ..onRecord.listen(
+ (rec) {
+ print(rec);
+ if (rec.error != null) print(rec.error);
+ if (rec.stackTrace != null) print(rec.stackTrace);
+ },
+ );
+
+ var server =
+ await AngelHttp(app).startServer(InternetAddress.loopbackIPv4, 8080);
+ print('Listening at http://${server.address.address}:${server.port}');
+ print(
+ 'Check this out! http://${server.address.address}:${server.port}/pub/packages/angel_framework');
+}
diff --git a/packages/proxy/lib/angel_proxy.dart b/packages/proxy/lib/angel_proxy.dart
new file mode 100644
index 00000000..a0138f3e
--- /dev/null
+++ b/packages/proxy/lib/angel_proxy.dart
@@ -0,0 +1,3 @@
+library angel_proxy;
+
+export 'src/proxy_layer.dart';
diff --git a/packages/proxy/lib/src/proxy_layer.dart b/packages/proxy/lib/src/proxy_layer.dart
new file mode 100644
index 00000000..dc1e87b3
--- /dev/null
+++ b/packages/proxy/lib/src/proxy_layer.dart
@@ -0,0 +1,218 @@
+import 'dart:async';
+import 'dart:io';
+import 'dart:convert';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:http_parser/http_parser.dart';
+import 'package:http/http.dart' as http;
+import 'package:path/path.dart' as p;
+
+final RegExp _straySlashes = RegExp(r'(^/+)|(/+$)');
+final MediaType _fallbackMediaType = MediaType('application', 'octet-stream');
+
+/// A middleware class that forwards requests (reverse proxies) to an upstream server.
+///
+/// Supports WebSockets, in addition to regular HTTP requests.
+class Proxy {
+ String _prefix;
+
+ /// The underlying [Client] to use.
+ final http.Client httpClient;
+
+ /// If `true` (default), then the plug-in will ignore failures to connect to the proxy, and allow other handlers to run.
+ final bool recoverFromDead;
+ final bool recoverFrom404;
+ final Uri baseUrl;
+ final String publicPath;
+
+ /// If `null` then no timout is added for requests
+ final Duration timeout;
+
+ Proxy(
+ baseUrl, {
+ http.Client httpClient,
+ this.publicPath = '/',
+ this.recoverFromDead = true,
+ this.recoverFrom404 = true,
+ this.timeout,
+ }) : this.baseUrl = baseUrl is Uri ? baseUrl : Uri.parse(baseUrl.toString()),
+ this.httpClient = httpClient ?? http.Client() {
+ if (!this.baseUrl.hasScheme || !this.baseUrl.hasAuthority) {
+ throw ArgumentError(
+ 'Invalid `baseUrl`. URI must have both a scheme and authority.');
+ }
+ if (this.recoverFromDead == null) {
+ throw ArgumentError.notNull("recoverFromDead");
+ }
+ if (this.recoverFrom404 == null) {
+ throw ArgumentError.notNull("recoverFrom404");
+ }
+
+ _prefix = publicPath?.replaceAll(_straySlashes, '') ?? '';
+ }
+
+ void close() => httpClient.close();
+
+ /// A handler that serves the file at the given path, unless the user has requested that path.
+ ///
+ /// You can also limit this functionality to specific values of the `Accept` header, ex. `text/html`.
+ /// If [accepts] is `null`, OR at least one of the content types in [accepts] is present,
+ /// the view will be served.
+ RequestHandler pushState(String path, {Iterable accepts}) {
+ var vPath = path.replaceAll(_straySlashes, '');
+ if (_prefix?.isNotEmpty == true) vPath = '$_prefix/$vPath';
+
+ return (RequestContext req, ResponseContext res) {
+ var path = req.path.replaceAll(_straySlashes, '');
+ if (path == vPath) return Future.value(true);
+
+ if (accepts?.isNotEmpty == true) {
+ if (!accepts.any((x) => req.accepts(x, strict: true))) {
+ return Future.value(true);
+ }
+ }
+
+ return servePath(vPath, req, res);
+ };
+ }
+
+ /// Handles an incoming HTTP request.
+ Future handleRequest(RequestContext req, ResponseContext res) {
+ var path = req.path.replaceAll(_straySlashes, '');
+
+ if (_prefix.isNotEmpty) {
+ if (!p.isWithin(_prefix, path) && !p.equals(_prefix, path)) {
+ return Future.value(true);
+ }
+
+ path = p.relative(path, from: _prefix);
+ }
+
+ return servePath(path, req, res);
+ }
+
+ /// Proxies a request to the given path on the remote server.
+ Future servePath(
+ String path, RequestContext req, ResponseContext res) async {
+ http.StreamedResponse rs;
+
+ var uri = baseUrl.replace(path: p.join(baseUrl.path, path));
+
+ try {
+ if (req is HttpRequestContext &&
+ WebSocketTransformer.isUpgradeRequest(req.rawRequest)) {
+ res.detach();
+ uri = uri.replace(scheme: uri.scheme == 'https' ? 'wss' : 'ws');
+
+ try {
+ var local = await WebSocketTransformer.upgrade(req.rawRequest);
+ var remote = await WebSocket.connect(uri.toString());
+
+ scheduleMicrotask(() => local.pipe(remote));
+ scheduleMicrotask(() => remote.pipe(local));
+ return false;
+ } catch (e, st) {
+ throw AngelHttpException(e,
+ message: 'Could not connect WebSocket', stackTrace: st);
+ }
+ }
+
+ Future accessRemote() async {
+ var headers = {
+ 'host': uri.authority,
+ 'x-forwarded-for': req.remoteAddress.address,
+ 'x-forwarded-port': req.uri.port.toString(),
+ 'x-forwarded-host':
+ req.headers.host ?? req.headers.value('host') ?? 'none',
+ 'x-forwarded-proto': uri.scheme,
+ };
+
+ req.headers.forEach((name, values) {
+ headers[name] = values.join(',');
+ });
+
+ headers[HttpHeaders.cookieHeader] =
+ req.cookies.map((c) => '${c.name}=${c.value}').join('; ');
+
+ List body;
+
+ if (!req.hasParsedBody) {
+ body = await req.body
+ .fold(BytesBuilder(), (bb, buf) => bb..add(buf))
+ .then((bb) => bb.takeBytes());
+ }
+
+ var rq = http.Request(req.method, uri);
+ rq.headers.addAll(headers);
+ rq.headers['host'] = rq.url.host;
+ rq.encoding = Utf8Codec(allowMalformed: true);
+
+ if (body != null) rq.bodyBytes = body;
+
+ return httpClient.send(rq);
+ }
+
+ var future = accessRemote();
+ if (timeout != null) future = future.timeout(timeout);
+ rs = await future;
+ } on TimeoutException catch (e, st) {
+ if (recoverFromDead) return true;
+
+ throw AngelHttpException(
+ e,
+ stackTrace: st,
+ statusCode: 504,
+ message:
+ 'Connection to remote host "$uri" timed out after ${timeout.inMilliseconds}ms.',
+ );
+ } catch (e) {
+ if (recoverFromDead && e is! AngelHttpException) return true;
+ rethrow;
+ }
+
+ if (rs.statusCode == 404 && recoverFrom404) return true;
+ if (rs.contentLength == 0 && recoverFromDead) return true;
+
+ MediaType mediaType;
+ if (rs.headers.containsKey(HttpHeaders.contentTypeHeader)) {
+ try {
+ mediaType = MediaType.parse(rs.headers[HttpHeaders.contentTypeHeader]);
+ } on FormatException catch (e, st) {
+ if (recoverFromDead) return true;
+
+ throw AngelHttpException(
+ e,
+ stackTrace: st,
+ statusCode: 504,
+ message: 'Host "$uri" returned a malformed content-type',
+ );
+ }
+ } else {
+ mediaType = _fallbackMediaType;
+ }
+
+ /// if [http.Client] does not provide us with a content length
+ /// OR [http.Client] is about to decode the response (bytecount returned by [http.Response].stream != known length)
+ /// then we can not provide a value downstream => set to '-1' for 'unspecified length'
+ var isContentLengthUnknown = rs.contentLength == null ||
+ rs.headers[HttpHeaders.contentEncodingHeader]?.isNotEmpty == true ||
+ rs.headers[HttpHeaders.transferEncodingHeader]?.isNotEmpty == true;
+
+ var proxiedHeaders = Map.from(rs.headers)
+ ..remove(
+ HttpHeaders.contentEncodingHeader) // drop, http.Client has decoded
+ ..remove(
+ HttpHeaders.transferEncodingHeader) // drop, http.Client has decoded
+ ..[HttpHeaders.contentLengthHeader] =
+ "${isContentLengthUnknown ? '-1' : rs.contentLength}";
+
+ res
+ ..contentType = mediaType
+ ..statusCode = rs.statusCode
+ ..headers.addAll(proxiedHeaders);
+
+ await rs.stream.pipe(res);
+
+ return false;
+ }
+}
diff --git a/packages/proxy/pubspec.yaml b/packages/proxy/pubspec.yaml
new file mode 100644
index 00000000..a319e2a9
--- /dev/null
+++ b/packages/proxy/pubspec.yaml
@@ -0,0 +1,19 @@
+name: angel_proxy
+description: Angel middleware to forward requests to another server (i.e. pub serve).
+version: 2.2.0
+author: Tobe O
+homepage: https://github.com/angel-dart/proxy
+environment:
+ sdk: ">=2.0.0 <3.0.0"
+dependencies:
+ angel_framework: ^2.0.0-alpha
+ http: ^0.12.0
+ http_parser: ^3.0.0
+ path: ^1.0.0
+dev_dependencies:
+ angel_test: ^2.0.0-alpha
+ logging:
+ mock_request:
+ pedantic: ^1.0.0
+ test: ^1.0.0
+
diff --git a/packages/proxy/test/basic_test.dart b/packages/proxy/test/basic_test.dart
new file mode 100644
index 00000000..6d62d82d
--- /dev/null
+++ b/packages/proxy/test/basic_test.dart
@@ -0,0 +1,88 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:angel_proxy/angel_proxy.dart';
+import 'package:http/io_client.dart' as http;
+import 'package:logging/logging.dart';
+import 'package:test/test.dart';
+import 'common.dart';
+
+main() {
+ Angel app;
+ var client = http.IOClient();
+ HttpServer server, testServer;
+ String url;
+
+ setUp(() async {
+ app = Angel();
+ var appHttp = AngelHttp(app);
+
+ testServer = await startTestServer();
+
+ var proxy1 = Proxy(
+ Uri(
+ scheme: 'http',
+ host: testServer.address.address,
+ port: testServer.port),
+ publicPath: '/proxy',
+ );
+
+ var proxy2 = Proxy(proxy1.baseUrl.replace(path: '/foo'));
+ print('Proxy 1 on: ${proxy1.baseUrl}');
+ print('Proxy 2 on: ${proxy2.baseUrl}');
+
+ app.all("/proxy/*", proxy1.handleRequest);
+ app.all("*", proxy2.handleRequest);
+
+ app.fallback((req, res) {
+ print('Intercepting empty from ${req.uri}');
+ res.write('intercept empty');
+ });
+
+ app.logger = Logger('angel');
+
+ Logger.root.onRecord.listen((rec) {
+ print(rec);
+ if (rec.error != null) print(rec.error);
+ if (rec.stackTrace != null) print(rec.stackTrace);
+ });
+
+ await appHttp.startServer();
+ url = appHttp.uri.toString();
+ });
+
+ tearDown(() async {
+ await testServer?.close(force: true);
+ await server?.close(force: true);
+ app = null;
+ url = null;
+ });
+
+ test('publicPath', () async {
+ final response = await client.get('$url/proxy/hello');
+ print('Response: ${response.body}');
+ expect(response.body, equals('world'));
+ });
+
+ test('empty', () async {
+ var response = await client.get('$url/proxy/empty');
+ print('Response: ${response.body}');
+ expect(response.body, 'intercept empty');
+ });
+
+ test('mapTo', () async {
+ final response = await client.get('$url/bar');
+ print('Response: ${response.body}');
+ expect(response.body, equals('baz'));
+ });
+
+ test('original buffer', () async {
+ var response = await client.post('$url/proxy/body',
+ body: json.encode({'foo': 'bar'}),
+ headers: {'content-type': 'application/json'});
+ print('Response: ${response.body}');
+ expect(response.body, isNotEmpty);
+ expect(response.body, isNot('intercept empty'));
+ });
+}
diff --git a/packages/proxy/test/common.dart b/packages/proxy/test/common.dart
new file mode 100644
index 00000000..c437361c
--- /dev/null
+++ b/packages/proxy/test/common.dart
@@ -0,0 +1,24 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:logging/logging.dart';
+
+Future startTestServer() {
+ final app = Angel();
+
+ app.get('/hello', (req, res) => res.write('world'));
+ app.get('/foo/bar', (req, res) => res.write('baz'));
+ app.post('/body', (RequestContext req, res) async {
+ var body = await req.parseBody().then((_) => req.bodyAsMap);
+ app.logger.info('Body: $body');
+ return body;
+ });
+
+ app.logger = Logger('testApp');
+ var server = AngelHttp(app);
+ app.dumpTree();
+
+ return server.startServer();
+}
diff --git a/packages/proxy/test/pub_serve_test.dart b/packages/proxy/test/pub_serve_test.dart
new file mode 100644
index 00000000..48cefa4e
--- /dev/null
+++ b/packages/proxy/test/pub_serve_test.dart
@@ -0,0 +1,94 @@
+import 'dart:convert';
+import 'dart:io';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:angel_proxy/angel_proxy.dart';
+import 'package:angel_test/angel_test.dart';
+import 'package:logging/logging.dart';
+import 'package:mock_request/mock_request.dart';
+import 'package:test/test.dart';
+
+main() {
+ Angel app, testApp;
+ TestClient client;
+ Proxy layer;
+
+ setUp(() async {
+ testApp = Angel();
+ testApp.get('/foo', (req, res) async {
+ res.useBuffer();
+ res.write('pub serve');
+ });
+ testApp.get('/empty', (req, res) => res.close());
+
+ testApp.responseFinalizers.add((req, res) async {
+ print('OUTGOING: ' + String.fromCharCodes(res.buffer.toBytes()));
+ });
+
+ testApp.encoders.addAll({'gzip': gzip.encoder});
+
+ var server = await AngelHttp(testApp).startServer();
+
+ app = Angel();
+ app.fallback((req, res) {
+ res.useBuffer();
+ return true;
+ });
+ app.get('/bar', (req, res) => res.write('normal'));
+
+ layer = Proxy(
+ Uri(scheme: 'http', host: server.address.address, port: server.port),
+ publicPath: '/proxy',
+ );
+
+ app.fallback(layer.handleRequest);
+
+ app.responseFinalizers.add((req, res) async {
+ print('Normal. Buf: ' +
+ String.fromCharCodes(res.buffer.toBytes()) +
+ ', headers: ${res.headers}');
+ });
+
+ app.encoders.addAll({'gzip': gzip.encoder});
+
+ client = await connectTo(app);
+
+ app.logger = testApp.logger = Logger('proxy')
+ ..onRecord.listen((rec) {
+ print(rec);
+ if (rec.error != null) print(rec.error);
+ if (rec.stackTrace != null) print(rec.stackTrace);
+ });
+ });
+
+ tearDown(() async {
+ await client.close();
+ await app.close();
+ await testApp.close();
+ app = null;
+ testApp = null;
+ });
+
+ test('proxied', () async {
+ var rq = MockHttpRequest('GET', Uri.parse('/proxy/foo'));
+ await rq.close();
+ var rqc = await HttpRequestContext.from(rq, app, '/proxy/foo');
+ var rsc = HttpResponseContext(rq.response, app);
+ await app.executeHandler(layer.handleRequest, rqc, rsc);
+ var response = await rq.response
+ //.transform(gzip.decoder)
+ .transform(utf8.decoder)
+ .join();
+ expect(response, 'pub serve');
+ });
+
+ test('empty', () async {
+ var response = await client.get('/proxy/empty');
+ expect(response.body, isEmpty);
+ });
+
+ test('normal', () async {
+ var response = await client.get('/bar');
+ expect(response, hasBody('normal'));
+ });
+}