v0.1.0

A number of updates from @nex3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index faabee1..4009b55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 0.1.0
+
+* `createProxyHandler` (new deprecated) is replaced with `proxyHandler`.
+
+* Updated to be compatible with RFC 2616 Proxy specification.
+
 ## 0.0.2
 
 * Updated `README.md` and doc comments on `createProxyHandler`.
diff --git a/LICENSE b/LICENSE
index b490318..5c60afe 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2014, the shelf_proxy authors. All rights reserved.
+Copyright 2014, the Dart project authors. All rights reserved.
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
 met:
diff --git a/README.md b/README.md
index 925154b..0cfa82f 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,19 @@
-[![Build Status](https://drone.io/github.com/kevmoo/shelf_proxy/status.png)](https://drone.io/github.com/kevmoo/shelf_proxy/latest)
+## Proxy for Shelf
 
-[Shelf][shelf] `handler` to proxy requests to another web server.
+`shelf_proxy` is a [Shelf][] handler that proxies requests to an external
+server. It can be served directly and used as a proxy server, or it can be
+mounted within a larger application to proxy only certain URLs.
 
-Useful if you want to send a subset of requests to another HTTP endpoint, for
-instance `pub serve`.
+[Shelf]: pub.dartlang.org/packages/shelf
 
-See `example/example_server.dart` for a usage demonstration.
+```dart
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_proxy/shelf_proxy.dart';
 
-[shelf]: http://pub.dartlang.org/packages/shelf
+void main() {
+  shelf_io.serve(proxyHandler("https://www.dartlang.org"), 'localhost', 8080)
+      .then((server) {
+    print('Proxying at http://${server.address.host}:${server.port}');
+  });
+}
+```
diff --git a/codereview.settings b/codereview.settings
new file mode 100644
index 0000000..620fe13
--- /dev/null
+++ b/codereview.settings
@@ -0,0 +1,3 @@
+CODE_REVIEW_SERVER: https://codereview.chromium.org/
+VIEW_VC: https://github.com/dart-lang/shelf_proxy/commit/
+CC_LIST: reviews@dartlang.org
diff --git a/example/example_server.dart b/example/example_server.dart
index 39be43c..f8dd2cf 100644
--- a/example/example_server.dart
+++ b/example/example_server.dart
@@ -10,7 +10,6 @@
 final _encoder = new JsonEncoder.withIndent('  ');
 
 void main() {
-
   //
   // The api handler responds to requests to '/api' with a JSON document
   // containing an incrementing 'count' value.
@@ -33,7 +32,7 @@
   //
   var cascade = new Cascade()
       .add(apiHandler)
-      .add(createProxyHandler(Uri.parse('http://localhost:$_PUB_PORT')));
+      .add(proxyHandler(Uri.parse('http://localhost:$_PUB_PORT')));
 
   //
   // Creates a pipeline handler which first logs requests and then sends them
diff --git a/lib/shelf_proxy.dart b/lib/shelf_proxy.dart
index 8a9d5b1..fc3cb4d 100644
--- a/lib/shelf_proxy.dart
+++ b/lib/shelf_proxy.dart
@@ -1,75 +1,103 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
 library shelf_proxy;
 
-import 'dart:io';
-
+import 'package:http/http.dart' as http;
+import 'package:path/path.dart' as p;
 import 'package:shelf/shelf.dart';
 
-/// Creates a [Handler] that sends requests to another web server at the
-/// specified [rootUri].
+import 'src/utils.dart';
+
+/// A handler that proxies requests to [url].
 ///
-/// [rootUri] must be absolue with an http(s) scheme and no query or fragment
-/// components.
+/// To generate the proxy request, this concatenates [url] and [Request.url].
+/// This means that if the handler mounted under `/documentation` and [url] is
+/// `https://www.dartlang.org/docs`, a request to `/documentation/tutorials`
+/// will be proxied to `https://www.dartlang.org/docs/tutorials`.
 ///
-/// Only requests with method `GET` are allowed. All other methods result in a
-/// `405` - [HttpStatus.METHOD_NOT_ALLOWED] response.
+/// [client] is used internally to make HTTP requests. It defaults to a
+/// `dart:io`-based client.
 ///
-/// Example:
-///
-/// If [rootUri] is specified as `http://example.com/files`, a request for
-/// `/test/sample.html` would result in a request to
-/// `http://example.com/files/test/sample.html`.
-Handler createProxyHandler(Uri rootUri) {
-  if (rootUri.scheme != 'http' && rootUri.scheme != 'https') {
-    throw new ArgumentError('rootUri must have a scheme of http or https.');
-  }
+/// [proxyName] is used in headers to identify this proxy. It should be a valid
+/// HTTP token or a hostname. It defaults to `shelf_proxy`.
+Handler proxyHandler(url, {http.Client client, String proxyName}) {
+  if (url is String) url = Uri.parse(url);
+  if (client == null) client = new http.Client();
+  if (proxyName == null) proxyName = 'shelf_proxy';
 
-  if (!rootUri.isAbsolute) {
-    throw new ArgumentError('rootUri must be absolute.');
-  }
+  return (serverRequest) {
+    // TODO(nweiz): Support WebSocket requests.
 
-  if (rootUri.query.isNotEmpty) {
-    throw new ArgumentError('rootUri cannot contain a query.');
-  }
+    // TODO(nweiz): Handle TRACE requests correctly. See
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.8
+    var requestUrl = url.resolve(serverRequest.url.toString());
+    var clientRequest = new http.StreamedRequest(
+        serverRequest.method, requestUrl);
+    clientRequest.followRedirects = false;
+    clientRequest.headers.addAll(serverRequest.headers);
+    clientRequest.headers['Host'] = url.authority;
 
-  return (Request request) {
-    if (request.method != 'GET') {
-      return new Response(HttpStatus.METHOD_NOT_ALLOWED);
-    }
+    // Add a Via header. See
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
+    _addHeader(clientRequest.headers, 'via',
+        '${serverRequest.protocolVersion} $proxyName');
 
-    // TODO: really need to tear down the client when this is done...
-    var client = new HttpClient();
+    store(serverRequest.read(), clientRequest.sink);
+    return client.send(clientRequest).then((clientResponse) {
+      // Add a Via header. See
+      // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.45
+      _addHeader(clientResponse.headers, 'via', '1.1 $proxyName');
 
-    var url = _getProxyUrl(rootUri, request.url);
+      // Remove the transfer-encoding since the body has already been decoded by
+      // [client].
+      clientResponse.headers.remove('transfer-encoding');
 
-    return client.openUrl(request.method, url).then((ioRequest) {
-      return ioRequest.close();
-    }).then((ioResponse) {
-      var headers = {};
-      // dart:io - HttpClientResponse.contentLength is -1 if not defined
-      if (ioResponse.contentLength >= 0) {
-        headers[HttpHeaders.CONTENT_LENGTH] =
-            ioResponse.contentLength.toString();
+      // If the original response was gzipped, it will be decoded by [client]
+      // and we'll have no way of knowing its actual content-length.
+      if (clientResponse.headers['content-encoding'] == 'gzip') {
+        clientResponse.headers.remove('content-encoding');
+        clientResponse.headers.remove('content-length');
+
+        // Add a Warning header. See
+        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.2
+        _addHeader(clientResponse.headers, 'warning',
+            '214 $proxyName "GZIP decoded"');
       }
 
-      return new Response(ioResponse.statusCode, body: ioResponse,
-          headers: headers);
+      // Make sure the Location header is pointing to the proxy server rather
+      // than the destination server, if possible.
+      if (clientResponse.isRedirect &&
+          clientResponse.headers.containsKey('location')) {
+        var location = requestUrl.resolve(clientResponse.headers['location'])
+            .toString();
+        if (p.url.isWithin(url.toString(), location)) {
+          clientResponse.headers['location'] =
+              '/' + p.url.relative(location, from: url.toString());
+        } else {
+          clientResponse.headers['location'] = location;
+        }
+      }
+
+      return new Response(clientResponse.statusCode,
+          body: clientResponse.stream,
+          headers: clientResponse.headers);
     });
   };
 }
 
-Uri _getProxyUrl(Uri proxyRoot, Uri requestUrl) {
-  assert(proxyRoot.scheme == 'http' || proxyRoot.scheme == 'https');
-  assert(proxyRoot.query == '');
-  assert(proxyRoot.isAbsolute);
-  assert(!requestUrl.isAbsolute);
+/// Use [proxyHandler] instead.
+@deprecated
+Handler createProxyHandler(Uri rootUri) => proxyHandler(rootUri);
 
-  var updatedPath = proxyRoot.pathSegments.toList()
-      ..addAll(requestUrl.pathSegments);
-
-  return new Uri(scheme: proxyRoot.scheme,
-      userInfo: proxyRoot.userInfo,
-      host: proxyRoot.host,
-      port: proxyRoot.port,
-      pathSegments: updatedPath,
-      query: requestUrl.query);
+// TODO(nweiz): use built-in methods for this when http and shelf support them.
+/// Add a header with [name] and [value] to [headers], handling existing headers
+/// gracefully.
+void _addHeader(Map<String, String> headers, String name, String value) {
+  if (headers.containsKey(name)) {
+    headers[name] += ', $value';
+  } else {
+    headers[name] = value;
+  }
 }
diff --git a/lib/src/util.dart b/lib/src/util.dart
deleted file mode 100644
index 6085013..0000000
--- a/lib/src/util.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library shelf_proxy.util;
-
-import 'dart:async';
-
-import 'package:stack_trace/stack_trace.dart';
-
-/// Like [Future.sync], but wraps the Future in [Chain.track] as well.
-Future syncFuture(callback()) => Chain.track(new Future.sync(callback));
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..f30545e
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library shelf_proxy.utils;
+
+import 'dart:async';
+
+// TODO(nweiz): remove this when issue 7786 is fixed.
+/// Pipes all data and errors from [stream] into [sink].
+///
+/// When [stream] is done, the returned [Future] is completed and [sink] is
+/// closed if [closeSink] is true.
+///
+/// When an error occurs on [stream], that error is passed to [sink]. If
+/// [cancelOnError] is true, [Future] will be completed successfully and no
+/// more data or errors will be piped from [stream] to [sink]. If
+/// [cancelOnError] and [closeSink] are both true, [sink] will then be
+/// closed.
+Future store(Stream stream, EventSink sink,
+    {bool cancelOnError: true, bool closeSink: true}) {
+  var completer = new Completer();
+  stream.listen(sink.add, onError: (e, stackTrace) {
+    sink.addError(e, stackTrace);
+    if (cancelOnError) {
+      completer.complete();
+      if (closeSink) sink.close();
+    }
+  }, onDone: () {
+    if (closeSink) sink.close();
+    completer.complete();
+  }, cancelOnError: cancelOnError);
+  return completer.future;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index ca9e746..f014699 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,18 +1,12 @@
 name: shelf_proxy
-version: 0.0.2
-author: Kevin Moore <github@j832.com>
-description: Shelf handlers to proxy requests to other HTTP servers
-homepage: https://github.com/kevmoo/shelf_proxy
-environment:
-  sdk: '>=1.0.0 <2.0.0'
+version: 0.1.0
+author: Dart Team <misc@dartlang.org>
+description: A shelf handler for proxying requests to another server.
+homepage: https://github.com/dart-lang/shelf_proxy
 dependencies:
-  http_parser: '>=0.0.2+2 <0.1.0'
-  mime: '>=0.9.0 <0.10.0'
-  shelf: '>=0.5.3 <0.6.0'
+  http: '>=0.9.0 <0.12.0'
+  path: '>=1.0.0 <2.0.0'
+  shelf: '>=0.5.2 <0.6.0'
 dev_dependencies:
-  browser: any
-  hop: '>=0.30.4 <0.32.0'
-  hop_unittest: '>=0.1.0 <0.2.0'
-  path: '>=1.1.0 <2.0.0'
+  browser: '>=0.10.0 <0.11.0'
   scheduled_test: '>=0.11.0 <0.12.0'
-  shelf_static: '>=0.2.0 <0.3.0'
diff --git a/test/harness_console.dart b/test/harness_console.dart
deleted file mode 100644
index 8051387..0000000
--- a/test/harness_console.dart
+++ /dev/null
@@ -1,13 +0,0 @@
-library shelf_proxy.harness_console;
-
-import 'package:scheduled_test/scheduled_test.dart';
-
-import 'proxy_test.dart' as proxy;
-import 'static_file_test.dart' as static_file;
-
-void main() {
-  groupSep = ' - ';
-
-  group('proxy', proxy.main);
-  group('static file example', static_file.main);
-}
diff --git a/test/proxy_test.dart b/test/proxy_test.dart
deleted file mode 100644
index 6790e1d..0000000
--- a/test/proxy_test.dart
+++ /dev/null
@@ -1,155 +0,0 @@
-library shelf_proxy.proxy_test;
-
-import 'dart:async';
-import 'dart:io';
-
-import 'package:scheduled_test/scheduled_test.dart';
-import 'package:shelf/shelf.dart';
-import 'package:shelf/shelf_io.dart' as shelf_io;
-import 'package:shelf_proxy/shelf_proxy.dart';
-
-import 'test_util.dart';
-
-void main() {
-  group('arguments', () {
-    group('root uri must be http or https', () {
-      test('http works', () {
-        expect(createProxyHandler(Uri.parse('http://example.com')), isNotNull);
-      });
-      test('http works', () {
-        expect(createProxyHandler(Uri.parse('https://example.com')), isNotNull);
-      });
-      test('ftp does not work', () {
-        expect(() => createProxyHandler(Uri.parse('ftp://example.com')),
-            throwsArgumentError);
-      });
-    });
-
-    group('root uri must be absolute without query', () {
-      test('http works', () {
-        expect(createProxyHandler(Uri.parse('http://example.com')), isNotNull);
-      });
-
-      test('with trailing slash works', () {
-        expect(createProxyHandler(Uri.parse('http://example.com/')), isNotNull);
-      });
-
-      test('with path item', () {
-        expect(createProxyHandler(Uri.parse('http://example.com/path')),
-            isNotNull);
-      });
-
-      test('with path item and trailing slash', () {
-        expect(createProxyHandler(Uri.parse('http://example.com/path/')),
-            isNotNull);
-      });
-
-      test('with a fragment', () {
-        expect(
-            () => createProxyHandler(Uri.parse('http://example.com/path#foo')),
-            throwsArgumentError);
-      });
-
-      test('with a query', () {
-        expect(
-            () => createProxyHandler(Uri.parse('http://example.com/path?a=b')),
-            throwsArgumentError);
-      });
-    });
-  });
-
-  group('requests', () {
-    test('root', () {
-      _scheduleServer(_handler);
-
-      schedule(() {
-        var url = new Uri.http('localhost:$_serverPort', '');
-        var handler = createProxyHandler(url);
-
-        return makeRequest(handler, '/').then((response) {
-          expect(response.statusCode, HttpStatus.OK);
-          expect(response.readAsString(), completion('root with slash'));
-        });
-      });
-    });
-
-    test('/bar', () {
-      _scheduleServer(_handler);
-
-      schedule(() {
-        var url = new Uri.http('localhost:$_serverPort', '');
-        var handler = createProxyHandler(url);
-
-        return makeRequest(handler, '/bar').then((response) {
-          expect(response.statusCode, HttpStatus.OK);
-          expect(response.readAsString(), completion('bar'));
-        });
-      });
-    });
-
-    test('/bar/', () {
-      _scheduleServer(_handler);
-
-      schedule(() {
-        var url = new Uri.http('localhost:$_serverPort', '');
-        var handler = createProxyHandler(url);
-
-        return makeRequest(handler, '/bar/').then((response) {
-          expect(response.statusCode, HttpStatus.OK);
-          expect(response.readAsString(), completion('bar with slash'));
-        });
-      });
-    });
-
-    test('only GET is supported', () {
-      _scheduleServer(_handler);
-
-      schedule(() {
-        var url = new Uri.http('localhost:$_serverPort', '');
-        var handler = createProxyHandler(url);
-
-        return makeRequest(handler, '/', method: 'PUT').then((response) {
-          expect(response.statusCode, HttpStatus.METHOD_NOT_ALLOWED);
-        });
-      });
-    });
-  });
-}
-
-Response _handler(Request request) {
-  if (request.method != 'GET') {
-    return new Response.forbidden("I don't like method ${request.method}.");
-  }
-
-  String content;
-  switch (request.url.path) {
-    case '':
-      content = 'root';
-      break;
-    case '/':
-      content = 'root with slash';
-      break;
-    case '/bar':
-      content = 'bar';
-      break;
-    case '/bar/':
-      content = 'bar with slash';
-      break;
-    default:
-      return new Response.notFound("I don't like '${request.url.path}'.");
-  }
-  return new Response.ok(content);
-}
-
-int _serverPort;
-
-Future _scheduleServer(Handler handler) {
-  return schedule(() => shelf_io.serve(handler, 'localhost', 0).then((server) {
-    currentSchedule.onComplete.schedule(() {
-      _serverPort = null;
-      return server.close(force: true);
-    });
-
-    _serverPort = server.port;
-  }));
-}
diff --git a/test/shelf_proxy_test.dart b/test/shelf_proxy_test.dart
new file mode 100644
index 0000000..fe76ddc
--- /dev/null
+++ b/test/shelf_proxy_test.dart
@@ -0,0 +1,259 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library shelf_proxy;
+
+import 'dart:async';
+
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+import 'package:scheduled_test/scheduled_test.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_proxy/shelf_proxy.dart';
+
+/// The URI of the server the current proxy server is proxying to.
+Uri targetUri;
+
+/// The URI of the current proxy server.
+Uri proxyUri;
+
+void main() {
+  group("forwarding", () {
+    test("forwards request method", () {
+      createProxy((request) {
+        expect(request.method, equals('DELETE'));
+        return new shelf.Response.ok(':)');
+      });
+
+      schedule(() => http.delete(proxyUri));
+    });
+
+    test("forwards request headers", () {
+      createProxy((request) {
+        expect(request.headers, containsPair('foo', 'bar'));
+        expect(request.headers, containsPair('accept', '*/*'));
+        return new shelf.Response.ok(':)');
+      });
+
+      get(headers: {
+        'foo': 'bar',
+        'accept': '*/*'
+      });
+    });
+
+    test("forwards request body", () {
+      createProxy((request) {
+        expect(request.readAsString(), completion(equals('hello, server')));
+        return new shelf.Response.ok(':)');
+      });
+
+      schedule(() => http.post(proxyUri, body: 'hello, server'));
+    });
+
+    test("forwards response status", () {
+      createProxy((request) {
+        return new shelf.Response(567);
+      });
+
+      expect(get().then((response) {
+        expect(response.statusCode, equals(567));
+      }), completes);
+    });
+
+    test("forwards response headers", () {
+      createProxy((request) {
+        return new shelf.Response.ok(':)', headers: {
+          'foo': 'bar',
+          'accept': '*/*'
+        });
+      });
+
+      expect(get().then((response) {
+        expect(response.headers, containsPair('foo', 'bar'));
+        expect(response.headers, containsPair('accept', '*/*'));
+      }), completes);
+    });
+
+    test("forwards response body", () {
+      createProxy((request) {
+        return new shelf.Response.ok('hello, client');
+      });
+
+      expect(schedule(() => http.read(proxyUri)),
+          completion(equals('hello, client')));
+    });
+
+    test("adjusts the Host header for the target server", () {
+      createProxy((request) {
+        expect(request.headers, containsPair('host', targetUri.authority));
+        return new shelf.Response.ok(':)');
+      });
+
+      get();
+    });
+  });
+
+  group("via", () {
+    test("adds a Via header to the request", () {
+      createProxy((request) {
+        expect(request.headers, containsPair('via', '1.1 shelf_proxy'));
+        return new shelf.Response.ok(':)');
+      });
+
+      get();
+    });
+
+    test("adds to a request's existing Via header", () {
+      createProxy((request) {
+        expect(request.headers,
+            containsPair('via', '1.0 something, 1.1 shelf_proxy'));
+        return new shelf.Response.ok(':)');
+      });
+
+      get(headers: {'via': '1.0 something'});
+    });
+
+    test("adds a Via header to the response", () {
+      createProxy((request) => new shelf.Response.ok(':)'));
+
+      expect(get().then((response) {
+        expect(response.headers, containsPair('via', '1.1 shelf_proxy'));
+      }), completes);
+    });
+
+    test("adds to a response's existing Via header", () {
+      createProxy((request) {
+        return new shelf.Response.ok(':)', headers: {'via': '1.0 something'});
+      });
+
+      expect(get().then((response) {
+        expect(response.headers,
+            containsPair('via', '1.0 something, 1.1 shelf_proxy'));
+      }), completes);
+    });
+
+    test("adds to a response's existing Via header", () {
+      createProxy((request) {
+        return new shelf.Response.ok(':)', headers: {'via': '1.0 something'});
+      });
+
+      expect(get().then((response) {
+        expect(response.headers,
+            containsPair('via', '1.0 something, 1.1 shelf_proxy'));
+      }), completes);
+    });
+  });
+
+  group("redirects", () {
+    test("doesn't modify a Location for a foreign server", () {
+      createProxy((request) {
+        return new shelf.Response.found('http://dartlang.org');
+      });
+
+      expect(get().then((response) {
+        expect(response.headers,
+            containsPair('location', 'http://dartlang.org'));
+      }), completes);
+    });
+
+    test("relativizes a reachable root-relative Location", () {
+      createProxy((request) {
+        return new shelf.Response.found('/foo/bar');
+      }, targetPath: '/foo');
+
+      expect(get().then((response) {
+        expect(response.headers, containsPair('location', '/bar'));
+      }), completes);
+    });
+
+    test("absolutizes an unreachable root-relative Location", () {
+      createProxy((request) {
+        return new shelf.Response.found('/baz');
+      }, targetPath: '/foo');
+
+      expect(get().then((response) {
+        expect(response.headers,
+            containsPair('location', targetUri.resolve('/baz').toString()));
+      }), completes);
+    });
+  });
+
+  test("removes a transfer-encoding header", () {
+    var handler = mockHandler((request) {
+      return new http.Response('', 200, headers: {
+        'transfer-encoding': 'chunked'
+      });
+    });
+
+    expect(handler(new shelf.Request('GET', Uri.parse('http://localhost/')))
+        .then((response) {
+      expect(response.headers, isNot(contains("transfer-encoding")));
+    }), completes);
+  });
+
+  test("removes content-length and content-encoding for a gzipped response",
+      () {
+    var handler = mockHandler((request) {
+      return new http.Response('', 200, headers: {
+        'content-encoding': 'gzip',
+        'content-length': '1234'
+      });
+    });
+
+    expect(handler(new shelf.Request('GET', Uri.parse('http://localhost/')))
+        .then((response) {
+      expect(response.headers, isNot(contains("content-encoding")));
+      expect(response.headers, isNot(contains("content-length")));
+      expect(response.headers,
+          containsPair('warning', '214 shelf_proxy "GZIP decoded"'));
+    }), completes);
+  });
+}
+
+/// Creates a proxy server proxying to a server running [handler].
+///
+/// [targetPath] is the root-relative path on the target server to proxy to. It
+/// defaults to `/`.
+void createProxy(shelf.Handler handler, {String targetPath}) {
+  handler = wrapAsync(handler, 'target server handler');
+  schedule(() {
+    return shelf_io.serve(handler, 'localhost', 0).then((targetServer) {
+      targetUri = Uri.parse('http://localhost:${targetServer.port}');
+      if (targetPath != null) targetUri = targetUri.resolve(targetPath);
+      var proxyServerHandler = wrapAsync(
+          proxyHandler(targetUri), 'proxy server handler');
+
+      return shelf_io.serve(proxyServerHandler, 'localhost', 0)
+          .then((proxyServer) {
+        proxyUri = Uri.parse('http://localhost:${proxyServer.port}');
+
+        currentSchedule.onComplete.schedule(() {
+          proxyServer.close(force: true);
+          targetServer.close(force: true);
+        }, 'tear down servers');
+      });
+    });
+  }, 'spin up servers');
+}
+
+/// Creates a [shelf.Handler] that's backed by a [MockClient] running
+/// [callback].
+shelf.Handler mockHandler(callback(http.Request request)) {
+  var client = new MockClient((request) {
+    return new Future.sync(() => callback(request));
+  });
+  return proxyHandler('http://dartlang.org', client: client);
+}
+
+/// Schedules a GET request with [headers] to the proxy server.
+Future<http.Response> get({Map<String, String> headers}) {
+  return schedule(() {
+    var uri = proxyUri;
+    var request = new http.Request('GET', uri);
+    if (headers != null) request.headers.addAll(headers);
+    request.followRedirects = false;
+    return request.send().then(http.Response.fromStream);
+  }, 'GET proxy server');
+}
diff --git a/test/static_file_test.dart b/test/static_file_test.dart
deleted file mode 100644
index 411ad58..0000000
--- a/test/static_file_test.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-library shelf_proxy.static_file_test;
-
-import 'dart:async';
-import 'dart:io';
-
-import 'package:scheduled_test/scheduled_test.dart';
-import 'package:shelf/shelf.dart';
-import 'package:shelf/shelf_io.dart' as shelf_io;
-import 'package:shelf_static/shelf_static.dart' as shelf_static;
-import 'package:shelf_proxy/shelf_proxy.dart';
-
-import 'test_util.dart';
-
-void main() {
-  test('default document', () {
-    _scheduleServer(_handler);
-
-    schedule(() {
-      var url = new Uri.http('localhost:$_serverPort', '');
-      var handler = createProxyHandler(url);
-
-      return makeRequest(handler, '/').then((response) {
-        expect(response.statusCode, HttpStatus.OK);
-        expect(response.readAsString(),
-            completion(contains('<title>shelf_static</title>')));
-        expect(response.contentLength, 228);
-      });
-    });
-  });
-}
-
-final _handler = shelf_static.createStaticHandler('test/test_files',
-    defaultDocument: 'index.html');
-
-int _serverPort;
-
-Future _scheduleServer(Handler handler) {
-  return schedule(() => shelf_io.serve(handler, 'localhost', 0).then((server) {
-    currentSchedule.onComplete.schedule(() {
-      _serverPort = null;
-      return server.close(force: true);
-    });
-
-    _serverPort = server.port;
-  }));
-}
diff --git a/test/test_files/dart.png b/test/test_files/dart.png
deleted file mode 100644
index fd6de2a..0000000
--- a/test/test_files/dart.png
+++ /dev/null
Binary files differ
diff --git a/test/test_files/favicon.ico b/test/test_files/favicon.ico
deleted file mode 100644
index e605972..0000000
--- a/test/test_files/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/test/test_files/index.html b/test/test_files/index.html
deleted file mode 100644
index 7bf28d1..0000000
--- a/test/test_files/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-<!DOCTYPE html>
-
-<html>
-  <head>
-  	<meta charset="utf-8">
-    <title>shelf_static</title>
-  </head>
-  <body>
-    <h1>Hello, shelf_static!</h1>
-    <img src='dart.png' alt='Dart logo' width='150' height='151'>
-  </body>
-</html>
diff --git a/test/test_util.dart b/test/test_util.dart
deleted file mode 100644
index b4275dc..0000000
--- a/test/test_util.dart
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-library shelf_proxy.test_util;
-
-import 'dart:async';
-
-import 'package:path/path.dart' as p;
-import 'package:shelf/shelf.dart';
-import 'package:shelf_proxy/src/util.dart';
-
-final p.Context _ctx = p.url;
-
-/// Makes a simple GET request to [handler] and returns the result.
-Future<Response> makeRequest(Handler handler, String path,
-    {String scriptName, Map<String, String> headers, String method}) {
-  var rootedHandler = _rootHandler(scriptName, handler);
-  return syncFuture(() =>
-      rootedHandler(_fromPath(path, headers, method: method)));
-}
-
-Request _fromPath(String path, Map<String, String> headers, {String method}) {
-  if (method == null) method = 'GET';
-  return new Request(method, Uri.parse('http://localhost' + path),
-      headers: headers);
-}
-
-Handler _rootHandler(String scriptName, Handler handler) {
-  if (scriptName == null || scriptName.isEmpty) {
-    return handler;
-  }
-
-  if (!scriptName.startsWith('/')) {
-    throw new ArgumentError('scriptName must start with "/" or be empty');
-  }
-
-  return (Request request) {
-    if (!_ctx.isWithin(scriptName, request.requestedUri.path)) {
-      return new Response.notFound('not found');
-    }
-    assert(request.scriptName.isEmpty);
-
-    var relativeRequest = request.change(scriptName: scriptName);
-
-    return handler(relativeRequest);
-  };
-}
diff --git a/tool/hop_runner.dart b/tool/hop_runner.dart
deleted file mode 100644
index 262c863..0000000
--- a/tool/hop_runner.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-library hop_runner;
-
-import 'dart:async';
-import 'dart:io';
-import 'package:hop/hop.dart';
-import 'package:hop/hop_tasks.dart';
-import 'package:hop_unittest/hop_unittest.dart';
-
-import '../test/harness_console.dart' as test_console;
-
-void main(List<String> args) {
-  addTask('test', createUnitTestTask(test_console.main));
-
-  //
-  // Analyzer
-  //
-  addTask('analyze_libs', createAnalyzerTask(_getLibs));
-
-  addTask('analyze_test_libs', createAnalyzerTask(
-      ['test/harness_console.dart']));
-
-  runHop(args);
-}
-
-Future<List<String>> _getLibs() {
-  return new Directory('lib').list()
-      .where((FileSystemEntity fse) => fse is File)
-      .map((File file) => file.path)
-      .toList();
-}