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