Created a new synchronous http client using RawSynchronousSockets.

BUG=
R=zra@google.com

Review-Url: https://codereview.chromium.org/2827083002 .
diff --git a/.analysis_options b/.analysis_options
new file mode 100644
index 0000000..a10d4c5
--- /dev/null
+++ b/.analysis_options
@@ -0,0 +1,2 @@
+analyzer:
+  strong-mode: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..80a5bb9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.pub/
+build/
+packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
+.packages
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..389ce98
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2017, 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:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
index 1c8c167..59379b1 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,16 @@
-TODO: add README
+Dart Synchronous HTTP Client
+============================
+
+A simple Dart HTTP client implemented using RawSynchronousSockets to allow for
+synchronous HTTP requests.
+
+Warning: This library should probably only be used to connect to HTTP servers
+that are hosted on 'localhost'. The operations in this library will block the
+calling thread to wait for a response from the HTTP server. The thread can
+process no other events while waiting for the server to respond. As such, this
+synchronous HTTP client library is not suitable for applications that require
+high performance. Instead, such applications should use libraries built on
+asynchronous I/O, including
+[dart:io](https://api.dartlang.org/stable/1.22.1/dart-io/dart-io-library.html)
+and [package:http](https://pub.dartlang.org/packages/http), for the best
+performance.
diff --git a/codereview.settings b/codereview.settings
new file mode 100644
index 0000000..181f5e6
--- /dev/null
+++ b/codereview.settings
@@ -0,0 +1,4 @@
+# This file is used by gcl to get repository specific information.
+CODE_REVIEW_SERVER: http://codereview.chromium.org
+VIEW_VC: https://github.com/dart-lang/sync_http/commit/
+CC_LIST: reviews@dartlang.org
diff --git a/lib/src/line_decoder.dart b/lib/src/line_decoder.dart
new file mode 100644
index 0000000..ced5499
--- /dev/null
+++ b/lib/src/line_decoder.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2017, 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.
+
+part of sync.http;
+
+// '\n' character
+const int _lineTerminator = 10;
+
+typedef void _LineDecoderCallback(
+    String line, int bytesRead, _LineDecoder decoder);
+
+class _LineDecoder {
+  BytesBuilder _unprocessedBytes = new BytesBuilder();
+
+  int expectedByteCount = -1;
+
+  final _LineDecoderCallback _callback;
+
+  _LineDecoder.withCallback(this._callback);
+
+  void add(List<int> chunk) {
+    while (chunk.isNotEmpty) {
+      int splitIndex = -1;
+
+      if (expectedByteCount > 0) {
+        splitIndex = expectedByteCount - _unprocessedBytes.length;
+      } else {
+        splitIndex = chunk.indexOf(_lineTerminator) + 1;
+      }
+
+      if (splitIndex > 0 && splitIndex <= chunk.length) {
+        _unprocessedBytes.add(chunk.sublist(0, splitIndex));
+        chunk = chunk.sublist(splitIndex);
+        expectedByteCount = -1;
+        _process(_unprocessedBytes.takeBytes());
+      } else {
+        _unprocessedBytes.add(chunk);
+        chunk = [];
+      }
+    }
+  }
+
+  void _process(List<int> line) =>
+      _callback(UTF8.decoder.convert(line), line.length, this);
+
+  int get bufferedBytes => _unprocessedBytes.length;
+
+  void close() => _process(_unprocessedBytes.takeBytes());
+}
diff --git a/lib/src/sync_http.dart b/lib/src/sync_http.dart
new file mode 100644
index 0000000..aa5faa3
--- /dev/null
+++ b/lib/src/sync_http.dart
@@ -0,0 +1,596 @@
+// Copyright (c) 2017, 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.
+
+part of sync.http;
+
+/// A simple synchronous HTTP client.
+///
+/// This is a two-step process. When a [SyncHttpClientRequest] is returned the
+/// underlying network connection has been established, but no data has yet been
+/// sent. The HTTP headers and body can be set on the request, and close is
+/// called to send it to the server and get the [SyncHttpClientResponse].
+abstract class SyncHttpClient {
+  /// Send a GET request to the provided URL.
+  static SyncHttpClientRequest getUrl(Uri uri) =>
+      new SyncHttpClientRequest._('GET', uri, false);
+
+  /// Send a POST request to the provided URL.
+  static SyncHttpClientRequest postUrl(uri) =>
+      new SyncHttpClientRequest._('POST', uri, true);
+
+  /// Send a DELETE request to the provided URL.
+  static SyncHttpClientRequest deleteUrl(uri) =>
+      new SyncHttpClientRequest._('DELETE', uri, false);
+
+  /// Send a PUT request to the provided URL.
+  static SyncHttpClientRequest putUrl(uri) =>
+      new SyncHttpClientRequest._('PUT', uri, true);
+}
+
+/// HTTP request for a synchronous client connection.
+class SyncHttpClientRequest {
+  static const String _protocolVersion = '1.1';
+
+  /// The length of the request body. Is set to null when no body exists.
+  int get contentLength => hasBody ? _body.length : null;
+
+  HttpHeaders _headers;
+
+  /// The headers associated with the HTTP request.
+  HttpHeaders get headers {
+    if (_headers == null) {
+      _headers = new _SyncHttpClientRequestHeaders(this);
+    }
+    return _headers;
+  }
+
+  /// The type of HTTP request being made.
+  final String method;
+
+  /// The Uri the HTTP request will be sent to.
+  final Uri uri;
+
+  /// The default encoding for the HTTP request (UTF8).
+  final Encoding encoding = UTF8;
+
+  /// The body of the HTTP request. This can be empty if there is no body
+  /// associated with the request.
+  final BytesBuilder _body;
+
+  /// The synchronous socket used to initiate the HTTP request.
+  final RawSynchronousSocket _socket;
+
+  SyncHttpClientRequest._(this.method, Uri uri, bool body)
+      : this.uri = uri,
+        this._body = body ? new BytesBuilder() : null,
+        this._socket = RawSynchronousSocket.connectSync(uri.host, uri.port);
+
+  /// Write content into the body of the HTTP request.
+  void write(Object obj) {
+    if (hasBody) {
+      _body.add(encoding.encoder.convert(obj.toString()));
+    } else {
+      throw new StateError('write not allowed for method $method');
+    }
+  }
+
+  /// Specifies whether or not the HTTP request has a body.
+  bool get hasBody => _body != null;
+
+  /// Send the HTTP request and get the response.
+  SyncHttpClientResponse close() {
+    StringBuffer buffer = new StringBuffer();
+    buffer.write('$method ${uri.path} HTTP/$_protocolVersion\r\n');
+    headers.forEach((name, values) {
+      values.forEach((value) {
+        buffer.write('$name: $value\r\n');
+      });
+    });
+    buffer.write('\r\n');
+    if (hasBody) {
+      buffer.write(new String.fromCharCodes(_body.takeBytes()));
+    }
+    _socket.writeFromSync(buffer.toString().codeUnits);
+    return new SyncHttpClientResponse(_socket);
+  }
+}
+
+class _SyncHttpClientRequestHeaders implements HttpHeaders {
+  Map<String, List> _headers = <String, List<String>>{};
+
+  final SyncHttpClientRequest _request;
+  ContentType contentType;
+
+  _SyncHttpClientRequestHeaders(this._request);
+
+  @override
+  List<String> operator [](String name) {
+    switch (name) {
+      case HttpHeaders.ACCEPT_CHARSET:
+        return ['utf-8'];
+      case HttpHeaders.ACCEPT_ENCODING:
+        return ['identity'];
+      case HttpHeaders.CONNECTION:
+        return ['close'];
+      case HttpHeaders.CONTENT_LENGTH:
+        if (!_request.hasBody) {
+          return null;
+        }
+        return [contentLength.toString()];
+      case HttpHeaders.CONTENT_TYPE:
+        if (contentType == null) {
+          return null;
+        }
+        return [contentType.toString()];
+      case HttpHeaders.HOST:
+        return ['$host:$port'];
+      default:
+        var values = _headers[name];
+        if (values == null || values.isEmpty) {
+          return null;
+        }
+        return values.map((e) => e.toString()).toList(growable: false);
+    }
+  }
+
+  /// Add [value] to the list of values associated with header [name].
+  @override
+  void add(String name, Object value) {
+    switch (name) {
+      case HttpHeaders.ACCEPT_CHARSET:
+      case HttpHeaders.ACCEPT_ENCODING:
+      case HttpHeaders.CONNECTION:
+      case HttpHeaders.CONTENT_LENGTH:
+      case HttpHeaders.DATE:
+      case HttpHeaders.EXPIRES:
+      case HttpHeaders.IF_MODIFIED_SINCE:
+      case HttpHeaders.HOST:
+        throw new UnsupportedError('Unsupported or immutable property: $name');
+      case HttpHeaders.CONTENT_TYPE:
+        contentType = value;
+        break;
+      default:
+        if (_headers[name] == null) {
+          _headers[name] = [];
+        }
+        _headers[name].add(value);
+    }
+  }
+
+  /// Remove [value] from the list associated with header [name].
+  @override
+  void remove(String name, Object value) {
+    switch (name) {
+      case HttpHeaders.ACCEPT_CHARSET:
+      case HttpHeaders.ACCEPT_ENCODING:
+      case HttpHeaders.CONNECTION:
+      case HttpHeaders.CONTENT_LENGTH:
+      case HttpHeaders.DATE:
+      case HttpHeaders.EXPIRES:
+      case HttpHeaders.IF_MODIFIED_SINCE:
+      case HttpHeaders.HOST:
+        throw new UnsupportedError('Unsupported or immutable property: $name');
+      case HttpHeaders.CONTENT_TYPE:
+        if (contentType == value) {
+          contentType = null;
+        }
+        break;
+      default:
+        if (_headers[name] != null) {
+          _headers[name].remove(value);
+          if (_headers[name].isEmpty) {
+            _headers.remove(name);
+          }
+        }
+    }
+  }
+
+  /// Remove all headers associated with key [name].
+  @override
+  void removeAll(String name) {
+    switch (name) {
+      case HttpHeaders.ACCEPT_CHARSET:
+      case HttpHeaders.ACCEPT_ENCODING:
+      case HttpHeaders.CONNECTION:
+      case HttpHeaders.CONTENT_LENGTH:
+      case HttpHeaders.DATE:
+      case HttpHeaders.EXPIRES:
+      case HttpHeaders.IF_MODIFIED_SINCE:
+      case HttpHeaders.HOST:
+        throw new UnsupportedError('Unsupported or immutable property: $name');
+      case HttpHeaders.CONTENT_TYPE:
+        contentType = null;
+        break;
+      default:
+        _headers.remove(name);
+    }
+  }
+
+  /// Replace values associated with key [name] with [value].
+  @override
+  void set(String name, Object value) {
+    removeAll(name);
+    add(name, value);
+  }
+
+  /// Returns the values associated with key [name], if it exists, otherwise
+  /// returns null.
+  @override
+  String value(String name) {
+    var val = this[name];
+    if (val == null || val.isEmpty) {
+      return null;
+    } else if (val.length == 1) {
+      return val[0];
+    } else {
+      throw new HttpException('header $name has more than one value');
+    }
+  }
+
+  /// Iterates over all header key-value pairs and applies [f].
+  @override
+  void forEach(void f(String name, List<String> values)) {
+    var forEachFunc = (String name) {
+      var values = this[name];
+      if (values != null && values.isNotEmpty) {
+        f(name, values);
+      }
+    };
+
+    [
+      HttpHeaders.ACCEPT_CHARSET,
+      HttpHeaders.ACCEPT_ENCODING,
+      HttpHeaders.CONNECTION,
+      HttpHeaders.CONTENT_LENGTH,
+      HttpHeaders.CONTENT_TYPE,
+      HttpHeaders.HOST
+    ].forEach(forEachFunc);
+    _headers.keys.forEach(forEachFunc);
+  }
+
+  @override
+  bool get chunkedTransferEncoding => null;
+
+  @override
+  void set chunkedTransferEncoding(bool _chunkedTransferEncoding) {
+    throw new UnsupportedError('chunked transfer is unsupported');
+  }
+
+  @override
+  int get contentLength => _request.contentLength;
+
+  @override
+  void set contentLength(int _contentLength) {
+    throw new UnsupportedError('content length is automatically set');
+  }
+
+  @override
+  void set date(DateTime _date) {
+    throw new UnsupportedError('date is unsupported');
+  }
+
+  @override
+  DateTime get date => null;
+
+  @override
+  void set expires(DateTime _expires) {
+    throw new UnsupportedError('expires is unsupported');
+  }
+
+  @override
+  DateTime get expires => null;
+
+  @override
+  void set host(String _host) {
+    throw new UnsupportedError('host is automatically set');
+  }
+
+  @override
+  String get host => _request.uri.host;
+
+  @override
+  DateTime get ifModifiedSince => null;
+
+  @override
+  void set ifModifiedSince(DateTime _ifModifiedSince) {
+    throw new UnsupportedError('if modified since is unsupported');
+  }
+
+  @override
+  void noFolding(String name) {
+    throw new UnsupportedError('no folding is unsupported');
+  }
+
+  @override
+  bool get persistentConnection => false;
+
+  @override
+  void set persistentConnection(bool _persistentConnection) {
+    throw new UnsupportedError('persistence connections are unsupported');
+  }
+
+  @override
+  void set port(int _port) {
+    throw new UnsupportedError('port is automatically set');
+  }
+
+  @override
+  int get port => _request.uri.port;
+
+  /// Clear all header key-value pairs.
+  @override
+  void clear() {
+    contentType = null;
+    _headers.clear();
+  }
+}
+
+/// HTTP response for a client connection.
+class SyncHttpClientResponse {
+  /// The length of the body associated with the HTTP response.
+  int get contentLength => headers.contentLength;
+
+  /// The headers associated with the HTTP response.
+  final HttpHeaders headers;
+
+  /// A short textual description of the status code associated with the HTTP
+  /// response.
+  final String reasonPhrase;
+
+  /// The resulting HTTP status code associated with the HTTP response.
+  final int statusCode;
+
+  /// The body of the HTTP response.
+  final String body;
+
+  /// Creates an instance of [SyncHttpClientResponse] that contains the response
+  /// sent by the HTTP server over [socket].
+  factory SyncHttpClientResponse(RawSynchronousSocket socket) {
+    int statusCode;
+    String reasonPhrase;
+    StringBuffer body = new StringBuffer();
+    Map<String, List<String>> headers = {};
+
+    bool inHeader = false;
+    bool inBody = false;
+    int contentLength = 0;
+    int contentRead = 0;
+
+    void processLine(String line, int bytesRead, _LineDecoder decoder) {
+      if (inBody) {
+        body.write(line);
+        contentRead += bytesRead;
+      } else if (inHeader) {
+        if (line.trim().isEmpty) {
+          inBody = true;
+          if (contentLength > 0) {
+            decoder.expectedByteCount = contentLength;
+          }
+          return;
+        }
+        int separator = line.indexOf(':');
+        String name = line.substring(0, separator).toLowerCase().trim();
+        String value = line.substring(separator + 1).trim();
+        if (name == HttpHeaders.TRANSFER_ENCODING &&
+            value.toLowerCase() != 'identity') {
+          throw new UnsupportedError(
+              'only identity transfer encoding is accepted');
+        }
+        if (name == HttpHeaders.CONTENT_LENGTH) {
+          contentLength = int.parse(value);
+        }
+        if (!headers.containsKey(name)) {
+          headers[name] = [];
+        }
+        headers[name].add(value);
+      } else if (line.startsWith('HTTP/1.1') || line.startsWith('HTTP/1.0')) {
+        statusCode = int
+            .parse(line.substring('HTTP/1.x '.length, 'HTTP/1.x xxx'.length));
+        reasonPhrase = line.substring('HTTP/1.x xxx '.length);
+        inHeader = true;
+      } else {
+        throw new UnsupportedError('unsupported http response format');
+      }
+    }
+
+    var lineDecoder = new _LineDecoder.withCallback(processLine);
+
+    try {
+      while (!inHeader ||
+          !inBody ||
+          ((contentRead + lineDecoder.bufferedBytes) < contentLength)) {
+        var bytes = socket.readSync(1024);
+
+        if (bytes == null || bytes.length == 0) {
+          break;
+        }
+        lineDecoder.add(bytes);
+      }
+    } finally {
+      try {
+        lineDecoder.close();
+      } finally {
+        socket.closeSync();
+      }
+    }
+
+    return new SyncHttpClientResponse._(
+        reasonPhrase: reasonPhrase,
+        statusCode: statusCode,
+        body: body.toString(),
+        headers: headers);
+  }
+
+  SyncHttpClientResponse._(
+      {this.reasonPhrase, this.statusCode, this.body, headers})
+      : this.headers = new _SyncHttpClientResponseHeaders(headers);
+}
+
+class _SyncHttpClientResponseHeaders implements HttpHeaders {
+  final Map<String, List<String>> _headers;
+
+  _SyncHttpClientResponseHeaders(this._headers);
+
+  @override
+  List<String> operator [](String name) => _headers[name];
+
+  @override
+  void add(String name, Object value) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  bool get chunkedTransferEncoding => null;
+
+  @override
+  void set chunkedTransferEncoding(bool _chunkedTransferEncoding) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  int get contentLength {
+    String val = value(HttpHeaders.CONTENT_LENGTH);
+    if (val != null) {
+      return int.parse(val, onError: (_) => null);
+    }
+    return null;
+  }
+
+  @override
+  void set contentLength(int _contentLength) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  ContentType get contentType {
+    var val = value(HttpHeaders.CONTENT_TYPE);
+    if (val != null) {
+      return ContentType.parse(val);
+    }
+    return null;
+  }
+
+  @override
+  void set contentType(ContentType _contentType) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  void set date(DateTime _date) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  DateTime get date {
+    var val = value(HttpHeaders.DATE);
+    if (val != null) {
+      return DateTime.parse(val);
+    }
+    return null;
+  }
+
+  @override
+  void set expires(DateTime _expires) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  DateTime get expires {
+    var val = value(HttpHeaders.EXPIRES);
+    if (val != null) {
+      return DateTime.parse(val);
+    }
+    return null;
+  }
+
+  @override
+  void forEach(void f(String name, List<String> values)) => _headers.forEach(f);
+
+  @override
+  void set host(String _host) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  String get host {
+    var val = value(HttpHeaders.HOST);
+    if (val != null) {
+      return Uri.parse(val).host;
+    }
+    return null;
+  }
+
+  @override
+  DateTime get ifModifiedSince {
+    var val = value(HttpHeaders.IF_MODIFIED_SINCE);
+    if (val != null) {
+      return DateTime.parse(val);
+    }
+    return null;
+  }
+
+  @override
+  void set ifModifiedSince(DateTime _ifModifiedSince) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  void noFolding(String name) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  bool get persistentConnection => false;
+
+  @override
+  void set persistentConnection(bool _persistentConnection) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  void set port(int _port) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  int get port {
+    var val = value(HttpHeaders.HOST);
+    if (val != null) {
+      return Uri.parse(val).port;
+    }
+    return null;
+  }
+
+  @override
+  void remove(String name, Object value) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  void removeAll(String name) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  void set(String name, Object value) {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+
+  @override
+  String value(String name) {
+    var val = this[name];
+    if (val == null || val.isEmpty) {
+      return null;
+    } else if (val.length == 1) {
+      return val[0];
+    } else {
+      throw new HttpException('header $name has more than one value');
+    }
+  }
+
+  @override
+  void clear() {
+    throw new UnsupportedError('Response headers are immutable');
+  }
+}
diff --git a/lib/sync_http.dart b/lib/sync_http.dart
new file mode 100644
index 0000000..7a5e9ee
--- /dev/null
+++ b/lib/sync_http.dart
@@ -0,0 +1,17 @@
+// Copyright (c) 2017, 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 sync.http;
+
+import 'dart:convert';
+import 'dart:io'
+    show
+        BytesBuilder,
+        ContentType,
+        HttpException,
+        HttpHeaders,
+        RawSynchronousSocket;
+
+part 'src/sync_http.dart';
+part 'src/line_decoder.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..5047079
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,9 @@
+name: sync_http
+version: 0.1.0
+author: Dart Team <misc@dartlang.org>
+description: Synchronous HTTP client for Dart.
+homepage: https://github.com/dart-lang/sync_http
+environment:
+  sdk: '>=1.24.0 <2.0.0'
+dev_dependencies:
+  test: ">=0.12.0 <0.13.0"
diff --git a/test/http_basic_test.dart b/test/http_basic_test.dart
new file mode 100644
index 0000000..02a4455
--- /dev/null
+++ b/test/http_basic_test.dart
@@ -0,0 +1,342 @@
+// Copyright (c) 2017, 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.
+
+import "dart:async";
+import "dart:isolate";
+import "dart:io";
+import "package:sync_http/sync_http.dart";
+import "package:test/test.dart";
+
+typedef void ServerCallback(int port);
+
+class TestServerMain {
+  TestServerMain() : _statusPort = new ReceivePort();
+
+  ReceivePort _statusPort; // Port for receiving messages from the server.
+  SendPort _serverPort; // Port for sending messages to the server.
+  ServerCallback _startedCallback;
+
+  void setServerStartedHandler(ServerCallback startedCallback) {
+    _startedCallback = startedCallback;
+  }
+
+  void start() {
+    ReceivePort receivePort = new ReceivePort();
+    Isolate.spawn(startTestServer, receivePort.sendPort);
+    receivePort.first.then((port) {
+      _serverPort = port;
+
+      // Send server start message to the server.
+      var command = new TestServerCommand.start();
+      port.send([command, _statusPort.sendPort]);
+    });
+
+    // Handle status messages from the server.
+    _statusPort.listen((var status) {
+      if (status.isStarted) {
+        _startedCallback(status.port);
+      }
+    });
+  }
+
+  void close() {
+    // Send server stop message to the server.
+    _serverPort.send([new TestServerCommand.stop(), _statusPort.sendPort]);
+    _statusPort.close();
+  }
+}
+
+enum TestServerCommandState {
+  start,
+  stop,
+}
+
+class TestServerCommand {
+  TestServerCommand.start() : _command = TestServerCommandState.start;
+  TestServerCommand.stop() : _command = TestServerCommandState.stop;
+
+  bool get isStart => (_command == TestServerCommandState.start);
+  bool get isStop => (_command == TestServerCommandState.stop);
+
+  TestServerCommandState _command;
+}
+
+enum TestServerStatusState {
+  started,
+  stopped,
+  error,
+}
+
+class TestServerStatus {
+  TestServerStatus.started(this._port) : _state = TestServerStatusState.started;
+  TestServerStatus.stopped() : _state = TestServerStatusState.stopped;
+  TestServerStatus.error() : _state = TestServerStatusState.error;
+
+  bool get isStarted => (_state == TestServerStatusState.started);
+  bool get isStopped => (_state == TestServerStatusState.stopped);
+  bool get isError => (_state == TestServerStatusState.error);
+
+  int get port => _port;
+
+  TestServerStatusState _state;
+  int _port;
+}
+
+void startTestServer(SendPort replyTo) {
+  var server = new TestServer();
+  server.init();
+  replyTo.send(server.dispatchSendPort);
+}
+
+class TestServer {
+  // Echo the request content back to the response.
+  void _echoHandler(HttpRequest request) {
+    var response = request.response;
+    if (request.method != "POST") {
+      response.close();
+      return;
+    }
+    response.contentLength = request.contentLength;
+    request.listen((List<int> data) {
+      var string = new String.fromCharCodes(data);
+      response.write(string);
+      response.close();
+    });
+  }
+
+  // Echo the request content back to the response.
+  void _zeroToTenHandler(HttpRequest request) {
+    var response = request.response;
+    String msg = "01234567890";
+    if (request.method != "GET") {
+      response.close();
+      return;
+    }
+    response.contentLength = msg.length;
+    response.write(msg);
+    response.close();
+  }
+
+  // Return a 404.
+  void _notFoundHandler(HttpRequest request) {
+    var response = request.response;
+    response.statusCode = HttpStatus.NOT_FOUND;
+    String msg = "Page not found";
+    response.contentLength = msg.length;
+    response.headers.set("Content-Type", "text/html; charset=UTF-8");
+    response.write(msg);
+    response.close();
+  }
+
+  // Return a 301 with a custom reason phrase.
+  void _reasonForMovingHandler(HttpRequest request) {
+    var response = request.response;
+    response.statusCode = HttpStatus.MOVED_PERMANENTLY;
+    response.reasonPhrase = "Don't come looking here any more";
+    response.close();
+  }
+
+  // Check the "Host" header.
+  void _hostHandler(HttpRequest request) {
+    var response = request.response;
+    expect(1, equals(request.headers["Host"].length));
+    expect("www.dartlang.org:1234", equals(request.headers["Host"][0]));
+    expect("www.dartlang.org", equals(request.headers.host));
+    expect(1234, equals(request.headers.port));
+    response.statusCode = HttpStatus.OK;
+    response.close();
+  }
+
+  void _hugeHandler(HttpRequest request) {
+    var response = request.response;
+    List<int> expected =
+        new List<int>.generate((1 << 20), (i) => (i + 1) % 256);
+    String msg = expected.toString();
+    response.contentLength = msg.length;
+    response.statusCode = HttpStatus.OK;
+    response.write(msg);
+    response.close();
+  }
+
+  void init() {
+    // Setup request handlers.
+    _requestHandlers = new Map();
+    _requestHandlers["/echo"] = _echoHandler;
+    _requestHandlers["/0123456789"] = _zeroToTenHandler;
+    _requestHandlers["/reasonformoving"] = _reasonForMovingHandler;
+    _requestHandlers["/host"] = _hostHandler;
+    _requestHandlers["/huge"] = _hugeHandler;
+    _dispatchPort = new ReceivePort();
+    _dispatchPort.listen(dispatch);
+  }
+
+  SendPort get dispatchSendPort => _dispatchPort.sendPort;
+
+  void dispatch(var message) {
+    TestServerCommand command = message[0];
+    SendPort replyTo = message[1];
+    if (command.isStart) {
+      try {
+        HttpServer.bind("127.0.0.1", 0).then((server) {
+          _server = server;
+          _server.listen(_requestReceivedHandler);
+          replyTo.send(new TestServerStatus.started(_server.port));
+        });
+      } catch (e) {
+        replyTo.send(new TestServerStatus.error());
+      }
+    } else if (command.isStop) {
+      _server.close();
+      _dispatchPort.close();
+      replyTo.send(new TestServerStatus.stopped());
+    }
+  }
+
+  void _requestReceivedHandler(HttpRequest request) {
+    var requestHandler = _requestHandlers[request.uri.path];
+    if (requestHandler != null) {
+      requestHandler(request);
+    } else {
+      _notFoundHandler(request);
+    }
+  }
+
+  HttpServer _server; // HTTP server instance.
+  ReceivePort _dispatchPort;
+  Map _requestHandlers;
+}
+
+Future testStartStop() async {
+  Completer completer = new Completer();
+  TestServerMain testServerMain = new TestServerMain();
+  testServerMain.setServerStartedHandler((int port) {
+    testServerMain.close();
+    completer.complete();
+  });
+  testServerMain.start();
+  return completer.future;
+}
+
+Future testGET() async {
+  Completer completer = new Completer();
+  TestServerMain testServerMain = new TestServerMain();
+  testServerMain.setServerStartedHandler((int port) {
+    var request =
+        SyncHttpClient.getUrl(new Uri.http("127.0.0.1:$port", "/0123456789"));
+    var response = request.close();
+    expect(HttpStatus.OK, equals(response.statusCode));
+    expect(11, equals(response.contentLength));
+    expect("01234567890", equals(response.body));
+    testServerMain.close();
+    completer.complete();
+  });
+  testServerMain.start();
+  return completer.future;
+}
+
+Future testPOST() async {
+  Completer completer = new Completer();
+  String data = "ABCDEFGHIJKLMONPQRSTUVWXYZ";
+  final int kMessageCount = 10;
+
+  TestServerMain testServerMain = new TestServerMain();
+
+  void runTest(int port) {
+    int count = 0;
+    void sendRequest() {
+      var request =
+          SyncHttpClient.postUrl(new Uri.http("127.0.0.1:$port", "/echo"));
+      request.write(data);
+      var response = request.close();
+      expect(HttpStatus.OK, equals(response.statusCode));
+      expect(data, equals(response.body));
+      count++;
+      if (count < kMessageCount) {
+        sendRequest();
+      } else {
+        testServerMain.close();
+        completer.complete();
+      }
+    }
+
+    sendRequest();
+  }
+
+  testServerMain.setServerStartedHandler(runTest);
+  testServerMain.start();
+  return completer.future;
+}
+
+Future test404() async {
+  Completer completer = new Completer();
+  TestServerMain testServerMain = new TestServerMain();
+  testServerMain.setServerStartedHandler((int port) {
+    var request = SyncHttpClient
+        .getUrl(new Uri.http("127.0.0.1:$port", "/thisisnotfound"));
+    var response = request.close();
+    expect(HttpStatus.NOT_FOUND, equals(response.statusCode));
+    expect("Page not found", equals(response.body));
+    testServerMain.close();
+    completer.complete();
+  });
+  testServerMain.start();
+  return completer.future;
+}
+
+Future testReasonPhrase() async {
+  Completer completer = new Completer();
+  TestServerMain testServerMain = new TestServerMain();
+  testServerMain.setServerStartedHandler((int port) {
+    var request = SyncHttpClient
+        .getUrl(new Uri.http("127.0.0.1:$port", "/reasonformoving"));
+    var response = request.close();
+    expect(HttpStatus.MOVED_PERMANENTLY, equals(response.statusCode));
+    expect(
+        "Don't come looking here any more\r\n", equals(response.reasonPhrase));
+    testServerMain.close();
+    completer.complete();
+  });
+  testServerMain.start();
+  return completer.future;
+}
+
+Future testHuge() async {
+  Completer completer = new Completer();
+  TestServerMain testServerMain = new TestServerMain();
+  testServerMain.setServerStartedHandler((int port) {
+    var request =
+        SyncHttpClient.getUrl(new Uri.http("127.0.0.1:$port", "/huge"));
+    var response = request.close();
+    String expected =
+        new List<int>.generate((1 << 20), (i) => (i + 1) % 256).toString();
+    expect(HttpStatus.OK, equals(response.statusCode));
+    expect(expected.length, equals(response.contentLength));
+    expect(expected.toString(), equals(response.body));
+    testServerMain.close();
+    completer.complete();
+  });
+  testServerMain.start();
+  return completer.future;
+}
+
+void main() {
+  test("Simple server test", () async {
+    await testStartStop();
+  });
+  test("Sync HTTP GET test", () async {
+    await testGET();
+  });
+  test("Sync HTTP POST test", () async {
+    await testPOST();
+  });
+  test("Sync HTTP 404 test", () async {
+    await test404();
+  });
+  test("Sync HTTP moved test", () async {
+    await testReasonPhrase();
+  });
+  test("Sync HTTP huge test", () async {
+    await testHuge();
+  });
+}