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