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();
+ });
+}