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