1 module requests.http;
2 
3 private:
4 import std.algorithm;
5 import std.array;
6 import std.conv;
7 import std.datetime;
8 import std.exception;
9 import std.format;
10 import std.stdio;
11 import std.range;
12 import std.socket;
13 import std.string;
14 import std.traits;
15 import std.typecons;
16 import std.experimental.logger;
17 import core.thread;
18 import core.stdc.errno;
19 
20 import requests.streams;
21 import requests.uri;
22 import requests.utils;
23 import requests.base;
24 
25 static this() {
26     globalLogLevel(LogLevel.error);
27 }
28 
29 public alias Cookie     = Tuple!(string, "path", string, "domain", string, "attr", string, "value");
30 public alias QueryParam = Tuple!(string, "key", string, "value");
31 
32 static immutable ushort[] redirectCodes = [301, 302, 303];
33 static immutable uint     defaultBufferSize = 12*1024;
34 
35 static string urlEncoded(string p) pure @safe {
36     immutable string[dchar] translationTable = [
37         ' ':  "%20", '!': "%21", '*': "%2A", '\'': "%27", '(': "%28", ')': "%29",
38         ';':  "%3B", ':': "%3A", '@': "%40", '&':  "%26", '=': "%3D", '+': "%2B",
39         '$':  "%24", ',': "%2C", '/': "%2F", '?':  "%3F", '#': "%23", '[': "%5B",
40         ']':  "%5D", '%': "%25",
41     ];
42     return p.translate(translationTable);
43 }
44 package unittest {
45     assert(urlEncoded(`abc !#$&'()*+,/:;=?@[]`) == "abc%20%21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D");
46 }
47 
48 public class TimeoutException: Exception {
49     this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure {
50         super(msg, file, line);
51     }
52 }
53 
54 public interface Auth {
55     string[string] authHeaders(string domain);
56 }
57 /**
58  * Basic authentication.
59  * Adds $(B Authorization: Basic) header to request.
60  */
61 public class BasicAuthentication: Auth {
62     private {
63         string   _username, _password;
64         string[] _domains;
65     }
66     /// Constructor.
67     /// Params:
68     /// username = username
69     /// password = password
70     /// domains = not used now
71     /// 
72     this(string username, string password, string[] domains = []) {
73         _username = username;
74         _password = password;
75         _domains = domains;
76     }
77     override string[string] authHeaders(string domain) {
78         import std.base64;
79         string[string] auth;
80         auth["Authorization"] = "Basic " ~ to!string(Base64.encode(cast(ubyte[])"%s:%s".format(_username, _password)));
81         return auth;
82     }
83 }
84 ///
85 ///
86 ///
87 public auto queryParams(T...)(T params) pure nothrow @safe
88 {
89     static assert (T.length % 2 == 0, "wrong args count");
90     
91     QueryParam[] output;
92     output.reserve = T.length / 2;
93     
94     void queryParamsHelper(T...)(T params, ref QueryParam[] output)
95     {
96         static if (T.length > 0)
97         {
98             output ~= QueryParam(params[0].to!string, params[1].to!string);
99             queryParamsHelper(params[2..$], output);
100         }
101     }
102 
103     queryParamsHelper(params, output);
104     return output;
105 }
106 
107 ///
108 /// Response - result of request execution.
109 ///
110 /// Response.code - response HTTP code.
111 /// Response.status_line - received HTTP status line.
112 /// Response.responseHeaders - received headers.
113 /// Response.responseBody - container for received body
114 /// Response.history - for redirected responses contain all history
115 /// 
116 public class HTTPResponse : Response {
117     private {
118         string         _status_line;
119 
120         HTTPResponse[] _history; // redirects history
121         SysTime        _startedAt, _connectedAt, _requestSentAt, _finishedAt;
122 
123         mixin(Setter!string("status_line"));
124     }
125 
126     ~this() {
127         _responseHeaders = null;
128         _history.length = 0;
129     }
130 
131     mixin(Getter!string("status_line"));
132     @property final string[string] responseHeaders() @safe @nogc nothrow {
133         return _responseHeaders;
134     }
135     @property final HTTPResponse[] history() @safe @nogc nothrow {
136         return _history;
137     }
138 
139     @property auto getStats() const pure @safe {
140         alias statTuple = Tuple!(Duration, "connectTime",
141                                  Duration, "sendTime",
142                                  Duration, "recvTime");
143         statTuple stat;
144         stat.connectTime = _connectedAt - _startedAt;
145         stat.sendTime = _requestSentAt - _connectedAt;
146         stat.recvTime = _finishedAt - _requestSentAt;
147         return stat;
148     }
149 }
150 /**
151  * Struct to send multiple files in POST request.
152  */
153 public struct PostFile {
154     /// Path to the file to send.
155     string fileName;
156     /// Name of the field (if empty - send file base name)
157     string fieldName;
158     /// contentType of the file if not empty
159     string contentType;
160 }
161 ///
162 /// This is File-like interface for sending data to multipart fotms
163 /// 
164 public interface FiniteReadable {
165     /// size of the content
166     abstract ulong  getSize();
167     /// file-like read()
168     abstract ubyte[] read();
169 }
170 ///
171 /// Helper to create form elements from File.
172 /// Params:
173 /// name = name of the field in form
174 /// f = opened std.stio.File to send to server
175 /// parameters = optional parameters (most important are "filename" and "Content-Type")
176 /// 
177 public auto formData(string name, File f, string[string] parameters = null) {
178     return MultipartForm.FormData(name, new FormDataFile(f), parameters);
179 }
180 ///
181 /// Helper to create form elements from ubyte[].
182 /// Params:
183 /// name = name of the field in form
184 /// b = data to send to server
185 /// parameters = optional parameters (can be "filename" and "Content-Type")
186 /// 
187 public auto formData(string name, ubyte[] b, string[string] parameters = null) {
188     return MultipartForm.FormData(name, new FormDataBytes(b), parameters);
189 }
190 public auto formData(string name, string b, string[string] parameters = null) {
191     return MultipartForm.FormData(name, new FormDataBytes(b.dup.representation), parameters);
192 }
193 public class FormDataBytes : FiniteReadable {
194     private {
195         ulong   _size;
196         ubyte[] _data;
197         size_t  _offset;
198         bool    _exhausted;
199     }
200     this(ubyte[] data) {
201         _data = data;
202         _size = data.length;
203     }
204     final override ulong getSize() {
205         return _size;
206     }
207     final override ubyte[] read() {
208         enforce( !_exhausted, "You can't read froum exhausted source" );
209         size_t toRead = min(defaultBufferSize, _size - _offset);
210         auto result = _data[_offset.._offset+toRead];
211         _offset += toRead;
212         if ( toRead == 0 ) {
213             _exhausted = true;
214         }
215         return result;
216     }
217 }
218 public class FormDataFile : FiniteReadable {
219     import  std.file;
220     private {
221         File    _fileHandle;
222         ulong   _fileSize;
223         size_t  _processed;
224         bool    _exhausted;
225     }
226     this(File file) {
227         import std.file;
228         _fileHandle = file;
229         _fileSize = std.file.getSize(file.name);
230     }
231     final override ulong getSize() pure nothrow @safe {
232         return _fileSize;
233     }
234     final override ubyte[] read() {
235         enforce( !_exhausted, "You can't read froum exhausted source" );
236         auto b = new ubyte[defaultBufferSize];
237         auto r = _fileHandle.rawRead(b);
238         auto toRead = min(r.length, _fileSize - _processed);
239         if ( toRead == 0 ) {
240             _exhausted = true;
241         }
242         _processed += toRead;
243         return r[0..toRead];
244     }
245 }
246 ///
247 /// This struct used to bulld POST's to forms.
248 /// Each part have name and data. data is something that can be read-ed and have size.
249 /// For example this can be string-like object (wrapped for reading) or opened File.
250 /// 
251 public struct MultipartForm {
252     package struct FormData {
253         FiniteReadable  input;
254         string          name;
255         string[string]  parameters;
256         this(string name, FiniteReadable i, string[string] parameters = null) {
257             this.input = i;
258             this.name = name;
259             this.parameters = parameters;
260         }
261     }
262 
263     private FormData[] _sources;
264     auto add(FormData d) {
265         _sources ~= d;
266         return this;
267     }
268     auto add(string name, FiniteReadable i, string[string]parameters = null) {
269         _sources ~= FormData(name, i, parameters);
270         return this;
271     }
272 }
273 ///
274 package unittest {
275     /// This is example on usage files with MultipartForm data.
276     /// For this example we have to create files which will be sent.
277     import std.file;
278     import std.path;
279     globalLogLevel(LogLevel.info);
280     info("Check POST files");
281     /// preapare files
282     auto tmpd = tempDir();
283     auto tmpfname1 = tmpd ~ dirSeparator ~ "request_test1.txt";
284     auto f = File(tmpfname1, "wb");
285     f.rawWrite("file1 content\n");
286     f.close();
287     auto tmpfname2 = tmpd ~ dirSeparator ~ "request_test2.txt";
288     f = File(tmpfname2, "wb");
289     f.rawWrite("file2 content\n");
290     f.close();
291     ///
292     /// Ok, files ready.
293     /// Now we will prepare Form data
294     /// 
295     File f1 = File(tmpfname1, "rb");
296     File f2 = File(tmpfname2, "rb");
297     scope(exit) {
298         f1.close();
299         f2.close();
300     }
301     ///
302     /// for each part we have to set field name, source (ubyte array or opened file) and optional filename and content-type
303     /// 
304     MultipartForm form = MultipartForm().
305         add(formData("Field1", cast(ubyte[])"form field from memory")).
306         add(formData("Field2", cast(ubyte[])"file field from memory", ["filename":"data2"])).
307         add(formData("File1", f1, ["filename":"file1", "Content-Type": "application/octet-stream"])).
308         add(formData("File2", f2, ["filename":"file2", "Content-Type": "application/octet-stream"]));
309     /// everything ready, send request
310     auto rq = HTTPRequest();
311     auto rs = rq.post("http://httpbin.org/post", form);
312 }
313 
314 ///
315 /// Request.
316 /// Configurable parameters:
317 /// $(B method) - string, method to use (GET, POST, ...)
318 /// $(B headers) - string[string], add any additional headers you'd like to send.
319 /// $(B authenticator) - class Auth, class to send auth headers.
320 /// $(B keepAlive) - bool, set true for keepAlive requests. default true.
321 /// $(B maxRedirects) - uint, maximum number of redirects. default 10.
322 /// $(B maxHeadersLength) - size_t, maximum length of server response headers. default = 32KB.
323 /// $(B maxContentLength) - size_t, maximun content length. delault - 0 = unlimited.
324 /// $(B bufferSize) - size_t, send and receive buffer size. default = 16KB.
325 /// $(B verbosity) - uint, level of verbosity(0 - nothing, 1 - headers, 2 - headers and body progress). default = 0.
326 /// $(B proxy) - string, set proxy url if needed. default - null.
327 /// $(B cookie) - Tuple Cookie, Read/Write cookie You can get cookie setted by server, or set cookies before doing request.
328 /// $(B timeout) - Duration, Set timeout value for connect/receive/send.
329 /// 
330 public struct HTTPRequest {
331     private {
332         enum           _preHeaders = [
333                             "Accept-Encoding": "gzip, deflate",
334                             "User-Agent":      "dlang-requests"
335                         ];
336         string         _method = "GET";
337         URI            _uri;
338         string[string] _headers;
339         string[]       _filteredHeaders;
340         Auth           _authenticator;
341         bool           _keepAlive = true;
342         uint           _maxRedirects = 10;
343         size_t         _maxHeadersLength = 32 * 1024; // 32 KB
344         size_t         _maxContentLength; // 0 - Unlimited
345         string         _proxy;
346         uint           _verbosity = 0;  // 0 - no output, 1 - headers, 2 - headers+body info
347         Duration       _timeout = 30.seconds;
348         size_t         _bufferSize = defaultBufferSize; // 16k
349         bool           _useStreaming; // return iterator instead of completed request
350 
351         SocketStream   _stream;
352         HTTPResponse[] _history; // redirects history
353         DataPipe!ubyte _bodyDecoder;
354         DecodeChunked  _unChunker;
355         long           _contentLength;
356         long           _contentReceived;
357         Cookie[]       _cookie;
358     }
359     package HTTPResponse   _response;
360 
361     mixin(Getter_Setter!string   ("method"));
362     mixin(Getter_Setter!bool     ("keepAlive"));
363     mixin(Getter_Setter!size_t   ("maxContentLength"));
364     mixin(Getter_Setter!size_t   ("maxHeadersLength"));
365     mixin(Getter_Setter!size_t   ("bufferSize"));
366     mixin(Getter_Setter!uint     ("maxRedirects"));
367     mixin(Getter_Setter!uint     ("verbosity"));
368     mixin(Getter_Setter!string   ("proxy"));
369     mixin(Getter_Setter!Duration ("timeout"));
370     mixin(Setter!Auth            ("authenticator"));
371     mixin(Getter_Setter!bool     ("useStreaming"));
372     mixin(Getter!long            ("contentLength"));
373     mixin(Getter!long            ("contentReceived"));
374 
375     @property final void cookie(Cookie[] s) pure @safe @nogc nothrow {
376         _cookie = s;
377     }
378     
379     @property final Cookie[] cookie() pure @safe @nogc nothrow {
380         return _cookie;
381     }    
382 
383     this(string uri) {
384         _uri = URI(uri);
385     }
386    ~this() {
387         if ( _stream && _stream.isConnected) {
388             _stream.close();
389         }
390         _stream = null;
391         _headers = null;
392         _authenticator = null;
393         _history = null;
394         _bodyDecoder = null;
395         _unChunker = null;
396     }
397 
398     @property void uri(in URI newURI) {
399         handleURLChange(_uri, newURI);
400         _uri = newURI;
401     }
402     /// Add headers to request
403     /// Params:
404     /// headers = headers to send.
405     void addHeaders(in string[string] headers) {
406         foreach(pair; headers.byKeyValue) {
407             _headers[pair.key] = pair.value;
408         }
409     }
410     /// Remove headers from request
411     /// Params:
412     /// headers = headers to remove.
413     void removeHeaders(in string[] headers) pure {
414         _filteredHeaders ~= headers;
415     }
416     ///
417     /// compose headers to send
418     /// 
419     private string[string] requestHeaders() {
420         string[string] generatedHeaders = _preHeaders;
421 
422         if ( _authenticator ) {
423             _authenticator.
424                 authHeaders(_uri.host).
425                 byKeyValue.
426                 each!(pair => generatedHeaders[pair.key] = pair.value);
427         }
428 
429         generatedHeaders["Connection"] = _keepAlive?"Keep-Alive":"Close";
430         generatedHeaders["Host"] = _uri.host;
431 
432         if ( _uri.scheme !in standard_ports || _uri.port != standard_ports[_uri.scheme] ) {
433             generatedHeaders["Host"] ~= ":%d".format(_uri.port);
434         }
435 
436         _headers.byKey.each!(h => generatedHeaders[h] = _headers[h]);
437 
438         if ( _cookie.length ) {
439             auto cs = _cookie.
440                 filter!(c => _uri.path.pathMatches(c.path) && _uri.host.domainMatches(c.domain)).
441                 map!(c => "%s=%s".format(c.attr, c.value)).
442                 joiner(";");
443             generatedHeaders["Cookie"] = "%s".format(cs);
444         }
445 
446         _filteredHeaders.each!(h => generatedHeaders.remove(h));
447 
448         return generatedHeaders;
449     }
450     ///
451     /// Build request string.
452     /// Handle proxy and query parameters.
453     /// 
454     private @property string requestString(QueryParam[] params = null) {
455         if ( _proxy ) {
456             return "%s %s HTTP/1.1\r\n".format(_method, _uri.uri);
457         }
458         auto query = _uri.query.dup;
459         if ( params ) {
460             query ~= params2query(params);
461             if ( query[0] != '?' ) {
462                 query = "?" ~ query;
463             }
464         }
465         return "%s %s%s HTTP/1.1\r\n".format(_method, _uri.path, query);
466     }
467     ///
468     /// encode parameters and build query part of the url
469     /// 
470     private static string params2query(in QueryParam[] params) pure @safe {
471         return params.
472                 map!(a => "%s=%s".format(a.key.urlEncoded, a.value.urlEncoded)).
473                 join("&");
474     }
475     //
476     package unittest {
477         assert(params2query(queryParams("a","b", "c", " d "))=="a=b&c=%20d%20");
478     }
479     ///
480     /// Analyze received headers, take appropriate actions:
481     /// check content length, attach unchunk and uncompress
482     /// 
483     private void analyzeHeaders(in string[string] headers) {
484 
485         _contentLength = -1;
486         _unChunker = null;
487         auto contentLength = "content-length" in headers;
488         if ( contentLength ) {
489             try {
490                 _contentLength = to!long(*contentLength);
491                 if ( _maxContentLength && _contentLength > _maxContentLength) {
492                     throw new RequestException("ContentLength > maxContentLength (%d>%d)".
493                                 format(_contentLength, _maxContentLength));
494                 }
495             } catch (ConvException e) {
496                 throw new RequestException("Can't convert Content-Length from %s".format(*contentLength));
497             }
498         }
499         auto transferEncoding = "transfer-encoding" in headers;
500         if ( transferEncoding ) {
501             tracef("transferEncoding: %s", *transferEncoding);
502             if ( *transferEncoding == "chunked") {
503                 _unChunker = new DecodeChunked();
504                 _bodyDecoder.insert(_unChunker);
505             }
506         }
507         auto contentEncoding = "content-encoding" in headers;
508         if ( contentEncoding ) switch (*contentEncoding) {
509             default:
510                 throw new RequestException("Unknown content-encoding " ~ *contentEncoding);
511             case "gzip":
512             case "deflate":
513                 _bodyDecoder.insert(new Decompressor!ubyte);
514         }
515 
516     }
517     ///
518     /// Called when we know that all headers already received in buffer.
519     /// This routine does not interpret headers content (see analyzeHeaders).
520     /// 1. Split headers on lines
521     /// 2. store status line, store response code
522     /// 3. unfold headers if needed
523     /// 4. store headers
524     /// 
525     private void parseResponseHeaders(ref Buffer!ubyte buffer) {
526         string lastHeader;
527 
528         foreach(line; buffer.data!(string).split("\n").map!(l => l.stripRight)) {
529             if ( ! _response.status_line.length ) {
530                 tracef("statusLine: %s", line);
531                 _response.status_line = line;
532                 if ( _verbosity >= 1 ) {
533                     writefln("< %s", line);
534                 }
535                 auto parsed = line.split(" ");
536                 if ( parsed.length >= 3 ) {
537                     _response.code = parsed[1].to!ushort;
538                 }
539                 continue;
540             }
541             if ( line[0] == ' ' || line[0] == '\t' ) {
542                 // unfolding https://tools.ietf.org/html/rfc822#section-3.1
543                 auto stored = lastHeader in _response._responseHeaders;
544                 if ( stored ) {
545                     *stored ~= line;
546                 }
547                 continue;
548             }
549             auto parsed = line.findSplit(":");
550             auto header = parsed[0].toLower;
551             auto value = parsed[2].strip;
552 
553             if ( _verbosity >= 1 ) {
554                 writefln("< %s: %s", header, value);
555             }
556 
557             lastHeader = header;
558             tracef("Header %s = %s", header, value);
559 
560             if ( header != "set-cookie" ) {
561                 auto stored = _response.responseHeaders.get(header, null);
562                 if ( stored ) {
563                     value = stored ~ ", " ~ value;
564                 }
565                 _response._responseHeaders[header] = value;
566                 continue;
567             }
568            _cookie ~= processCookie(value);
569         }
570     }
571     ///
572     /// Process Set-Cookie header from server response
573     /// 
574     private Cookie[] processCookie(string value ) pure {
575         // cookie processing
576         //
577         // as we can't join several set-cookie lines in single line
578         // < Set-Cookie: idx=7f2800f63c112a65ef5082957bcca24b; expires=Mon, 29-May-2017 00:31:25 GMT; path=/; domain=example.com
579         // < Set-Cookie: idx=7f2800f63c112a65ef5082957bcca24b; expires=Mon, 29-May-2017 00:31:25 GMT; path=/; domain=example.com, cs=ip764-RgKqc-HvSkxRxdQQAKW8LA; path=/; domain=.example.com; HttpOnly
580         //
581         Cookie[] res;
582         string[string] kv;
583         auto fields = value.split(";").map!strip;
584         while(!fields.empty) {
585             auto s = fields.front.findSplit("=");
586             fields.popFront;
587             if ( s[1] != "=" ) {
588                 continue;
589             }
590             auto k = s[0];
591             auto v = s[2];
592             switch(k.toLower()) {
593                 case "domain":
594                     k = "domain";
595                     break;
596                 case "path":
597                     k = "path";
598                     break;
599                 case "expires":
600                     continue;
601                 default:
602                     break;
603             }
604             kv[k] = v;
605         }
606         if ( "domain" !in kv ) {
607             kv["domain"] = _uri.host;
608         }
609         if ( "path" !in kv ) {
610             kv["path"] = _uri.path;
611         }
612         auto domain = kv["domain"]; kv.remove("domain");
613         auto path   = kv["path"];   kv.remove("path");
614         foreach(pair; kv.byKeyValue) {
615             auto _attr = pair.key;
616             auto _value = pair.value;
617             auto cookie = Cookie(path, domain, _attr, _value);
618             res ~= cookie;
619         }
620         return res;
621     }
622     ///
623     /// Do we received \r\n\r\n?
624     /// 
625     private bool headersHaveBeenReceived(in ubyte[] data, ref Buffer!ubyte buffer, out string separator) const @safe {
626         foreach(s; ["\r\n\r\n", "\n\n"]) {
627             if ( data.canFind(s) || buffer.canFind(s) ) {
628                 separator = s;
629                 return true;
630             }
631         }
632         return false;
633     }
634 
635     private bool followRedirectResponse() {
636         if ( _history.length >= _maxRedirects ) {
637             return false;
638         }
639         auto location = "location" in _response.responseHeaders;
640         if ( !location ) {
641             return false;
642         }
643         _history ~= _response;
644         auto connection = "connection" in _response._responseHeaders;
645         if ( !connection || *connection == "close" ) {
646             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
647             _stream.close();
648         }
649         URI oldURI = _uri;
650         URI newURI = oldURI;
651         try {
652             newURI = URI(*location);
653         } catch (UriException e) {
654             trace("Can't parse Location:, try relative uri");
655             newURI.path = *location;
656             newURI.uri = newURI.recalc_uri;
657         }
658         handleURLChange(oldURI, newURI);
659             oldURI = _response.uri;
660         _uri = newURI;
661         _response = new HTTPResponse;
662         _response.uri = oldURI;
663         _response.finalURI = newURI;
664         return true;
665     }
666     ///
667     /// If uri changed so that we have to change host or port, then we have to close socket stream
668     /// 
669     private void handleURLChange(in URI from, in URI to) {
670         if ( _stream !is null && _stream.isConnected && 
671             ( from.scheme != to.scheme || from.host != to.host || from.port != to.port) ) {
672             debug tracef("Have to reopen stream, because of URI change");
673             _stream.close();
674         }
675     }
676     ///
677     /// if we have new uri, then we need to check if we have to reopen existent connection
678     /// 
679     private void checkURL(string url, string file=__FILE__, size_t line=__LINE__) {
680         if (url is null && _uri.uri == "" ) {
681             throw new RequestException("No url configured", file, line);
682         }
683         
684         if ( url !is null ) {
685             URI newURI = URI(url);
686             handleURLChange(_uri, newURI);
687             _uri = newURI;
688         }
689     }
690     ///
691     /// Setup connection. Handle proxy and https case
692     /// 
693     private void setupConnection() {
694         if ( !_stream || !_stream.isConnected ) {
695             tracef("Set up new connection");
696             URI   uri;
697             if ( _proxy ) {
698                 // use proxy uri to connect
699                 uri.uri_parse(_proxy);
700             } else {
701                 // use original uri
702                 uri = _uri;
703             }
704             final switch (uri.scheme) {
705                 case "http":
706                     _stream = new TCPSocketStream().connect(uri.host, uri.port, _timeout);
707                     break;
708                 case "https":
709                     _stream = new SSLSocketStream().connect(uri.host, uri.port, _timeout);
710                     break;
711             }
712         } else {
713             tracef("Use old connection");
714         }
715     }
716     ///
717     /// Request sent, now receive response.
718     /// Find headers, split on headers and body, continue to receive body
719     /// 
720     private void receiveResponse() {
721 
722         _stream.so.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, timeout);
723         scope(exit) {
724             _stream.so.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, 0.seconds);
725         }
726 
727         _bodyDecoder = new DataPipe!ubyte();
728         scope(exit) {
729             if ( !_useStreaming ) {
730                 _bodyDecoder = null;
731                 _unChunker = null;
732             }
733         }
734 
735         auto buffer = Buffer!ubyte();
736         Buffer!ubyte ResponseHeaders, partialBody;
737         ptrdiff_t read;
738         string separator;
739         
740         while(true) {
741 
742             auto b = new ubyte[_bufferSize];
743             read = _stream.receive(b);
744 
745             debug tracef("read: %d", read);
746             if ( read < 0 ) {
747                 version(Windows) {
748                     if ( errno == 0 ) {
749                         throw new TimeoutException("Timeout receiving headers");
750                     }
751                 }
752                 version(Posix) {
753                     if ( errno == EINTR ) {
754                         continue;
755                     }
756                     if ( errno == EAGAIN ) {
757                         throw new TimeoutException("Timeout receiving headers");
758                     }
759                     throw new ErrnoException("receiving Headers");
760                 }
761             }
762             if ( read == 0 ) {
763                 break;
764             }
765             auto data = b[0..read];
766             buffer.putNoCopy(data);
767 
768             if ( buffer.length > maxHeadersLength ) {
769                 throw new RequestException("Headers length > maxHeadersLength (%d > %d)".format(buffer.length, maxHeadersLength));
770             }
771             if ( headersHaveBeenReceived(data, buffer, separator) ) {
772                 auto s = buffer.data!(ubyte[]).findSplit(separator);
773                 ResponseHeaders = Buffer!ubyte(s[0]);
774                 partialBody = Buffer!ubyte(s[2]);
775                 _contentReceived += partialBody.length;
776                 parseResponseHeaders(ResponseHeaders);
777                 break;
778             }
779         }
780         
781         analyzeHeaders(_response._responseHeaders);
782 
783         _bodyDecoder.putNoCopy(partialBody.data);
784 
785         if ( _verbosity >= 2 ) {
786             writefln("< %d bytes of body received", partialBody.length);
787         }
788 
789         if ( _method == "HEAD" ) {
790             // HEAD response have ContentLength, but have no body
791             return;
792         }
793 
794         while( true ) {
795             if ( _contentLength >= 0 && _contentReceived >= _contentLength ) {
796                 debug trace("Body received.");
797                 break;
798             }
799             if ( _unChunker && _unChunker.done ) {
800                 break;
801             }
802             if ( _useStreaming && _response._responseBody.length && !redirectCodes.canFind(_response.code) ) {
803                 trace("streaming requested");
804                 _response.receiveAsRange.activated = true;
805                 _response.receiveAsRange.data = _response._responseBody.data;
806                 _response.receiveAsRange.read = delegate ubyte[] () {
807                     while(true) {
808                         // check if we received everything we need
809                         if ( ( _unChunker && _unChunker.done )
810                             || !_stream.isOpen() 
811                             || (_contentLength > 0 && _contentReceived >= _contentLength) ) 
812                         {
813                             trace("streaming_in receive completed");
814                             _bodyDecoder.flush();
815                             return _bodyDecoder.get();
816                         }
817                         // have to continue
818                         auto b = new ubyte[_bufferSize];
819                         read = _stream.receive(b);
820                         debug tracef("streaming_in received %d bytes", read);
821                         if ( read < 0 ) {
822                             version(Posix) {
823                                 if ( errno == EINTR ) {
824                                     continue;
825                                 }
826                             }
827                             throw new RequestException("streaming_in error reading from socket");
828                         }
829 
830                         if ( read == 0 ) {
831                             debug tracef("streaming_in: server closed connection");
832                             _bodyDecoder.flush();
833                             return _bodyDecoder.get();
834                         }
835 
836                         _contentReceived += read;
837                         _bodyDecoder.putNoCopy(b[0..read]);
838                         auto res = _bodyDecoder.getNoCopy();
839                         if ( res.length == 0 ) {
840                             // there were nothing to produce (beginning of the chunk or no decompressed data)
841                             continue;
842                         }
843                         //                        
844                         // I'd like to "return _bodyDecoder.getNoCopy().join;" but if is slower
845                         //
846                         if (res.length == 1) {
847                             return res[0];
848                         }
849                         auto total = res.map!(b=>b.length).sum;
850                         // create buffer for joined bytes
851                         ubyte[] joined = new ubyte[total];
852                         size_t p;
853                         // memcopy 
854                         foreach(ref _; res) {
855                             joined[p .. p + _.length] = _;
856                             p += _.length;
857                         }
858                         return joined;
859                     }
860                     assert(0);
861                 };
862                 // we prepared for streaming
863                 return;
864             }
865 
866             auto b = new ubyte[_bufferSize];
867             read = _stream.receive(b);
868 
869             if ( read < 0 ) {
870                 version(Posix) {
871                     if ( errno == EINTR ) {
872                         continue;
873                     }
874                 }
875                 if ( errno == EAGAIN ) {
876                     throw new TimeoutException("Timeout receiving body");
877                 }
878                 throw new ErrnoException("receiving body");
879             }
880             if ( read == 0 ) {
881                 debug trace("read done");
882                 break;
883             }
884             if ( _verbosity >= 2 ) {
885                 writefln("< %d bytes of body received", read);
886             }
887 
888             debug tracef("read: %d", read);
889             _contentReceived += read;
890             if ( _maxContentLength && _contentReceived > _maxContentLength ) {
891                 throw new RequestException("ContentLength > maxContentLength (%d>%d)".
892                     format(_contentLength, _maxContentLength));
893             }
894 
895             _bodyDecoder.putNoCopy(b[0..read]); // send buffer to all decoders
896 
897             _bodyDecoder.getNoCopy.             // fetch result and place to body
898                 each!(b => _response._responseBody.putNoCopy(b));
899 
900             debug tracef("receivedTotal: %d, contentLength: %d, bodyLength: %d", _contentReceived, _contentLength, _response._responseBody.length);
901 
902         }
903         _bodyDecoder.flush();
904         _response._responseBody.putNoCopy(_bodyDecoder.get());
905     }
906     private bool serverClosedKeepAliveConnection() pure @safe nothrow {
907         return _response._responseHeaders.length == 0 && _keepAlive;
908     }
909     private bool isIdempotent(in string method) pure @safe nothrow {
910         return ["GET", "HEAD"].canFind(method);
911     }
912     ///
913     /// Send multipart for request.
914     /// You would like to use this method for sending large portions of mixed data or uploading files to forms.
915     /// Content of the posted form consist of sources. Each source have at least name and value (can be string-like object or opened file, see more docs for MultipartForm struct)
916     /// Params:
917     ///     url = url
918     ///     sources = array of sources.
919     HTTPResponse exec(string method="POST")(string url, MultipartForm sources) {
920         import std.uuid;
921         import std.file;
922         //
923         // application/json
924         //
925         bool restartedRequest = false;
926         
927         _method = method;
928         
929         _response = new HTTPResponse;
930         checkURL(url);
931         _response.uri = _uri;
932         _response.finalURI = _uri;
933 
934     connect:
935         _contentReceived = 0;
936         _response._startedAt = Clock.currTime;
937         setupConnection();
938         
939         if ( !_stream.isConnected() ) {
940             return _response;
941         }
942         _response._connectedAt = Clock.currTime;
943         
944         Appender!string req;
945         req.put(requestString());
946         
947         string   boundary = randomUUID().toString;
948         string[] partHeaders;
949         size_t   contentLength;
950         
951         foreach(ref part; sources._sources) {
952             string h = "--" ~ boundary ~ "\r\n";
953             string disposition = "form-data; name=%s".format(part.name);
954             string optionals = part.
955                 parameters.byKeyValue().
956                 filter!(p => p.key!="Content-Type").
957                 map!   (p => "%s=%s".format(p.key, p.value)).
958                 join("; ");
959 
960             h ~= `Content-Disposition: ` ~ [disposition, optionals].join("; ") ~ "\r\n";
961 
962             auto contentType = "Content-Type" in part.parameters;
963             if ( contentType ) {
964                 h ~= "Content-Type: " ~ *contentType ~ "\r\n";
965             }
966 
967             h ~= "\r\n";
968             partHeaders ~= h;
969             contentLength += h.length + part.input.getSize() + "\r\n".length;
970         }
971         contentLength += "--".length + boundary.length + "--\r\n".length;
972         
973         auto h = requestHeaders();
974         h["Content-Type"] = "multipart/form-data; boundary=" ~ boundary;
975         h["Content-Length"] = to!string(contentLength);
976         h.byKeyValue.
977             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
978                 each!(h => req.put(h));
979         req.put("\r\n");
980         
981         trace(req.data);
982         if ( _verbosity >= 1 ) {
983             req.data.splitLines.each!(a => writeln("> " ~ a));
984         }
985         
986         auto rc = _stream.send(req.data());
987         if ( rc == -1 ) {
988             errorf("Error sending request: ", lastSocketError);
989             return _response;
990         }
991         foreach(ref source; sources._sources) {
992             auto hdr = partHeaders.front;
993             partHeaders.popFront;
994             tracef("sending part headers <%s>", hdr);
995             _stream.send(hdr);
996             while (true) {
997                 auto chunk = source.input.read();
998                 if ( chunk.length <= 0 ) {
999                     break;
1000                 }
1001                 _stream.send(chunk);
1002             }
1003             _stream.send("\r\n");
1004         }
1005         _stream.send("--" ~ boundary ~ "--\r\n");
1006         _response._requestSentAt = Clock.currTime;
1007         
1008         receiveResponse();
1009         
1010         if ( _useStreaming ) {
1011             if ( _response._receiveAsRange.activated ) {
1012                 trace("streaming_in activated");
1013                 return _response;
1014             } else {
1015                 _response._receiveAsRange.data = _response.responseBody.data;
1016             }
1017         }
1018 
1019         if ( serverClosedKeepAliveConnection()
1020             && !restartedRequest
1021             && isIdempotent(_method)
1022             ) {
1023             tracef("Server closed keepalive connection");
1024             _stream.close();
1025             restartedRequest = true;
1026             goto connect;
1027         }
1028         
1029         _response._finishedAt = Clock.currTime;
1030         ///
1031         auto connection = "connection" in _response._responseHeaders;
1032         if ( !connection || *connection == "close" ) {
1033             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
1034             _stream.close();
1035         }
1036         if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) {
1037             if ( _method != "GET" ) {
1038                 return this.get();
1039             }
1040             goto connect;
1041         }
1042         _response._history = _history;
1043         ///
1044         return _response;
1045     }
1046     ///
1047     /// POST/PUT/... data from some string(with Content-Length), or from range of strings/bytes (use Transfer-Encoding: chunked).
1048     /// When rank 1 (flat array) used as content it must have length. In that case "content" will be sent directly to network, and Content-Length headers will be added.
1049     /// If you are goung to send some range and do not know length at the moment when you start to send request, then you can send chunks of chars or ubyte.
1050     /// Try not to send too short chunks as this will put additional load on client and server. Chunks of length 2048 or 4096 are ok.
1051     /// 
1052     /// Parameters:
1053     ///    url = url
1054     ///    content = string or input range
1055     ///    contentType = content type
1056     ///  Returns:
1057     ///     Response
1058     ///  Examples:
1059     ///  ---------------------------------------------------------------------------------------------------------
1060     ///      rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream");
1061     ///      
1062     ///      auto s = lineSplitter("one,\ntwo,\nthree.");
1063     ///      rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream");
1064     ///      
1065     ///      auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1066     ///      rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream");
1067     ///
1068     ///      auto f = File("tests/test.txt", "rb");
1069     ///      rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream");
1070     ///  --------------------------------------------------------------------------------------------------------
1071     HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="text/html")
1072         if ( (rank!R == 1)
1073             || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 
1074             || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte)))
1075         ) {
1076         //
1077         // application/json
1078         //
1079         bool restartedRequest = false;
1080 
1081         _method = method;
1082         
1083         _response = new HTTPResponse;
1084         checkURL(url);
1085         _response.uri = _uri;
1086         _response.finalURI = _uri;
1087 
1088     connect:
1089         _contentReceived = 0;
1090         _response._startedAt = Clock.currTime;
1091         setupConnection();
1092         
1093         if ( !_stream.isConnected() ) {
1094             return _response;
1095         }
1096         _response._connectedAt = Clock.currTime;
1097 
1098         Appender!string req;
1099         req.put(requestString());
1100 
1101         auto h = requestHeaders;
1102         h["Content-Type"] = contentType;
1103         static if ( rank!R == 1 ) {
1104             h["Content-Length"] = to!string(content.length);
1105         } else {
1106             h["Transfer-Encoding"] = "chunked";
1107         }
1108         h.byKeyValue.
1109             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
1110             each!(h => req.put(h));
1111         req.put("\r\n");
1112 
1113         debug trace(req.data);
1114         if ( _verbosity >= 1 ) {
1115             req.data.splitLines.each!(a => writeln("> " ~ a));
1116         }
1117 
1118         auto rc = _stream.send(req.data());
1119         if ( rc == -1 ) {
1120             errorf("Error sending request: ", lastSocketError);
1121             return _response;
1122         }
1123 
1124         static if ( rank!R == 1) {
1125             _stream.send(content);
1126         } else {
1127             while ( !content.empty ) {
1128                 auto chunk = content.front;
1129                 auto chunkHeader = "%x\r\n".format(chunk.length);
1130                 debug tracef("sending %s%s", chunkHeader, chunk);
1131                 _stream.send(chunkHeader);
1132                 _stream.send(chunk);
1133                 _stream.send("\r\n");
1134                 content.popFront;
1135             }
1136             tracef("sent");
1137             _stream.send("0\r\n\r\n");
1138         }
1139         _response._requestSentAt = Clock.currTime;
1140 
1141         receiveResponse();
1142 
1143         if ( _useStreaming ) {
1144             if ( _response._receiveAsRange.activated ) {
1145                 debug trace("streaming_in activated");
1146                 return _response;
1147             } else {
1148                 _response._receiveAsRange.data = _response.responseBody.data;
1149             }
1150         }
1151 
1152         if ( serverClosedKeepAliveConnection()
1153             && !restartedRequest
1154             && isIdempotent(_method)
1155         ) {
1156             debug tracef("Server closed keepalive connection");
1157             _stream.close();
1158             restartedRequest = true;
1159             goto connect;
1160         }
1161 
1162         _response._finishedAt = Clock.currTime;
1163 
1164         ///
1165         auto connection = "connection" in _response._responseHeaders;
1166         if ( !connection || *connection == "close" ) {
1167             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
1168             _stream.close();
1169         }
1170         if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) {
1171             if ( _method != "GET" ) {
1172                 return this.get();
1173             }
1174             goto connect;
1175         }
1176         ///
1177         _response._history = _history;
1178         return _response;
1179     }
1180     ///
1181     /// Send request with pameters.
1182     /// If used for POST or PUT requests then application/x-www-form-urlencoded used.
1183     /// Request parameters will be encoded into request string or placed in request body for POST/PUT
1184     /// requests.
1185     /// Parameters:
1186     ///     url = url
1187     ///     params = request parameters
1188     ///  Returns:
1189     ///     Response
1190     ///  Examples:
1191     ///  ---------------------------------------------------------------------------------
1192     ///     rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]);
1193     ///  ---------------------------------------------------------------------------------
1194     ///     
1195     HTTPResponse exec(string method="GET")(string url = null, QueryParam[] params = null) {
1196 
1197         _method = method;
1198         _response = new HTTPResponse;
1199         _history.length = 0;
1200         bool restartedRequest = false; // True if this is restarted keepAlive request
1201         string encoded;
1202 
1203         checkURL(url);
1204         _response.uri = _uri;
1205         _response.finalURI = _uri;
1206 
1207     connect:
1208         _contentReceived = 0;
1209         _response._startedAt = Clock.currTime;
1210         setupConnection();
1211 
1212         if ( !_stream.isConnected() ) {
1213             return _response;
1214         }
1215         _response._connectedAt = Clock.currTime;
1216 
1217         auto h = requestHeaders();
1218 
1219         Appender!string req;
1220 
1221         if ( ["POST", "PUT"].canFind(_method) && params ) {
1222             encoded = params2query(params);
1223             h["Content-Type"] = "application/x-www-form-urlencoded";
1224             h["Content-Length"] = to!string(encoded.length);
1225             req.put(requestString());
1226         } else {
1227             req.put(requestString(params));
1228         }
1229 
1230         h.byKeyValue.
1231             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
1232             each!(h => req.put(h));
1233         req.put("\r\n");
1234         if ( encoded ) {
1235             req.put(encoded);
1236         }
1237 
1238         trace(req.data);
1239 
1240         if ( _verbosity >= 1 ) {
1241             req.data.splitLines.each!(a => writeln("> " ~ a));
1242         }
1243         auto rc = _stream.send(req.data());
1244         if ( rc == -1 ) {
1245             errorf("Error sending request: ", lastSocketError);
1246             return _response;
1247         }
1248         _response._requestSentAt = Clock.currTime;
1249 
1250         receiveResponse();
1251 
1252         if ( _useStreaming ) {
1253             if ( _response._receiveAsRange.activated ) {
1254                 trace("streaming_in activated");
1255                 return _response;
1256             } else {
1257                 _response._receiveAsRange.data = _response.responseBody.data;
1258             }
1259         }
1260         if ( serverClosedKeepAliveConnection()
1261             && !restartedRequest
1262             && isIdempotent(_method)
1263         ) {
1264             tracef("Server closed keepalive connection");
1265             _stream.close();
1266             restartedRequest = true;
1267             goto connect;
1268         }
1269 
1270         _response._finishedAt = Clock.currTime;
1271 
1272         ///
1273         auto connection = "connection" in _response._responseHeaders;
1274         if ( !connection || *connection == "close" ) {
1275             tracef("Closing connection because of 'Connection: close' or no 'Connection' header");
1276             _stream.close();
1277         }
1278         if ( _verbosity >= 1 ) {
1279             writeln(">> Connect time: ", _response._connectedAt - _response._startedAt);
1280             writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt);
1281             writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt);
1282         }
1283         if ( canFind(redirectCodes, _response.code) && followRedirectResponse() ) {
1284             if ( _method != "GET" ) {
1285                 return this.get();
1286             }
1287             goto connect;
1288         }
1289         ///
1290         _response._history = _history;
1291         return _response;
1292     }
1293 
1294     /// WRAPPERS
1295     ///
1296     /// send file(s) using POST and multipart form.
1297     /// This wrapper will be deprecated, use post with MultipartForm - it is more general and clear.
1298     /// Parameters:
1299     ///     url = url
1300     ///     files = array of PostFile structures
1301     /// Returns:
1302     ///     Response
1303     /// Each PostFile structure contain path to file, and optional field name and content type.
1304     /// If no field name provided, then basename of the file will be used.
1305     /// application/octet-stream is default when no content type provided.
1306     /// Example:
1307     /// ---------------------------------------------------------------
1308     ///    PostFile[] files = [
1309     ///                   {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"}, 
1310     ///                   {fileName:"tests/test.txt"}
1311     ///               ];
1312     ///    rs = rq.exec!"POST"("http://httpbin.org/post", files);
1313     /// ---------------------------------------------------------------
1314     /// 
1315     HTTPResponse exec(string method="POST")(string url, PostFile[] files) if (method=="POST") {
1316         MultipartForm multipart;
1317         File[]        toClose;
1318         foreach(ref f; files) {
1319             File file = File(f.fileName, "rb");
1320             toClose ~= file;
1321             string fileName = f.fileName ? f.fileName : f.fieldName;
1322             string contentType = f.contentType ? f.contentType : "application/octetstream";
1323             multipart.add(f.fieldName, new FormDataFile(file), ["filename":fileName, "Content-Type": contentType]);
1324         }
1325         auto res = exec!"POST"(url, multipart);
1326         toClose.each!"a.close";
1327         return res;
1328     }
1329     ///
1330     /// exec request with parameters when you can use dictionary (when you have no duplicates in parameter names)
1331     /// Consider switch to exec(url, QueryParams) as it more generic and clear.
1332     /// Parameters:
1333     ///     url = url
1334     ///     params = dictionary with field names as keys and field values as values.
1335     /// Returns:
1336     ///     Response
1337     HTTPResponse exec(string method="GET")(string url, string[string] params) {
1338         return exec!method(url, params.byKeyValue.map!(p => QueryParam(p.key, p.value)).array);
1339     }
1340     ///
1341     /// GET request. Simple wrapper over exec!"GET"
1342     /// Params:
1343     /// args = request parameters. see exec docs.
1344     ///
1345     HTTPResponse get(A...)(A args) {
1346         return exec!"GET"(args);
1347     }
1348     ///
1349     /// POST request. Simple wrapper over exec!"POST"
1350     /// Params:
1351     /// uri = endpoint uri
1352     /// args = request parameters. see exec docs.
1353     ///
1354     HTTPResponse post(A...)(string uri, A args) {
1355         return exec!"POST"(uri, args);
1356     }
1357 }
1358 
1359 ///
1360 package unittest {
1361     import std.json;
1362     globalLogLevel(LogLevel.info);
1363     tracef("http tests - start");
1364 
1365     auto rq = HTTPRequest();
1366     auto rs = rq.get("https://httpbin.org/");
1367     assert(rs.code==200);
1368     assert(rs.responseBody.length > 0);
1369     rs = HTTPRequest().get("http://httpbin.org/get", ["c":" d", "a":"b"]);
1370     assert(rs.code == 200);
1371     auto json = parseJSON(rs.responseBody.data).object["args"].object;
1372     assert(json["c"].str == " d");
1373     assert(json["a"].str == "b");
1374 
1375     globalLogLevel(LogLevel.info);
1376     rq = HTTPRequest();
1377     rq.keepAlive = true;
1378     // handmade json
1379     info("Check POST json");
1380     rs = rq.post("http://httpbin.org/post?b=x", `{"a":"☺ ", "c":[1,2,3]}`, "application/json");
1381     assert(rs.code==200);
1382     json = parseJSON(rs.responseBody.data).object["args"].object;
1383     assert(json["b"].str == "x");
1384     json = parseJSON(rs.responseBody.data).object["json"].object;
1385     assert(json["a"].str == "☺ ");
1386     assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]);
1387     {
1388         import std.file;
1389         import std.path;
1390         auto tmpd = tempDir();
1391         auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt";
1392         auto f = File(tmpfname, "wb");
1393         f.rawWrite("abcdefgh\n12345678\n");
1394         f.close();
1395         // files
1396         globalLogLevel(LogLevel.info);
1397         info("Check POST files");
1398         PostFile[] files = [
1399                         {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 
1400                         {fileName: tmpfname}
1401                     ];
1402         rs = rq.post("http://httpbin.org/post", files);
1403         assert(rs.code==200);
1404         info("Check POST chunked from file.byChunk");
1405         f = File(tmpfname, "rb");
1406         rs = rq.post("http://httpbin.org/post", f.byChunk(3), "application/octet-stream");
1407         assert(rs.code==200);
1408         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1409         assert(data=="abcdefgh\n12345678\n");
1410         f.close();
1411     }
1412     {
1413         // string
1414         info("Check POST utf8 string");
1415         rs = rq.post("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream");
1416         assert(rs.code==200);
1417         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1418         assert(data=="привiт, свiт!");
1419     }
1420     // ranges
1421     {
1422         info("Check POST chunked from lineSplitter");
1423         auto s = lineSplitter("one,\ntwo,\nthree.");
1424         rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream");
1425         assert(rs.code==200);
1426         auto data = parseJSON(rs.responseBody.toString).object["data"].str;
1427         assert(data=="one,two,three.");
1428     }
1429     {
1430         info("Check POST chunked from array");
1431         auto s = ["one,", "two,", "three."];
1432         rs = rq.post("http://httpbin.org/post", s, "application/octet-stream");
1433         assert(rs.code==200);
1434         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1435         assert(data=="one,two,three.");
1436     }
1437     {
1438         info("Check POST chunked using std.range.chunks()");
1439         auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1440         rs = rq.post("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream");
1441         assert(rs.code==200);
1442         auto data = parseJSON(rs.responseBody.data).object["data"].str;
1443         assert(data==s);
1444     }
1445     {
1446         info("Check POST from AA");
1447         rs = rq.post("http://httpbin.org/post", ["a":"b", "c":"d"]);
1448         assert(rs.code==200);
1449         auto data = parseJSON(rs.responseBody.data).object["form"].object;
1450         assert(data["a"].str == "b");
1451         assert(data["c"].str == "d");
1452     }
1453     {
1454         info("Check POST from QueryParams");
1455         rs = rq.post("http://httpbin.org/post", queryParams("name[]", "first", "name[]", 2));
1456         assert(rs.code==200);
1457         auto data = parseJSON(rs.responseBody.data).object["form"].object;
1458         assert((data["name[]"].array[0].str == "first"));
1459         assert((data["name[]"].array[1].str == "2"));
1460     }
1461     // associative array
1462     rs = rq.post("http://httpbin.org/post", ["a":"b ", "c":"d"]);
1463     assert(rs.code==200);
1464     auto form = parseJSON(rs.responseBody.data).object["form"].object;
1465     assert(form["a"].str == "b ");
1466     assert(form["c"].str == "d");
1467     info("Check HEAD");
1468     rs = rq.exec!"HEAD"("http://httpbin.org/");
1469     assert(rs.code==200);
1470     info("Check DELETE");
1471     rs = rq.exec!"DELETE"("http://httpbin.org/delete");
1472     assert(rs.code==200);
1473     info("Check PUT");
1474     rs = rq.exec!"PUT"("http://httpbin.org/put",  `{"a":"b", "c":[1,2,3]}`, "application/json");
1475     assert(rs.code==200);
1476     info("Check PATCH");
1477     rs = rq.exec!"PATCH"("http://httpbin.org/patch", "привiт, свiт!", "application/octet-stream");
1478     assert(rs.code==200);
1479 
1480     info("Check compressed content");
1481     globalLogLevel(LogLevel.info);
1482     rq = HTTPRequest();
1483     rq.keepAlive = true;
1484     rs = rq.get("http://httpbin.org/gzip");
1485     assert(rs.code==200);
1486     info("gzip - ok");
1487     rs = rq.get("http://httpbin.org/deflate");
1488     assert(rs.code==200);
1489     info("deflate - ok");
1490 
1491     info("Check redirects");
1492     globalLogLevel(LogLevel.info);
1493     rq = HTTPRequest();
1494     //rq.verbosity = 3;
1495     rq.keepAlive = true;
1496     rs = rq.get("http://httpbin.org/relative-redirect/2");
1497     assert(rs.history.length == 2);
1498     assert(rs.code==200);
1499 //    rq = Request();
1500 //    rq.keepAlive = true;
1501 //    rq.proxy = "http://localhost:8888/";
1502     rs = rq.get("http://httpbin.org/absolute-redirect/2");
1503     assert(rs.history.length == 2);
1504     assert(rs.code==200);
1505 //    rq = Request();
1506     rq.maxRedirects = 2;
1507     rq.keepAlive = false;
1508     rs = rq.get("https://httpbin.org/absolute-redirect/3");
1509     assert(rs.history.length == 2);
1510     assert(rs.code==302);
1511     info("Check utf8 content");
1512     globalLogLevel(LogLevel.info);
1513     rq = HTTPRequest();
1514     rs = rq.get("http://httpbin.org/encoding/utf8");
1515     assert(rs.code==200);
1516 
1517     info("Check cookie");
1518     rs = HTTPRequest().get("http://httpbin.org/cookies/set?A=abcd&b=cdef");
1519     assert(rs.code == 200);
1520     json = parseJSON(rs.responseBody.data).object["cookies"].object;
1521     assert(json["A"].str == "abcd");
1522     assert(json["b"].str == "cdef");
1523     auto cookie = rq.cookie();
1524     foreach(c; rq.cookie) {
1525         final switch(c.attr) {
1526             case "A":
1527                 assert(c.value == "abcd");
1528                 break;
1529             case "b":
1530                 assert(c.value == "cdef");
1531                 break;
1532         }
1533     }
1534 
1535     info("Check chunked content");
1536     globalLogLevel(LogLevel.info);
1537     rq = HTTPRequest();
1538     rq.keepAlive = true;
1539     rs = rq.get("http://httpbin.org/range/1024");
1540     assert(rs.code==200);
1541     assert(rs.responseBody.length==1024);
1542 
1543     info("Check basic auth");
1544     globalLogLevel(LogLevel.info);
1545     rq = HTTPRequest();
1546     rq.authenticator = new BasicAuthentication("user", "passwd");
1547     rs = rq.get("http://httpbin.org/basic-auth/user/passwd");
1548     assert(rs.code==200);
1549  
1550     globalLogLevel(LogLevel.info);
1551     info("Check exception handling, error messages are OK");
1552     rq = HTTPRequest();
1553     rq.timeout = 1.seconds;
1554     assertThrown!TimeoutException(rq.get("http://httpbin.org/delay/3"));
1555 //    assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/"));
1556 //    assertThrown!ConnectError(rq.get("http://1.1.1.1/"));
1557     //assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/"));
1558 
1559     globalLogLevel(LogLevel.info);
1560     info("Check limits");
1561     rq = HTTPRequest();
1562     rq.maxContentLength = 1;
1563     assertThrown!RequestException(rq.get("http://httpbin.org/"));
1564     rq = HTTPRequest();
1565     rq.maxHeadersLength = 1;
1566     assertThrown!RequestException(rq.get("http://httpbin.org/"));
1567     tracef("http tests - ok");
1568 }