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