Explorar el Código

Bytes API for haxe.Http (#8570)

Aleksandr Kuzmenko hace 6 años
padre
commit
019ea83cdb

+ 9 - 4
std/flash/_std/haxe/Http.hx

@@ -22,6 +22,8 @@
 
 
 package haxe;
 package haxe;
 
 
+import haxe.io.Bytes;
+
 typedef Http = HttpFlash;
 typedef Http = HttpFlash;
 
 
 class HttpFlash extends haxe.http.HttpBase {
 class HttpFlash extends haxe.http.HttpBase {
@@ -39,12 +41,12 @@ class HttpFlash extends haxe.http.HttpBase {
 	}
 	}
 
 
 	public override function request(?post:Bool) {
 	public override function request(?post:Bool) {
-		responseData = null;
+		responseBytes = null;
 		var loader = req = new flash.net.URLLoader();
 		var loader = req = new flash.net.URLLoader();
+		loader.dataFormat = BINARY;
 		loader.addEventListener("complete", function(e) {
 		loader.addEventListener("complete", function(e) {
 			req = null;
 			req = null;
-			responseData = loader.data;
-			onData(loader.data);
+			success(Bytes.ofData(loader.data));
 		});
 		});
 		loader.addEventListener("httpStatus", function(e:flash.events.HTTPStatusEvent) {
 		loader.addEventListener("httpStatus", function(e:flash.events.HTTPStatusEvent) {
 			// on Firefox 1.5, Flash calls onHTTPStatus with 0 (!??)
 			// on Firefox 1.5, Flash calls onHTTPStatus with 0 (!??)
@@ -53,7 +55,7 @@ class HttpFlash extends haxe.http.HttpBase {
 		});
 		});
 		loader.addEventListener("ioError", function(e:flash.events.IOErrorEvent) {
 		loader.addEventListener("ioError", function(e:flash.events.IOErrorEvent) {
 			req = null;
 			req = null;
-			responseData = loader.data;
+			responseBytes = Bytes.ofData(loader.data);
 			onError(e.text);
 			onError(e.text);
 		});
 		});
 		loader.addEventListener("securityError", function(e:flash.events.SecurityErrorEvent) {
 		loader.addEventListener("securityError", function(e:flash.events.SecurityErrorEvent) {
@@ -86,6 +88,9 @@ class HttpFlash extends haxe.http.HttpBase {
 		if (postData != null) {
 		if (postData != null) {
 			request.data = postData;
 			request.data = postData;
 			request.method = "POST";
 			request.method = "POST";
+		} else if (postBytes != null) {
+			request.data = postBytes.getData();
+			request.method = "POST";
 		} else {
 		} else {
 			request.data = vars;
 			request.data = vars;
 			request.method = if (post) "POST" else "GET";
 			request.method = if (post) "POST" else "GET";

+ 69 - 7
std/haxe/http/HttpBase.hx

@@ -22,6 +22,8 @@
 
 
 package haxe.http;
 package haxe.http;
 
 
+import haxe.io.Bytes;
+
 private typedef StringKeyValue = {
 private typedef StringKeyValue = {
 	var name:String;
 	var name:String;
 	var value:String;
 	var value:String;
@@ -44,12 +46,17 @@ class HttpBase {
 	**/
 	**/
 	public var url:String;
 	public var url:String;
 
 
-	public var responseData(default, null):Null<String>;
+	public var responseData(get,never):Null<String>;
+	public var responseBytes(default,null):Null<Bytes>;
 
 
-	var postData:String;
+	var responseAsString:Null<String>;
+	var postData:Null<String>;
+	var postBytes:Null<Bytes>;
 	var headers:Array<StringKeyValue>;
 	var headers:Array<StringKeyValue>;
 	var params:Array<StringKeyValue>;
 	var params:Array<StringKeyValue>;
 
 
+	final emptyOnData:(String)->Void;
+
 	/**
 	/**
 		Creates a new Http instance with `url` as parameter.
 		Creates a new Http instance with `url` as parameter.
 
 
@@ -65,6 +72,7 @@ class HttpBase {
 		this.url = url;
 		this.url = url;
 		headers = [];
 		headers = [];
 		params = [];
 		params = [];
+		emptyOnData = onData;
 	}
 	}
 
 
 	/**
 	/**
@@ -122,17 +130,36 @@ class HttpBase {
 	}
 	}
 
 
 	/**
 	/**
-		Sets the post data of `this` Http request to `data`.
+		Sets the post data of `this` Http request to `data` string.
 
 
-		There can only be one post data per request. Subsequent calls overwrite
-		the previously set value.
+		There can only be one post data per request. Subsequent calls to
+		this method or to `setPostBytes()` overwrite the previously set value.
 
 
 		If `data` is null, the post data is considered to be absent.
 		If `data` is null, the post data is considered to be absent.
 
 
 		This method provides a fluent interface.
 		This method provides a fluent interface.
 	**/
 	**/
-	public function setPostData(data:String) {
+	public function setPostData(data:Null<String>) {
 		postData = data;
 		postData = data;
+		postBytes = null;
+		#if hx3compat
+		return this;
+		#end
+	}
+
+	/**
+		Sets the post data of `this` Http request to `data` bytes.
+
+		There can only be one post data per request. Subsequent calls to
+		this method or to `setPostData()` overwrite the previously set value.
+
+		If `data` is null, the post data is considered to be absent.
+
+		This method provides a fluent interface.
+	**/
+	public function setPostBytes(data:Null<Bytes>) {
+		postBytes = data;
+		postData = null;
 		#if hx3compat
 		#if hx3compat
 		return this;
 		return this;
 		#end
 		#end
@@ -145,7 +172,7 @@ class HttpBase {
 		sent as GET request.
 		sent as GET request.
 
 
 		Depending on the outcome of the request, this method calls the
 		Depending on the outcome of the request, this method calls the
-		`onStatus()`, `onError()` or `onData()` callback functions.
+		`onStatus()`, `onError()`, `onData()` or `onBytes()` callback functions.
 
 
 		If `this.url` is null, the result is unspecified.
 		If `this.url` is null, the result is unspecified.
 
 
@@ -168,6 +195,15 @@ class HttpBase {
 	**/
 	**/
 	public dynamic function onData(data:String) {}
 	public dynamic function onData(data:String) {}
 
 
+	/**
+		This method is called upon a successful request, with `data` containing
+		the result String.
+
+		The intended usage is to bind it to a custom function:
+		`httpInstance.onBytes = function(data) { // handle result }`
+	**/
+	public dynamic function onBytes(data:Bytes) {}
+
 	/**
 	/**
 		This method is called upon a request error, with `msg` containing the
 		This method is called upon a request error, with `msg` containing the
 		error description.
 		error description.
@@ -185,4 +221,30 @@ class HttpBase {
 		`httpInstance.onStatus = function(status) { // handle status }`
 		`httpInstance.onStatus = function(status) { // handle status }`
 	**/
 	**/
 	public dynamic function onStatus(status:Int) {}
 	public dynamic function onStatus(status:Int) {}
+
+	/**
+		Override this if extending `haxe.Http` with overriding `onData`
+	**/
+	function hasOnData():Bool {
+		return !Reflect.compareMethods(onData, emptyOnData);
+	}
+
+	function success(data:Bytes) {
+		responseBytes = data;
+		if (hasOnData()) {
+			onData(responseData);
+		}
+		onBytes(responseBytes);
+	}
+
+	function get_responseData() {
+		if (responseAsString == null && responseBytes != null) {
+			#if neko
+			responseAsString = neko.Lib.stringReference(responseBytes);
+			#else
+			responseAsString = responseBytes.getString(0, responseBytes.length, UTF8);
+			#end
+		}
+		return responseAsString;
+	}
 }
 }

+ 18 - 7
std/haxe/http/HttpJs.hx

@@ -23,6 +23,10 @@
 package haxe.http;
 package haxe.http;
 
 
 #if js
 #if js
+import js.html.XMLHttpRequestResponseType;
+import js.html.Blob;
+import haxe.io.Bytes;
+
 class HttpJs extends haxe.http.HttpBase {
 class HttpJs extends haxe.http.HttpBase {
 	public var async:Bool;
 	public var async:Bool;
 	public var withCredentials:Bool;
 	public var withCredentials:Bool;
@@ -47,7 +51,8 @@ class HttpJs extends haxe.http.HttpBase {
 	}
 	}
 
 
 	public override function request(?post:Bool) {
 	public override function request(?post:Bool) {
-		responseData = null;
+		this.responseAsString = null;
+		this.responseBytes = null;
 		var r = req = js.Browser.createXMLHttpRequest();
 		var r = req = js.Browser.createXMLHttpRequest();
 		var onreadystatechange = function(_) {
 		var onreadystatechange = function(_) {
 			if (r.readyState != 4)
 			if (r.readyState != 4)
@@ -59,7 +64,7 @@ class HttpJs extends haxe.http.HttpBase {
 				var rlocalProtocol = ~/^(?:about|app|app-storage|.+-extension|file|res|widget):$/;
 				var rlocalProtocol = ~/^(?:about|app|app-storage|.+-extension|file|res|widget):$/;
 				var isLocal = rlocalProtocol.match(protocol);
 				var isLocal = rlocalProtocol.match(protocol);
 				if (isLocal) {
 				if (isLocal) {
-					s = r.responseText != null ? 200 : 404;
+					s = r.response != null ? 200 : 404;
 				}
 				}
 			}
 			}
 			if (s == js.Lib.undefined)
 			if (s == js.Lib.undefined)
@@ -68,7 +73,7 @@ class HttpJs extends haxe.http.HttpBase {
 				onStatus(s);
 				onStatus(s);
 			if (s != null && s >= 200 && s < 400) {
 			if (s != null && s >= 200 && s < 400) {
 				req = null;
 				req = null;
-				onData(responseData = r.responseText);
+				success(Bytes.ofData(r.response));
 			} else if (s == null) {
 			} else if (s == null) {
 				req = null;
 				req = null;
 				onError("Failed to connect or resolve host");
 				onError("Failed to connect or resolve host");
@@ -82,13 +87,18 @@ class HttpJs extends haxe.http.HttpBase {
 						onError("Unknown host");
 						onError("Unknown host");
 					default:
 					default:
 						req = null;
 						req = null;
-						responseData = r.responseText;
+						responseBytes = Bytes.ofData(r.response);
 						onError("Http Error #" + r.status);
 						onError("Http Error #" + r.status);
 				}
 				}
 		};
 		};
 		if (async)
 		if (async)
 			r.onreadystatechange = onreadystatechange;
 			r.onreadystatechange = onreadystatechange;
-		var uri = postData;
+		var uri:Null<Any> = switch [postData, postBytes] {
+			case [null, null]: null;
+			case [str, null]: str;
+			case [null, bytes]: new Blob([bytes.getData()]);
+			case _: null;
+		}
 		if (uri != null)
 		if (uri != null)
 			post = true;
 			post = true;
 		else
 		else
@@ -96,8 +106,8 @@ class HttpJs extends haxe.http.HttpBase {
 				if (uri == null)
 				if (uri == null)
 					uri = "";
 					uri = "";
 				else
 				else
-					uri += "&";
-				uri += StringTools.urlEncode(p.name) + "=" + StringTools.urlEncode(p.value);
+					uri = uri + "&";
+				uri = uri + StringTools.urlEncode(p.name) + "=" + StringTools.urlEncode(p.value);
 			}
 			}
 		try {
 		try {
 			if (post)
 			if (post)
@@ -108,6 +118,7 @@ class HttpJs extends haxe.http.HttpBase {
 				uri = null;
 				uri = null;
 			} else
 			} else
 				r.open("GET", url, async);
 				r.open("GET", url, async);
+			r.responseType = ARRAYBUFFER;
 		} catch (e:Dynamic) {
 		} catch (e:Dynamic) {
 			req = null;
 			req = null;
 			onError(e.toString());
 			onError(e.toString());

+ 20 - 8
std/haxe/http/HttpNodeJs.hx

@@ -23,6 +23,9 @@
 package haxe.http;
 package haxe.http;
 
 
 #if nodejs
 #if nodejs
+import js.node.Buffer;
+import haxe.io.Bytes;
+
 class HttpNodeJs extends haxe.http.HttpBase {
 class HttpNodeJs extends haxe.http.HttpBase {
 	var req:js.node.http.ClientRequest;
 	var req:js.node.http.ClientRequest;
 
 
@@ -42,7 +45,8 @@ class HttpNodeJs extends haxe.http.HttpBase {
 	}
 	}
 
 
 	public override function request(?post:Bool) {
 	public override function request(?post:Bool) {
-		responseData = null;
+		responseAsString = null;
+		responseBytes = null;
 		var parsedUrl = js.node.Url.parse(url);
 		var parsedUrl = js.node.Url.parse(url);
 		var secure = (parsedUrl.protocol == "https:");
 		var secure = (parsedUrl.protocol == "https:");
 		var host = parsedUrl.hostname;
 		var host = parsedUrl.hostname;
@@ -58,7 +62,7 @@ class HttpNodeJs extends haxe.http.HttpBase {
 
 
 			arr.push(i.value);
 			arr.push(i.value);
 		}
 		}
-		if (postData != null)
+		if (postData != null || postBytes != null)
 			post = true;
 			post = true;
 		var uri = null;
 		var uri = null;
 		for (p in params) {
 		for (p in params) {
@@ -81,18 +85,20 @@ class HttpNodeJs extends haxe.http.HttpBase {
 			headers: h
 			headers: h
 		};
 		};
 		function httpResponse(res) {
 		function httpResponse(res) {
+			res.setEncoding('binary');
 			var s = res.statusCode;
 			var s = res.statusCode;
 			if (s != null)
 			if (s != null)
 				onStatus(s);
 				onStatus(s);
-			var body = '';
-			res.on('data', function(d) {
-				body += d;
+			var data = [];
+			res.on('data', function(chunk:String) {
+				data.push(Buffer.from(chunk, 'binary'));
 			});
 			});
 			res.on('end', function(_) {
 			res.on('end', function(_) {
-				responseData = body;
+				var buf = (data.length == 1 ? data[0] : Buffer.concat(data));
+				responseBytes = Bytes.ofData(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
 				req = null;
 				req = null;
 				if (s != null && s >= 200 && s < 400) {
 				if (s != null && s >= 200 && s < 400) {
-					onData(body);
+					success(responseBytes);
 				} else {
 				} else {
 					onError("Http Error #" + s);
 					onError("Http Error #" + s);
 				}
 				}
@@ -100,7 +106,13 @@ class HttpNodeJs extends haxe.http.HttpBase {
 		}
 		}
 		req = secure ? js.node.Https.request(untyped opts, httpResponse) : js.node.Http.request(untyped opts, httpResponse);
 		req = secure ? js.node.Https.request(untyped opts, httpResponse) : js.node.Http.request(untyped opts, httpResponse);
 		if (post)
 		if (post)
-			req.write(postData);
+			if (postData != null) {
+				req.write(postData);
+			} else if(postBytes != null) {
+				req.setHeader("Content-Length", '${postBytes.length}');
+				req.write(Buffer.from(postBytes.getData()));
+			}
+
 		req.end();
 		req.end();
 	}
 	}
 }
 }

+ 1 - 1
std/lua/Table.hx

@@ -59,7 +59,7 @@ extern class Table<A, B> implements ArrayAccess<B> implements Dynamic<B> {
 	}
 	}
 
 
 	@:overload(function<A, B>(table:Table<A, B>):Void {})
 	@:overload(function<A, B>(table:Table<A, B>):Void {})
-	public static function concat<A, B>(table:Table<A, B>, ?sep:String):String;
+	public static function concat<A, B>(table:Table<A, B>, ?sep:String, ?i:Int, ?j:Int):String;
 
 
 	public static function foreach<A, B>(table:Table<A, B>, f:A->B->Void):Void;
 	public static function foreach<A, B>(table:Table<A, B>, f:A->B->Void):Void;
 	public static function foreachi<A, B>(table:Table<A, B>, f:A->B->Int->Void):Void;
 	public static function foreachi<A, B>(table:Table<A, B>, f:A->B->Int->Void):Void;

+ 33 - 9
std/lua/_std/sys/net/Socket.hx

@@ -25,7 +25,9 @@ package sys.net;
 import lua.lib.luasocket.Socket as LuaSocket;
 import lua.lib.luasocket.Socket as LuaSocket;
 import lua.lib.luasocket.socket.*;
 import lua.lib.luasocket.socket.*;
 import lua.*;
 import lua.*;
+
 import haxe.io.Bytes;
 import haxe.io.Bytes;
+import haxe.io.Error;
 
 
 /**
 /**
 	A TCP socket class : allow you to both connect to a given server and exchange messages or start your own server and wait for connections.
 	A TCP socket class : allow you to both connect to a given server and exchange messages or start your own server and wait for connections.
@@ -223,16 +225,17 @@ class Socket {
 }
 }
 
 
 private class SocketInput extends haxe.io.Input {
 private class SocketInput extends haxe.io.Input {
-	var s:TcpClient;
+	var tcp:TcpClient;
 
 
-	public function new(s:TcpClient) {
-		this.s = s;
+	public function new(tcp:TcpClient) {
+		this.tcp = tcp;
 	}
 	}
 
 
 	override public function readByte():Int {
 	override public function readByte():Int {
-		var res = s.receive(1);
-		if (res.message == "closed")
+		var res = tcp.receive(1);
+		if (res.message == "closed"){
 			throw new haxe.io.Eof();
 			throw new haxe.io.Eof();
+		}
 		else if (res.message != null)
 		else if (res.message != null)
 			throw 'Error : ${res.message}';
 			throw 'Error : ${res.message}';
 		return res.result.charCodeAt(0);
 		return res.result.charCodeAt(0);
@@ -258,16 +261,37 @@ private class SocketInput extends haxe.io.Input {
 		}
 		}
 		return readCount;
 		return readCount;
 	}
 	}
+
 }
 }
 
 
 private class SocketOutput extends haxe.io.Output {
 private class SocketOutput extends haxe.io.Output {
-	var s:TcpClient;
+	var tcp:TcpClient;
 
 
-	public function new(s:TcpClient) {
-		this.s = s;
+	public function new(tcp:TcpClient) {
+		this.tcp = tcp;
 	}
 	}
 
 
 	override public function writeByte(c:Int):Void {
 	override public function writeByte(c:Int):Void {
-		s.send(String.fromCharCode(c));
+		var char = NativeStringTools.char(c);
+		var res = tcp.send(char);
+		if (res.message != null){
+			throw 'Error : Socket writeByte : ${res.message}';
+		}
+	}
+
+	override public function writeBytes(s:Bytes, pos:Int, len:Int):Int {
+		if (pos < 0 || len < 0 || pos + len > s.length)
+			throw Error.OutsideBounds;
+		var b = s.getData().slice(pos, pos +len).map(function(byte){
+			return lua.NativeStringTools.char(byte);
+		});
+		var encoded = Table.concat(cast b, 0);
+		var res = tcp.send(encoded);
+		if (res.message != null){
+			throw 'Error : Socket writeByte : ${res.message}';
+		}
+
+		return len;
 	}
 	}
+
 }
 }

+ 59 - 55
std/sys/Http.hx

@@ -22,6 +22,9 @@
 
 
 package sys;
 package sys;
 
 
+import haxe.io.BytesOutput;
+import haxe.io.Bytes;
+import haxe.io.Input;
 import sys.net.Host;
 import sys.net.Host;
 import sys.net.Socket;
 import sys.net.Socket;
 
 
@@ -55,23 +58,17 @@ class Http extends haxe.http.HttpBase {
 		var old = onError;
 		var old = onError;
 		var err = false;
 		var err = false;
 		onError = function(e) {
 		onError = function(e) {
-			#if neko
-			responseData = neko.Lib.stringReference(output.getBytes());
-			#else
-			responseData = output.getBytes().toString();
-			#end
+			responseBytes = output.getBytes();
 			err = true;
 			err = true;
 			// Resetting back onError before calling it allows for a second "retry" request to be sent without onError being wrapped twice
 			// Resetting back onError before calling it allows for a second "retry" request to be sent without onError being wrapped twice
 			onError = old;
 			onError = old;
 			onError(e);
 			onError(e);
 		}
 		}
+		post = post || postBytes != null || postData != null;
 		customRequest(post, output);
 		customRequest(post, output);
-		if (!err)
-			#if neko
-			onData(responseData = neko.Lib.stringReference(output.getBytes()));
-			#else
-			onData(responseData = output.getBytes().toString());
-			#end
+		if (!err) {
+			success(output.getBytes());
+		}
 	}
 	}
 
 
 	@:noCompletion
 	@:noCompletion
@@ -91,7 +88,8 @@ class Http extends haxe.http.HttpBase {
 	}
 	}
 
 
 	public function customRequest(post:Bool, api:haxe.io.Output, ?sock:sys.net.Socket, ?method:String) {
 	public function customRequest(post:Bool, api:haxe.io.Output, ?sock:sys.net.Socket, ?method:String) {
-		this.responseData = null;
+		this.responseAsString = null;
+		this.responseBytes = null;
 		var url_regexp = ~/^(https?:\/\/)?([a-zA-Z\.0-9_-]+)(:[0-9]+)?(.*)$/;
 		var url_regexp = ~/^(https?:\/\/)?([a-zA-Z\.0-9_-]+)(:[0-9]+)?(.*)$/;
 		if (!url_regexp.match(url)) {
 		if (!url_regexp.match(url)) {
 			onError("Invalid URL");
 			onError("Invalid URL");
@@ -175,72 +173,76 @@ class Http extends haxe.http.HttpBase {
 			}
 			}
 		}
 		}
 
 
-		var b = new StringBuf();
+		var b = new BytesOutput();
 		if (method != null) {
 		if (method != null) {
-			b.add(method);
-			b.add(" ");
+			b.writeString(method);
+			b.writeString(" ");
 		} else if (post)
 		} else if (post)
-			b.add("POST ");
+			b.writeString("POST ");
 		else
 		else
-			b.add("GET ");
+			b.writeString("GET ");
 
 
 		if (Http.PROXY != null) {
 		if (Http.PROXY != null) {
-			b.add("http://");
-			b.add(host);
+			b.writeString("http://");
+			b.writeString(host);
 			if (port != 80) {
 			if (port != 80) {
-				b.add(":");
-				b.add(port);
+				b.writeString(":");
+				b.writeString('$port');
 			}
 			}
 		}
 		}
-		b.add(request);
+		b.writeString(request);
 
 
 		if (!post && uri != null) {
 		if (!post && uri != null) {
 			if (request.indexOf("?", 0) >= 0)
 			if (request.indexOf("?", 0) >= 0)
-				b.add("&");
+				b.writeString("&");
 			else
 			else
-				b.add("?");
-			b.add(uri);
+				b.writeString("?");
+			b.writeString(uri);
+		}
+		b.writeString(" HTTP/1.1\r\nHost: " + host + "\r\n");
+		if (postData != null) {
+			postBytes = Bytes.ofString(postData);
+			postData = null;
 		}
 		}
-		b.add(" HTTP/1.1\r\nHost: " + host + "\r\n");
-		if (postData != null)
-			b.add("Content-Length: " + postData.length + "\r\n");
+		if (postBytes != null)
+			b.writeString("Content-Length: " + postBytes.length + "\r\n");
 		else if (post && uri != null) {
 		else if (post && uri != null) {
 			if (multipart || !Lambda.exists(headers, function(h) return h.name == "Content-Type")) {
 			if (multipart || !Lambda.exists(headers, function(h) return h.name == "Content-Type")) {
-				b.add("Content-Type: ");
+				b.writeString("Content-Type: ");
 				if (multipart) {
 				if (multipart) {
-					b.add("multipart/form-data");
-					b.add("; boundary=");
-					b.add(boundary);
+					b.writeString("multipart/form-data");
+					b.writeString("; boundary=");
+					b.writeString(boundary);
 				} else
 				} else
-					b.add("application/x-www-form-urlencoded");
-				b.add("\r\n");
+					b.writeString("application/x-www-form-urlencoded");
+				b.writeString("\r\n");
 			}
 			}
 			if (multipart)
 			if (multipart)
-				b.add("Content-Length: " + (uri.length + file.size + boundary.length + 6) + "\r\n");
+				b.writeString("Content-Length: " + (uri.length + file.size + boundary.length + 6) + "\r\n");
 			else
 			else
-				b.add("Content-Length: " + uri.length + "\r\n");
+				b.writeString("Content-Length: " + uri.length + "\r\n");
 		}
 		}
-		b.add("Connection: close\r\n");
+		b.writeString("Connection: close\r\n");
 		for (h in headers) {
 		for (h in headers) {
-			b.add(h.name);
-			b.add(": ");
-			b.add(h.value);
-			b.add("\r\n");
+			b.writeString(h.name);
+			b.writeString(": ");
+			b.writeString(h.value);
+			b.writeString("\r\n");
 		}
 		}
-		b.add("\r\n");
-		if (postData != null)
-			b.add(postData);
+		b.writeString("\r\n");
+		if (postBytes != null)
+			b.writeFullBytes(postBytes, 0, postBytes.length);
 		else if (post && uri != null)
 		else if (post && uri != null)
-			b.add(uri);
+			b.writeString(uri);
 		try {
 		try {
 			if (Http.PROXY != null)
 			if (Http.PROXY != null)
 				sock.connect(new Host(Http.PROXY.host), Http.PROXY.port);
 				sock.connect(new Host(Http.PROXY.host), Http.PROXY.port);
 			else
 			else
 				sock.connect(new Host(host), port);
 				sock.connect(new Host(host), port);
 			if (multipart)
 			if (multipart)
-				writeBody(b.toString(),file.io,file.size,boundary,sock);
+				writeBody(b,file.io,file.size,boundary,sock)
 			else
 			else
-				writeBody(b.toString(),null,0,null,sock);
+				writeBody(b,null,0,null,sock);
 			readHttpResponse(api, sock);
 			readHttpResponse(api, sock);
 			sock.close();
 			sock.close();
 		} catch (e:Dynamic) {
 		} catch (e:Dynamic) {
@@ -251,10 +253,12 @@ class Http extends haxe.http.HttpBase {
 		}
 		}
 	}
 	}
 
 
-	function writeBody(body:String, fileInput:haxe.io.Input, fileSize:Int, boundary:String, sock:sys.net.Socket) {
-		if (body!=null && body.length>0)
-			sock.write(body);
-		if (boundary!=null) {
+	function writeBody(body:Null<BytesOutput>, fileInput:Null<Input>, fileSize:Int, boundary:Null<String>, sock:Socket) {
+		if (body != null) {
+			var bytes = body.getBytes();
+			sock.output.writeFullBytes(bytes, 0, bytes.length);
+		}
+		if (boundary != null) {
 			var bufsize = 4096;
 			var bufsize = 4096;
 			var buf = haxe.io.Bytes.alloc(bufsize);
 			var buf = haxe.io.Bytes.alloc(bufsize);
 			while (fileSize > 0) {
 			while (fileSize > 0) {
@@ -267,10 +271,10 @@ class Http extends haxe.http.HttpBase {
 				sock.output.writeFullBytes(buf, 0, len);
 				sock.output.writeFullBytes(buf, 0, len);
 				fileSize -= len;
 				fileSize -= len;
 			}
 			}
-			sock.write("\r\n");
-			sock.write("--");
-			sock.write(boundary);
-			sock.write("--");
+			sock.output.writeString("\r\n");
+			sock.output.writeString("--");
+			sock.output.writeString(boundary);
+			sock.output.writeString("--");
 		}
 		}
 	}
 	}
 
 

+ 10 - 0
tests/RunCi.hx

@@ -31,6 +31,10 @@ class RunCi {
 
 
 		infoMsg('Going to test: $tests');
 		infoMsg('Going to test: $tests');
 
 
+		changeDirectory('echoServer');
+		runCommand('haxe', ['build.hxml']);
+		changeDirectory(cwd);
+
 		for (test in tests) {
 		for (test in tests) {
 			switch (ci) {
 			switch (ci) {
 				case TravisCI:
 				case TravisCI:
@@ -47,6 +51,9 @@ class RunCi {
 					//pass
 					//pass
 			}
 			}
 
 
+			//run neko-based http echo server
+			var echoServer = new sys.io.Process('nekotools', ['server', '-d', 'echoServer/www/', '-p', '20200']);
+
 			infoMsg('test $test');
 			infoMsg('test $test');
 			var success = true;
 			var success = true;
 			try {
 			try {
@@ -112,6 +119,9 @@ class RunCi {
 			} else {
 			} else {
 				failMsg('test ${test} failed');
 				failMsg('test ${test} failed');
 			}
 			}
+
+			echoServer.kill();
+			echoServer.close();
 		}
 		}
 
 
 		if (success) {
 		if (success) {

+ 1 - 0
tests/echoServer/.gitignore

@@ -0,0 +1 @@
+www/echoServer.n

+ 6 - 0
tests/echoServer/EchoServer.hx

@@ -0,0 +1,6 @@
+class EchoServer {
+	static function main() {
+		Sys.print(neko.Web.getPostData());
+		Sys.sleep(0.3);
+	}
+}

+ 2 - 0
tests/echoServer/build.hxml

@@ -0,0 +1,2 @@
+-main EchoServer
+-neko www/echoServer.n

+ 7 - 0
tests/echoServer/www/crossdomain.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
+<cross-domain-policy>
+	<site-control permitted-cross-domain-policies="master-only" />
+	<allow-access-from domain="*" to-ports="*" secure="false"/>
+	<allow-http-request-headers-from domain="*" headers="*"/>
+</cross-domain-policy>

+ 1 - 0
tests/unit/compile-flash9.hxml

@@ -7,3 +7,4 @@ compile-each.hxml
 --swf-version 11
 --swf-version 11
 -swf bin/unit9.swf
 -swf bin/unit9.swf
 -swf-lib native_swf/lib.swc
 -swf-lib native_swf/lib.swc
+-D network-sandbox

+ 79 - 0
tests/unit/src/unit/TestHttp.hx

@@ -0,0 +1,79 @@
+package unit;
+
+import utest.Async;
+
+class TestHttp extends Test {
+	public function setupClass() {
+		#if flash
+		flash.system.Security.allowDomain("*");
+		flash.system.Security.allowInsecureDomain("*");
+		flash.system.Security.loadPolicyFile("http://localhost:20200/crossdomain.xml");
+		#end
+	}
+
+	function run(async:Async, test:()->Void) {
+		#if (js && !nodejs)
+		if(js.Syntax.code('typeof XMLHttpRequest == "undefined"')) {
+			noAssert();
+			async.done();
+			return;
+		}
+		#elseif (azure && (hl || java || (flash && (Linux || Mac)) || (cs && Windows)))
+		noAssert();
+		async.done();
+		return;
+		#else
+		test();
+		#end
+	}
+#if !(azure && hl)
+	@:timeout(1000)
+	public function testPostData(async:Async) run(async, () -> {
+		var srcStr = 'hello, world';
+		var d = new haxe.Http('http://localhost:20200/echoServer.n');
+		d.onData = echoStr -> {
+			if(echoStr != srcStr) {
+				assert('String data from Http request is corrupted: $echoStr');
+			}
+			noAssert();
+			async.done();
+		}
+		d.onError = e -> {
+			assert('Failed Http request with string data: $e');
+			async.done();
+		}
+		d.setPostData(srcStr);
+		d.request();
+	});
+
+	@:timeout(1000)
+	public function testPostBytes(async:Async) run(async, () -> {
+		var srcData = haxe.io.Bytes.alloc(256);
+		for(i in 0...srcData.length) {
+			srcData.set(i, i);
+		}
+		var d = new haxe.Http('http://localhost:20200/echoServer.n');
+		d.onBytes = echoData -> {
+			if(srcData.length != echoData.length) {
+				assert('Binary data from Http request is corrupted. Wrong amount of bytes.');
+			}
+			for(i in 0...echoData.length) {
+				switch [srcData.get(i), echoData.get(i)] {
+					case [a, b] if(a != b):
+						assert('Binary data from Http request is corrupted. Invalid byte value at index #$i: (src) $a != $b (echo)');
+						break;
+					case _:
+				}
+			}
+			noAssert();
+			async.done();
+		}
+		d.onError = e -> {
+			assert('Failed Http request with binary data: $e');
+			async.done();
+		}
+		d.setPostBytes(srcData);
+		d.request();
+	});
+#end
+}

+ 3 - 0
tests/unit/src/unit/TestMain.hx

@@ -71,6 +71,9 @@ class TestMain {
 			new TestCasts(),
 			new TestCasts(),
 			new TestSyntaxModule(),
 			new TestSyntaxModule(),
 			new TestNull(),
 			new TestNull(),
+			#if (!azure || !(php && Windows))
+			new TestHttp(),
+			#end
 			#if !no_pattern_matching
 			#if !no_pattern_matching
 			new TestMatch(),
 			new TestMatch(),
 			#end
 			#end