1 module requests.http;
2 
3 private:
4 import std.algorithm;
5 import std.array;
6 import std.ascii;
7 import std.conv;
8 import std.datetime;
9 import std.exception;
10 import std.format;
11 import std.stdio;
12 import std.range;
13 import std.string;
14 import std.traits;
15 import std.typecons;
16 import std.bitmanip;
17 import std.experimental.logger;
18 import core.thread;
19 
20 import requests.streams;
21 import requests.uri;
22 import requests.utils;
23 import requests.base;
24 import requests.connmanager;
25 
26 static immutable ushort[] redirectCodes = [301, 302, 303, 307, 308];
27 static immutable uint     defaultBufferSize = 12*1024;
28 enum   HTTP11 = 101;
29 enum   HTTP10 = 100;
30 
31 static immutable string[string] proxies;
32 static this() {
33     import std.process;
34     proxies["http"] = environment.get("http_proxy", environment.get("HTTP_PROXY"));
35     proxies["https"] = environment.get("https_proxy", environment.get("HTTPS_PROXY"));
36     proxies["all"] = environment.get("all_proxy", environment.get("ALL_PROXY"));
37     foreach(p; proxies.byKey()) {
38         if (proxies[p] is null) {
39             continue;
40         }
41         URI u = URI(proxies[p]);
42     }
43 }
44 
45 public class MaxRedirectsException: Exception {
46     this(string message, string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow {
47         super(message, file, line, next);
48     }
49 }
50 
51 /**
52  * Basic authentication.
53  * Adds $(B Authorization: Basic) header to request.
54  */
55 public class BasicAuthentication: Auth {
56     private {
57         string   _username, _password;
58         string[] _domains;
59     }
60     /// Constructor.
61     /// Params:
62     /// username = username
63     /// password = password
64     /// domains = not used now
65     ///
66     this(string username, string password, string[] domains = []) {
67         _username = username;
68         _password = password;
69         _domains = domains;
70     }
71     override string[string] authHeaders(string domain) {
72         import std.base64;
73         string[string] auth;
74         auth["Authorization"] = "Basic " ~ to!string(Base64.encode(cast(ubyte[])"%s:%s".format(_username, _password)));
75         return auth;
76     }
77     override string userName() {
78         return _username;
79     }
80     override string password() {
81         return _password;
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 
122         mixin(Setter!string("status_line"));
123 
124         int                 _version;
125     }
126 
127     ~this() {
128         _responseHeaders = null;
129         _history.length = 0;
130     }
131 
132     mixin(Getter!string("status_line"));
133     @property final string[string] responseHeaders() @safe @nogc nothrow {
134         return _responseHeaders;
135     }
136     @property final HTTPResponse[] history() @safe @nogc nothrow {
137         return _history;
138     }
139 
140     @property auto getStats() const pure @safe {
141         alias statTuple = Tuple!(Duration, "connectTime",
142                                  Duration, "sendTime",
143                                  Duration, "recvTime");
144         statTuple stat;
145         stat.connectTime = _connectedAt - _startedAt;
146         stat.sendTime = _requestSentAt - _connectedAt;
147         stat.recvTime = _finishedAt - _requestSentAt;
148         return stat;
149     }
150     private int parse_version(in string v) pure const nothrow @safe {
151         // try to parse HTTP/1.x to version
152         try if ( v.length > 5 ) {
153             return (v[5..$].split(".").map!"to!int(a)".array[0..2].reduce!((a,b) => a*100 + b));
154         } catch (Exception e) {
155         }
156         return 0;
157     }
158     unittest {
159         auto r = new HTTPResponse();
160         assert(r.parse_version("HTTP/1.1") == 101);
161         assert(r.parse_version("HTTP/1.0") == 100);
162         assert(r.parse_version("HTTP/0.9") == 9);
163         assert(r.parse_version("HTTP/xxx") == 0);
164     }
165 }
166 /**
167  * Struct to send multiple files in POST request.
168  */
169 public struct PostFile {
170     /// Path to the file to send.
171     string fileName;
172     /// Name of the field (if empty - send file base name)
173     string fieldName;
174     /// contentType of the file if not empty
175     string contentType;
176 }
177 ///
178 /// This is File-like interface for sending data to multipart fotms
179 ///
180 public interface FiniteReadable {
181     /// size of the content
182     abstract ulong  getSize();
183     /// file-like read()
184     abstract ubyte[] read();
185 }
186 ///
187 /// Helper to create form elements from File.
188 /// Params:
189 /// name = name of the field in form
190 /// f = opened std.stio.File to send to server
191 /// parameters = optional parameters (most important are "filename" and "Content-Type")
192 ///
193 public auto formData(string name, File f, string[string] parameters = null) {
194     return MultipartForm.FormData(name, new FormDataFile(f), parameters);
195 }
196 ///
197 /// Helper to create form elements from ubyte[].
198 /// Params:
199 /// name = name of the field in form
200 /// b = data to send to server
201 /// parameters = optional parameters (can be "filename" and "Content-Type")
202 ///
203 public auto formData(string name, ubyte[] b, string[string] parameters = null) {
204     return MultipartForm.FormData(name, new FormDataBytes(b), parameters);
205 }
206 public auto formData(string name, string b, string[string] parameters = null) {
207     return MultipartForm.FormData(name, new FormDataBytes(b.dup.representation), parameters);
208 }
209 public class FormDataBytes : FiniteReadable {
210     private {
211         ulong   _size;
212         ubyte[] _data;
213         size_t  _offset;
214         bool    _exhausted;
215     }
216     this(ubyte[] data) {
217         _data = data;
218         _size = data.length;
219     }
220     final override ulong getSize() {
221         return _size;
222     }
223     final override ubyte[] read() {
224         enforce( !_exhausted, "You can't read froum exhausted source" );
225         size_t toRead = min(defaultBufferSize, _size - _offset);
226         auto result = _data[_offset.._offset+toRead];
227         _offset += toRead;
228         if ( toRead == 0 ) {
229             _exhausted = true;
230         }
231         return result;
232     }
233 }
234 public class FormDataFile : FiniteReadable {
235     import  std.file;
236     private {
237         File    _fileHandle;
238         ulong   _fileSize;
239         size_t  _processed;
240         bool    _exhausted;
241     }
242     this(File file) {
243         import std.file;
244         _fileHandle = file;
245         _fileSize = std.file.getSize(file.name);
246     }
247     final override ulong getSize() pure nothrow @safe {
248         return _fileSize;
249     }
250     final override ubyte[] read() {
251         enforce( !_exhausted, "You can't read froum exhausted source" );
252         auto b = new ubyte[defaultBufferSize];
253         auto r = _fileHandle.rawRead(b);
254         auto toRead = min(r.length, _fileSize - _processed);
255         if ( toRead == 0 ) {
256             _exhausted = true;
257         }
258         _processed += toRead;
259         return r[0..toRead];
260     }
261 }
262 ///
263 /// This struct used to bulld POST's to forms.
264 /// Each part have name and data. data is something that can be read-ed and have size.
265 /// For example this can be string-like object (wrapped for reading) or opened File.
266 ///
267 public struct MultipartForm {
268     package struct FormData {
269         FiniteReadable  input;
270         string          name;
271         string[string]  parameters;
272         this(string name, FiniteReadable i, string[string] parameters = null) {
273             this.input = i;
274             this.name = name;
275             this.parameters = parameters;
276         }
277     }
278 
279     private FormData[] _sources;
280     auto add(FormData d) {
281         _sources ~= d;
282         return this;
283     }
284     auto add(string name, FiniteReadable i, string[string]parameters = null) {
285         _sources ~= FormData(name, i, parameters);
286         return this;
287     }
288 }
289 ///
290 
291 ///
292 /// Request.
293 /// Configurable parameters:
294 /// $(B method) - string, method to use (GET, POST, ...)
295 /// $(B headers) - string[string], add any additional headers you'd like to send.
296 /// $(B authenticator) - class Auth, class to send auth headers.
297 /// $(B keepAlive) - bool, set true for keepAlive requests. default true.
298 /// $(B maxRedirects) - uint, maximum number of redirects. default 10.
299 /// $(B maxHeadersLength) - size_t, maximum length of server response headers. default = 32KB.
300 /// $(B maxContentLength) - size_t, maximun content length. delault - 0 = unlimited.
301 /// $(B bufferSize) - size_t, send and receive buffer size. default = 16KB.
302 /// $(B verbosity) - uint, level of verbosity(0 - nothing, 1 - headers, 2 - headers and body progress). default = 0.
303 /// $(B proxy) - string, set proxy url if needed. default - null.
304 /// $(B cookie) - Tuple Cookie, Read/Write cookie You can get cookie setted by server, or set cookies before doing request.
305 /// $(B timeout) - Duration, Set timeout value for connect/receive/send.
306 ///
307 public struct HTTPRequest {
308     private {
309         struct _UH {
310             // flags for each important header, added by user using addHeaders
311             mixin(bitfields!(
312                 bool, "Host", 1,
313                 bool, "UserAgent", 1,
314                 bool, "ContentLength", 1,
315                 bool, "Connection", 1,
316                 bool, "AcceptEncoding", 1,
317                 bool, "ContentType", 1,
318                 bool, "Cookie", 1,
319                 uint, "", 1
320             ));
321         }
322         string         _method = "GET";
323         URI            _uri;
324         string[string] _headers;
325         string[]       _filteredHeaders;
326         Auth           _authenticator;
327         bool           _keepAlive = true;
328         uint           _maxRedirects = 10;
329         size_t         _maxHeadersLength = 32 * 1024;   // 32 KB
330         size_t         _maxContentLength;               // 0 - Unlimited
331         string         _proxy;
332         uint           _verbosity = 0;                  // 0 - no output, 1 - headers, 2 - headers+body info
333         Duration       _timeout = 30.seconds;
334         size_t         _bufferSize = defaultBufferSize; // 16k
335         bool           _useStreaming;                   // return iterator instead of completed request
336 
337         string[URI]     _permanent_redirects;            // cache 301 redirects for GET requests
338         HTTPResponse[] _history;                        // redirects history
339         DataPipe!ubyte _bodyDecoder;
340         DecodeChunked  _unChunker;
341         long           _contentLength;
342         long           _contentReceived;
343         Cookie[]       _cookie;
344         SSLOptions     _sslOptions;
345         string         _bind;
346         _UH            _userHeaders;
347         ConnManager    _cm;
348     }
349     package HTTPResponse   _response;
350 
351     mixin(Getter_Setter!string     ("method"));
352     mixin(Getter_Setter!bool       ("keepAlive"));
353     mixin(Getter_Setter!size_t     ("maxContentLength"));
354     mixin(Getter_Setter!size_t     ("maxHeadersLength"));
355     mixin(Getter_Setter!size_t     ("bufferSize"));
356     mixin(Getter_Setter!uint       ("maxRedirects"));
357     mixin(Getter_Setter!uint       ("verbosity"));
358     mixin(Getter!string            ("proxy"));
359     mixin(Getter_Setter!Duration   ("timeout"));
360     mixin(Setter!Auth              ("authenticator"));
361     mixin(Getter_Setter!bool       ("useStreaming"));
362     mixin(Getter!long              ("contentLength"));
363     mixin(Getter!long              ("contentReceived"));
364     mixin(Getter_Setter!SSLOptions ("sslOptions"));
365     mixin(Getter_Setter!string     ("bind"));
366 
367     @property void sslSetVerifyPeer(bool v) pure @safe nothrow @nogc {
368         _sslOptions.setVerifyPeer(v);
369     }
370     @property void sslSetKeyFile(string p, SSLOptions.filetype t = SSLOptions.filetype.pem) pure @safe nothrow @nogc {
371         _sslOptions.setKeyFile(p, t);
372     }
373     @property void sslSetCertFile(string p, SSLOptions.filetype t = SSLOptions.filetype.pem) pure @safe nothrow @nogc {
374         _sslOptions.setCertFile(p, t);
375     }
376     @property void sslSetCaCert(string path) pure @safe nothrow @nogc {
377         _sslOptions.setCaCert(path);
378     }
379     @property final void cookie(Cookie[] s) pure @safe @nogc nothrow {
380         _cookie = s;
381     }
382     @property final void proxy(string v) {
383         if ( v != _proxy ) {
384             _cm.clear();
385         }
386         _proxy = v;
387     }
388     @property final Cookie[] cookie() pure @safe @nogc nothrow {
389         return _cookie;
390     }
391 
392     this(string uri) {
393         _uri = URI(uri);
394         _cm = new ConnManager;
395     }
396     ~this() {
397         _headers = null;
398         _authenticator = null;
399         _history = null;
400         _bodyDecoder = null;
401         _unChunker = null;
402         if ( _cm ) {
403             _cm.clear();
404         }
405     }
406     string toString() const {
407         return "HTTPRequest(%s, %s)".format(_method, _uri.uri());
408     }
409     string format(string fmt) const {
410         import std.array;
411         import std.stdio;
412         auto a = appender!string();
413         auto f = FormatSpec!char(fmt);
414         while (f.writeUpToNextSpec(a)) {
415             switch(f.spec) {
416                 case 'h':
417                     // Remote hostname.
418                     a.put(_uri.host);
419                     break;
420                 case 'm':
421                     // method.
422                     a.put(_method);
423                     break;
424                 case 'p':
425                     // Remote port.
426                     a.put("%d".format(_uri.port));
427                     break;
428                 case 'P':
429                     // Path
430                     a.put(_uri.path);
431                     break;
432                 case 'q':
433                     // query parameters supplied with url.
434                     a.put(_uri.query);
435                     break;
436                 case 'U':
437                     a.put(_uri.uri());
438                     break;
439                 default:
440                     throw new FormatException("Unknown Request format spec " ~ f.spec);
441             }
442         }
443         return a.data();
444     }
445     string select_proxy(string scheme) {
446         if ( _proxy is null && proxies.length == 0 ) {
447             debug(requests) tracef("proxy=null");
448             return null;
449         }
450         if ( _proxy ) {
451             debug(requests) tracef("proxy=%s", _proxy);
452             return _proxy;
453         }
454         auto p = scheme in proxies;
455         if ( p !is null && *p != "") {
456             debug(requests) tracef("proxy=%s", *p);
457             return *p;
458         }
459         p = "all" in proxies;
460         if ( p !is null && *p != "") {
461             debug(requests) tracef("proxy=%s", *p);
462             return *p;
463         }
464         debug(requests) tracef("proxy=null");
465         return null;
466     }
467     void clearHeaders() {
468         _headers = null;
469     }
470     @property void uri(in URI newURI) {
471         //handleURLChange(_uri, newURI);
472         _uri = newURI;
473     }
474     /// Add headers to request
475     /// Params:
476     /// headers = headers to send.
477     void addHeaders(in string[string] headers) {
478         foreach(pair; headers.byKeyValue) {
479             string _h = pair.key;
480             switch(toLower(_h)) {
481             case "host":
482                 _userHeaders.Host = true;
483                 break;
484             case "user-agent":
485                 _userHeaders.UserAgent = true;
486                 break;
487             case "content-length":
488                 _userHeaders.ContentLength = true;
489                 break;
490             case "content-type":
491                 _userHeaders.ContentType = true;
492                 break;
493             case "connection":
494                 _userHeaders.Connection = true;
495                 break;
496             case "cookie":
497                 _userHeaders.Cookie = true;
498                 break;
499             default:
500                 break;
501             }
502             _headers[pair.key] = pair.value;
503         }
504     }
505     private void safeSetHeader(ref string[string] headers, bool userAdded, string h, string v) pure @safe {
506         if ( !userAdded ) {
507             headers[h] = v;
508         }
509     }
510     /// Remove headers from request
511     /// Params:
512     /// headers = headers to remove.
513     void removeHeaders(in string[] headers) pure {
514         _filteredHeaders ~= headers;
515     }
516     ///
517     /// compose headers to send
518     ///
519     private string[string] requestHeaders() {
520 
521         string[string] generatedHeaders;
522 
523         if ( _authenticator ) {
524             _authenticator.
525                 authHeaders(_uri.host).
526                 byKeyValue.
527                 each!(pair => generatedHeaders[pair.key] = pair.value);
528         }
529 
530         _headers.byKey.each!(h => generatedHeaders[h] = _headers[h]);
531 
532         safeSetHeader(generatedHeaders, _userHeaders.AcceptEncoding, "Accept-Encoding", "gzip,deflate");
533         safeSetHeader(generatedHeaders, _userHeaders.UserAgent, "User-Agent", "dlang-requests");
534         safeSetHeader(generatedHeaders, _userHeaders.Connection, "Connection", _keepAlive?"Keep-Alive":"Close");
535 
536         if ( !_userHeaders.Host )
537         {
538             generatedHeaders["Host"] = _uri.host;
539             if ( _uri.scheme !in standard_ports || _uri.port != standard_ports[_uri.scheme] ) {
540                 generatedHeaders["Host"] ~= ":%d".format(_uri.port);
541             }
542         }
543 
544         if ( _cookie.length && !_userHeaders.Cookie ) {
545             auto cs = _cookie.
546                 filter!(c => _uri.path.pathMatches(c.path) && _uri.host.domainMatches(c.domain)).
547                 map!(c => "%s=%s".format(c.attr, c.value)).
548                 joiner(";");
549             if ( ! cs.empty )
550             {
551                 generatedHeaders["Cookie"] = to!string(cs);
552             }
553         }
554 
555         _filteredHeaders.each!(h => generatedHeaders.remove(h));
556 
557         return generatedHeaders;
558     }
559     ///
560     /// Build request string.
561     /// Handle proxy and query parameters.
562     ///
563     private @property string requestString(QueryParam[] params = null) {
564         auto query = _uri.query.dup;
565         if ( params ) {
566             query ~= "&" ~ params2query(params);
567             if ( query[0] != '?' ) {
568                 query = "?" ~ query;
569             }
570         }
571         string actual_proxy = select_proxy(_uri.scheme);
572         if ( actual_proxy && _uri.scheme != "https" ) {
573             return "%s %s%s HTTP/1.1\r\n".format(_method, _uri.uri(No.params), query);
574         }
575         return "%s %s%s HTTP/1.1\r\n".format(_method, _uri.path, query);
576     }
577     ///
578     /// encode parameters and build query part of the url
579     ///
580     private static string params2query(in QueryParam[] params) pure @safe {
581         return params.
582                 map!(a => "%s=%s".format(a.key.urlEncoded, a.value.urlEncoded)).
583                 join("&");
584     }
585     //
586     package unittest {
587         assert(params2query(queryParams("a","b", "c", " d "))=="a=b&c=%20d%20");
588     }
589     ///
590     /// Analyze received headers, take appropriate actions:
591     /// check content length, attach unchunk and uncompress
592     ///
593     private void analyzeHeaders(in string[string] headers) {
594 
595         _contentLength = -1;
596         _unChunker = null;
597         auto contentLength = "content-length" in headers;
598         if ( contentLength ) {
599             try {
600                 _contentLength = to!long(*contentLength);
601                 if ( _maxContentLength && _contentLength > _maxContentLength) {
602                     throw new RequestException("ContentLength > maxContentLength (%d>%d)".
603                                 format(_contentLength, _maxContentLength));
604                 }
605             } catch (ConvException e) {
606                 throw new RequestException("Can't convert Content-Length from %s".format(*contentLength));
607             }
608         }
609         auto transferEncoding = "transfer-encoding" in headers;
610         if ( transferEncoding ) {
611             debug(requests) tracef("transferEncoding: %s", *transferEncoding);
612             if ( (*transferEncoding).toLower == "chunked") {
613                 _unChunker = new DecodeChunked();
614                 _bodyDecoder.insert(_unChunker);
615             }
616         }
617         auto contentEncoding = "content-encoding" in headers;
618         if ( contentEncoding ) switch (*contentEncoding) {
619             default:
620                 throw new RequestException("Unknown content-encoding " ~ *contentEncoding);
621             case "gzip":
622             case "deflate":
623                 _bodyDecoder.insert(new Decompressor!ubyte);
624         }
625 
626     }
627     ///
628     /// Called when we know that all headers already received in buffer.
629     /// This routine does not interpret headers content (see analyzeHeaders).
630     /// 1. Split headers on lines
631     /// 2. store status line, store response code
632     /// 3. unfold headers if needed
633     /// 4. store headers
634     ///
635     private void parseResponseHeaders(in ubyte[] input) {
636         string lastHeader;
637         auto buffer = cast(string)input;
638 
639         foreach(line; buffer.split("\n").map!(l => l.stripRight)) {
640             if ( ! _response.status_line.length ) {
641                 debug (requests) tracef("statusLine: %s", line);
642                 _response.status_line = line;
643                 if ( _verbosity >= 1 ) {
644                     writefln("< %s", line);
645                 }
646                 auto parsed = line.split(" ");
647                 if ( parsed.length >= 2 ) {
648                     _response.code = parsed[1].to!ushort;
649                     _response._version = _response.parse_version(parsed[0]);
650                 }
651                 continue;
652             }
653             if ( line[0] == ' ' || line[0] == '\t' ) {
654                 // unfolding https://tools.ietf.org/html/rfc822#section-3.1
655                 if ( auto stored = lastHeader in _response._responseHeaders) {
656                     *stored ~= line;
657                 }
658                 continue;
659             }
660             auto parsed = line.findSplit(":");
661             auto header = parsed[0].toLower;
662             auto value = parsed[2].strip;
663 
664             if ( _verbosity >= 1 ) {
665                 writefln("< %s: %s", header, value);
666             }
667 
668             lastHeader = header;
669             debug (requests) tracef("Header %s = %s", header, value);
670 
671             if ( header != "set-cookie" ) {
672                 auto stored = _response.responseHeaders.get(header, null);
673                 if ( stored ) {
674                     value = stored ~ "," ~ value;
675                 }
676                 _response._responseHeaders[header] = value;
677                 continue;
678             }
679             _cookie ~= processCookie(value);
680         }
681     }
682 
683     ///
684     /// Process Set-Cookie header from server response
685     ///
686     private Cookie[] processCookie(string value ) pure {
687         // cookie processing
688         //
689         // as we can't join several set-cookie lines in single line
690         // < Set-Cookie: idx=7f2800f63c112a65ef5082957bcca24b; expires=Mon, 29-May-2017 00:31:25 GMT; path=/; domain=example.com
691         // < 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
692         //
693         Cookie[] res;
694         string[string] kv;
695         auto fields = value.split(";").map!strip;
696         while(!fields.empty) {
697             auto s = fields.front.findSplit("=");
698             fields.popFront;
699             if ( s[1] != "=" ) {
700                 continue;
701             }
702             auto k = s[0];
703             auto v = s[2];
704             switch(k.toLower()) {
705                 case "domain":
706                     k = "domain";
707                     break;
708                 case "path":
709                     k = "path";
710                     break;
711                 case "expires":
712                     continue;
713                 default:
714                     break;
715             }
716             kv[k] = v;
717         }
718         if ( "domain" !in kv ) {
719             kv["domain"] = _uri.host;
720         }
721         if ( "path" !in kv ) {
722             kv["path"] = _uri.path;
723         }
724         auto domain = kv["domain"]; kv.remove("domain");
725         auto path   = kv["path"];   kv.remove("path");
726         foreach(pair; kv.byKeyValue) {
727             auto _attr = pair.key;
728             auto _value = pair.value;
729             auto cookie = Cookie(path, domain, _attr, _value);
730             res ~= cookie;
731         }
732         return res;
733     }
734     ///
735     /// Do we received \r\n\r\n?
736     ///
737     private bool headersHaveBeenReceived(in ubyte[] data, ref Buffer!ubyte buffer, out string separator) const @safe {
738         foreach(s; ["\r\n\r\n", "\n\n"]) {
739             if ( data.canFind(s) || buffer.canFind(s) ) {
740                 separator = s;
741                 return true;
742             }
743         }
744         return false;
745     }
746 
747     private bool willFollowRedirect() {
748         if ( !canFind(redirectCodes, _response.code) ) {
749             return false;
750         }
751         if ( !_maxRedirects ) {
752             return false;
753         }
754         if ( "location" !in _response.responseHeaders ) {
755             return false;
756         }
757         return true;
758     }
759     private URI uriFromLocation(const ref URI uri, in string location) {
760         URI newURI = uri;
761         try {
762             newURI = URI(location);
763         } catch (UriException e) {
764             debug(requests) trace("Can't parse Location:, try relative uri");
765             newURI.path = location;
766             newURI.uri = newURI.recalc_uri;
767         }
768         return newURI;
769     }
770     ///
771     /// if we have new uri, then we need to check if we have to reopen existent connection
772     ///
773     private void checkURL(string url, string file=__FILE__, size_t line=__LINE__) {
774         if (url is null && _uri.uri == "" ) {
775             throw new RequestException("No url configured", file, line);
776         }
777 
778         if ( url !is null ) {
779             URI newURI = URI(url);
780             //handleURLChange(_uri, newURI);
781             _uri = newURI;
782         }
783     }
784     ///
785     /// Setup connection. Handle proxy and https case
786     ///
787     /// Place new connection in ConnManager cache
788     ///
789     private NetworkStream setupConnection()
790     do {
791 
792         debug(requests) tracef("Set up new connection");
793         NetworkStream stream;
794 
795         URI   uri; // this URI will be used temporarry if we need proxy
796         string actual_proxy = select_proxy(_uri.scheme);
797         final switch (_uri.scheme) {
798             case "http":
799             if ( actual_proxy ) {
800                 uri.uri_parse(actual_proxy);
801                 uri.idn_encode();
802             } else {
803                 // use original uri
804                 uri = _uri;
805             }
806             stream = new TCPStream();
807             stream.bind(_bind);
808             stream.connect(uri.host, uri.port, _timeout);
809             break;
810             case "https":
811             if ( actual_proxy ) {
812                 uri.uri_parse(actual_proxy);
813                 uri.idn_encode();
814                 stream = new TCPStream();
815                 stream.bind(_bind);
816                 stream.connect(uri.host, uri.port, _timeout);
817                 if ( verbosity>=1 ) {
818                     writeln("> CONNECT %s:%d HTTP/1.1".format(_uri.host, _uri.port));
819                 }
820                 stream.send("CONNECT %s:%d HTTP/1.1\r\n\r\n".format(_uri.host, _uri.port));
821                 while ( stream.isConnected ) {
822                     ubyte[1024] b;
823                     auto read = stream.receive(b);
824                     if ( verbosity>=1) {
825                         writefln("< %s", cast(string)b[0..read]);
826                     }
827                     debug(requests) tracef("read: %d", read);
828                     if ( b[0..read].canFind("\r\n\r\n") || b[0..read].canFind("\n\n") ) {
829                         debug(requests) tracef("proxy connection ready");
830                         // convert connection to ssl
831                         stream = new SSLStream(stream, _sslOptions, _uri.host);
832                         break;
833                     } else {
834                         debug(requests) tracef("still wait for proxy connection");
835                     }
836                 }
837             } else {
838                 uri = _uri;
839                 stream = new SSLStream(_sslOptions);
840                 stream.bind(_bind);
841                 stream.connect(uri.host, uri.port, _timeout);
842                 debug(requests) tracef("ssl connection to origin server ready");
843             }
844             break;
845         }
846 
847         if ( stream ) {
848             auto purged_connection = _cm.put(_uri.scheme, _uri.host, _uri.port, stream);
849             if ( purged_connection ) {
850                 debug(requests) tracef("closing purged connection %s", purged_connection);
851                 purged_connection.close();
852             }
853         }
854 
855         return stream;
856     }
857     ///
858     /// Request sent, now receive response.
859     /// Find headers, split on headers and body, continue to receive body
860     ///
861     private void receiveResponse(NetworkStream _stream) {
862 
863         try {
864             _stream.readTimeout = timeout;
865         } catch (Exception e) {
866             debug(requests) tracef("Failed to set read timeout for stream: %s", e.msg);
867             return;
868         }
869         // Commented this out as at exit we can have alreade closed socket
870         // scope(exit) {
871         //     if ( _stream && _stream.isOpen ) {
872         //         _stream.readTimeout = 0.seconds;
873         //     }
874         // }
875 
876         _bodyDecoder = new DataPipe!ubyte();
877         scope(exit) {
878             if ( !_useStreaming ) {
879                 _bodyDecoder = null;
880                 _unChunker = null;
881             }
882         }
883 
884         auto buffer = Buffer!ubyte();
885         Buffer!ubyte partialBody;
886         ptrdiff_t read;
887         string separator;
888 
889         while(true) {
890 
891             auto b = new ubyte[_bufferSize];
892             read = _stream.receive(b);
893 
894             debug(requests) tracef("read: %d", read);
895             if ( read == 0 ) {
896                 break;
897             }
898             auto data = b[0..read];
899             buffer.putNoCopy(data);
900             if ( verbosity>=3 ) {
901                 writeln(data.dump.join("\n"));
902             }
903 
904             if ( buffer.length > maxHeadersLength ) {
905                 throw new RequestException("Headers length > maxHeadersLength (%d > %d)".format(buffer.length, maxHeadersLength));
906             }
907             if ( headersHaveBeenReceived(data, buffer, separator) ) {
908                 auto s = buffer.data.findSplit(separator);
909                 auto ResponseHeaders = s[0];
910                 partialBody = Buffer!ubyte(s[2]);
911                 _contentReceived += partialBody.length;
912                 parseResponseHeaders(ResponseHeaders);
913                 break;
914             }
915         }
916 
917         analyzeHeaders(_response._responseHeaders);
918 
919         _bodyDecoder.putNoCopy(partialBody.data);
920 
921         auto v = _bodyDecoder.get();
922         _response._responseBody.putNoCopy(v);
923 
924         if ( _verbosity >= 2 ) writefln("< %d bytes of body received", partialBody.length);
925 
926         if ( _method == "HEAD" ) {
927             // HEAD response have ContentLength, but have no body
928             return;
929         }
930 
931         while( true ) {
932             if ( _contentLength >= 0 && _contentReceived >= _contentLength ) {
933                 debug(requests) trace("Body received.");
934                 break;
935             }
936             if ( _unChunker && _unChunker.done ) {
937                 break;
938             }
939 
940             if ( _useStreaming && _response._responseBody.length && !redirectCodes.canFind(_response.code) ) {
941                 debug(requests) trace("streaming requested");
942                 // save _stream in closure
943                 auto stream = _stream;
944                 // set up response
945                 _response.receiveAsRange.activated = true;
946                 _response.receiveAsRange.data = _response._responseBody.data;
947                 _response.receiveAsRange.read = delegate ubyte[] () {
948 
949                     while(true) {
950                         // check if we received everything we need
951                         if ( ( _unChunker && _unChunker.done )
952                             || !stream.isConnected()
953                             || (_contentLength > 0 && _contentReceived >= _contentLength) )
954                         {
955                             debug(requests) trace("streaming_in receive completed");
956                             _bodyDecoder.flush();
957                             return _bodyDecoder.get();
958                         }
959                         // have to continue
960                         auto b = new ubyte[_bufferSize];
961                         try {
962                             read = stream.receive(b);
963                         }
964                         catch (Exception e) {
965                             throw new RequestException("streaming_in error reading from socket", __FILE__, __LINE__, e);
966                         }
967                         debug(requests) tracef("streaming_in received %d bytes", read);
968 
969                         if ( read == 0 ) {
970                             debug(requests) tracef("streaming_in: server closed connection");
971                             _bodyDecoder.flush();
972                             return _bodyDecoder.get();
973                         }
974 
975                         if ( verbosity>=3 ) {
976                             writeln(b[0..read].dump.join("\n"));
977                         }
978 
979                         _contentReceived += read;
980                         _bodyDecoder.putNoCopy(b[0..read]);
981                         auto res = _bodyDecoder.getNoCopy();
982                         if ( res.length == 0 ) {
983                             // there were nothing to produce (beginning of the chunk or no decompressed data)
984                             continue;
985                         }
986                         if (res.length == 1) {
987                             return res[0];
988                         }
989                         //
990                         // I'd like to "return _bodyDecoder.getNoCopy().join;" but it is slower
991                         //
992                         auto total = res.map!(b=>b.length).sum;
993                         // create buffer for joined bytes
994                         ubyte[] joined = new ubyte[total];
995                         size_t p;
996                         // memcopy
997                         foreach(ref _; res) {
998                             joined[p .. p + _.length] = _;
999                             p += _.length;
1000                         }
1001                         return joined;
1002                     }
1003                     assert(0);
1004                 };
1005                 // we prepared for streaming
1006                 return;
1007             }
1008 
1009             auto b = new ubyte[_bufferSize];
1010             read = _stream.receive(b);
1011 
1012             if ( read == 0 ) {
1013                 debug(requests) trace("read done");
1014                 break;
1015             }
1016             if ( _verbosity >= 2 ) {
1017                 writefln("< %d bytes of body received", read);
1018             }
1019 
1020             if ( verbosity>=3 ) {
1021                 writeln(b[0..read].dump.join("\n"));
1022             }
1023 
1024             debug(requests) tracef("read: %d", read);
1025             _contentReceived += read;
1026             if ( _maxContentLength && _contentReceived > _maxContentLength ) {
1027                 throw new RequestException("ContentLength > maxContentLength (%d>%d)".
1028                     format(_contentLength, _maxContentLength));
1029             }
1030 
1031             _bodyDecoder.putNoCopy(b[0..read]); // send buffer to all decoders
1032 
1033             _bodyDecoder.getNoCopy.             // fetch result and place to body
1034                 each!(b => _response._responseBody.putNoCopy(b));
1035 
1036             debug(requests) tracef("receivedTotal: %d, contentLength: %d, bodyLength: %d", _contentReceived, _contentLength, _response._responseBody.length);
1037 
1038         }
1039         _bodyDecoder.flush();
1040         _response._responseBody.putNoCopy(_bodyDecoder.get());
1041     }
1042     ///
1043     /// Check that we received anything.
1044     /// Server can close previous connection (keepalive or not)
1045     ///
1046     private bool serverPrematurelyClosedConnection() pure @safe {
1047         immutable server_closed_connection = _response._responseHeaders.length == 0 && _response._status_line.length == 0;
1048         // debug(requests) tracef("server closed connection = %s (headers.length=%s, status_line.length=%s)",
1049         //     server_closed_connection, _response._responseHeaders.length,  _response._status_line.length);
1050         return server_closed_connection;
1051     }
1052     private bool isIdempotent(in string method) pure @safe nothrow {
1053         return ["GET", "HEAD"].canFind(method);
1054     }
1055     ///
1056     /// If we do not want keepalive request,
1057     /// or server signalled to close connection,
1058     /// then close it
1059     ///
1060     void close_connection_if_not_keepalive(NetworkStream _stream) {
1061         auto connection = "connection" in _response._responseHeaders;
1062         if ( !_keepAlive ) {
1063             _stream.close();
1064         } else switch(_response._version) {
1065             case HTTP11:
1066                 // HTTP/1.1 defines the "close" connection option for the sender to signal that the connection 
1067                 // will be closed after completion of the response. For example,
1068                 //        Connection: close
1069                 // in either the request or the response header fields indicates that the connection 
1070                 // SHOULD NOT be considered `persistent' (section 8.1) after the current request/response is complete.
1071                 // HTTP/1.1 applications that do not support persistent connections MUST include the "close" connection 
1072                 // option in every message.
1073                 if ( connection && (*connection).toLower.split(",").canFind("close") ) {
1074                     _stream.close();
1075                 }
1076                 break;
1077             default:
1078                 // for anything else close connection if there is no keep-alive in Connection
1079                 if ( connection && !(*connection).toLower.split(",").canFind("keep-alive") ) {
1080                     _stream.close();
1081                 }
1082                 break;
1083         }
1084     }
1085     ///
1086     /// Send multipart for request.
1087     /// You would like to use this method for sending large portions of mixed data or uploading files to forms.
1088     /// Content of the posted form consist of sources. Each source have at least name and value (can be string-like object or opened file, see more docs for MultipartForm struct)
1089     /// Params:
1090     ///     url = url
1091     ///     sources = array of sources.
1092     HTTPResponse exec(string method="POST")(string url, MultipartForm sources) {
1093         import std.uuid;
1094         import std.file;
1095 
1096         checkURL(url);
1097         if ( _cm is null ) {
1098             _cm = new ConnManager();
1099         }
1100 
1101         NetworkStream _stream;
1102         _method = method;
1103         _response = new HTTPResponse;
1104         _response.uri = _uri;
1105         _response.finalURI = _uri;
1106         bool restartedRequest = false;
1107 
1108     connect:
1109         _contentReceived = 0;
1110         _response._startedAt = Clock.currTime;
1111 
1112         assert(_stream is null);
1113 
1114         _stream = _cm.get(_uri.scheme, _uri.host, _uri.port);
1115 
1116         if ( _stream is null ) {
1117             debug(requests) trace("create new connection");
1118             _stream = setupConnection();
1119         } else {
1120             debug(requests) trace("reuse old connection");
1121         }
1122 
1123         assert(_stream !is null);
1124 
1125         if ( !_stream.isConnected ) {
1126             debug(requests) trace("disconnected stream on enter");
1127             if ( !restartedRequest ) {
1128                 debug(requests) trace("disconnected stream on enter: retry");
1129                 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream);
1130 
1131                 _cm.del(_uri.scheme, _uri.host, _uri.port);
1132                 _stream.close();
1133                 _stream = null;
1134 
1135                 restartedRequest = true;
1136                 goto connect;
1137             }
1138             debug(requests) trace("disconnected stream on enter: return response");
1139             //_stream = null;
1140             return _response;
1141         }
1142         _response._connectedAt = Clock.currTime;
1143 
1144         Appender!string req;
1145         req.put(requestString());
1146 
1147         string   boundary = randomUUID().toString;
1148         string[] partHeaders;
1149         size_t   contentLength;
1150 
1151         foreach(ref part; sources._sources) {
1152             string h = "--" ~ boundary ~ "\r\n";
1153             string disposition = `form-data; name="%s"`.format(part.name);
1154             string optionals = part.
1155                 parameters.byKeyValue().
1156                 filter!(p => p.key!="Content-Type").
1157                 map!   (p => "%s=%s".format(p.key, p.value)).
1158                 join("; ");
1159 
1160             h ~= `Content-Disposition: ` ~ [disposition, optionals].join("; ") ~ "\r\n";
1161 
1162             auto contentType = "Content-Type" in part.parameters;
1163             if ( contentType ) {
1164                 h ~= "Content-Type: " ~ *contentType ~ "\r\n";
1165             }
1166 
1167             h ~= "\r\n";
1168             partHeaders ~= h;
1169             contentLength += h.length + part.input.getSize() + "\r\n".length;
1170         }
1171         contentLength += "--".length + boundary.length + "--\r\n".length;
1172 
1173         auto h = requestHeaders();
1174         safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "multipart/form-data; boundary=" ~ boundary);
1175         safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(contentLength));
1176         
1177         h.byKeyValue.
1178             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
1179                 each!(h => req.put(h));
1180         req.put("\r\n");
1181 
1182         debug(requests) trace(req.data);
1183         if ( _verbosity >= 1 ) req.data.splitLines.each!(a => writeln("> " ~ a));
1184 
1185         try {
1186             _stream.send(req.data());
1187             foreach(ref source; sources._sources) {
1188                 debug(requests) tracef("sending part headers <%s>", partHeaders.front);
1189                 _stream.send(partHeaders.front);
1190                 partHeaders.popFront;
1191                 while (true) {
1192                     auto chunk = source.input.read();
1193                     if ( chunk.length <= 0 ) {
1194                         break;
1195                     }
1196                     _stream.send(chunk);
1197                 }
1198                 _stream.send("\r\n");
1199             }
1200             _stream.send("--" ~ boundary ~ "--\r\n");
1201             _response._requestSentAt = Clock.currTime;
1202             receiveResponse(_stream);
1203             _response._finishedAt = Clock.currTime;
1204         }
1205         catch (NetworkException e) {
1206             errorf("Error sending request: ", e.msg);
1207             _stream.close();
1208             return _response;
1209         }
1210 
1211         if ( serverPrematurelyClosedConnection()
1212         && !restartedRequest
1213         && isIdempotent(_method)
1214         ) {
1215             ///
1216             /// We didn't receive any data (keepalive connectioin closed?)
1217             /// and we can restart this request.
1218             /// Go ahead.
1219             ///
1220             debug(requests) tracef("Server closed keepalive connection");
1221 
1222             assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream);
1223 
1224             _cm.del(_uri.scheme, _uri.host, _uri.port);
1225             _stream.close();
1226             _stream = null;
1227 
1228             restartedRequest = true;
1229             goto connect;
1230         }
1231 
1232         if ( _useStreaming ) {
1233             if ( _response._receiveAsRange.activated ) {
1234                 debug(requests) trace("streaming_in activated");
1235                 return _response;
1236             } else {
1237                 // this can happen if whole response body received together with headers
1238                 _response._receiveAsRange.data = _response.responseBody.data;
1239             }
1240         }
1241 
1242         close_connection_if_not_keepalive(_stream);
1243 
1244         if ( _verbosity >= 1 ) {
1245             writeln(">> Connect time: ", _response._connectedAt - _response._startedAt);
1246             writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt);
1247             writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt);
1248         }
1249 
1250         if ( willFollowRedirect ) {
1251             if ( _history.length >= _maxRedirects ) {
1252                 _stream = null;
1253                 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects));
1254             }
1255             // "location" in response already checked in canFollowRedirect
1256             immutable new_location = *("location" in _response.responseHeaders);
1257             immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location);
1258 
1259             // save current response for history
1260             _history ~= _response;
1261 
1262             // prepare new response (for redirected request)
1263             _response = new HTTPResponse;
1264             _response.uri = current_uri;
1265             _response.finalURI = next_uri;
1266             _stream = null;
1267 
1268             // set new uri
1269             this._uri = next_uri;
1270             debug(requests) tracef("Redirected to %s", next_uri);
1271             if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) {
1272                 // 307 and 308 do not change method
1273                 return this.get();
1274             }
1275             if ( restartedRequest ) {
1276                 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect");
1277                 restartedRequest = false;
1278             }
1279             goto connect;
1280         }
1281 
1282         _response._history = _history;
1283         return _response;
1284     }
1285 
1286     // we use this if we send from ubyte[][] and user provided Content-Length
1287     private void sendFlattenContent(T)(NetworkStream _stream, T content) {
1288         while ( !content.empty ) {
1289             auto chunk = content.front;
1290             _stream.send(chunk);
1291             content.popFront;
1292         }
1293         debug(requests) tracef("sent");
1294     }
1295     // we use this if we send from ubyte[][] as chunked content
1296     private void sendChunkedContent(T)(NetworkStream _stream, T content) {
1297         while ( !content.empty ) {
1298             auto chunk = content.front;
1299             auto chunkHeader = "%x\r\n".format(chunk.length);
1300             debug(requests) tracef("sending %s%s", chunkHeader, chunk);
1301             _stream.send(chunkHeader);
1302             _stream.send(chunk);
1303             _stream.send("\r\n");
1304             content.popFront;
1305         }
1306         debug(requests) tracef("sent");
1307         _stream.send("0\r\n\r\n");
1308     }
1309     ///
1310     /// POST/PUT/... data from some string(with Content-Length), or from range of strings/bytes (use Transfer-Encoding: chunked).
1311     /// When rank 1 (flat array) used as content it must have length. In that case "content" will be sent directly to network, and Content-Length headers will be added.
1312     /// If you are goung to send some range and do not know length at the moment when you start to send request, then you can send chunks of chars or ubyte.
1313     /// Try not to send too short chunks as this will put additional load on client and server. Chunks of length 2048 or 4096 are ok.
1314     ///
1315     /// Parameters:
1316     ///    url = url
1317     ///    content = string or input range
1318     ///    contentType = content type
1319     ///  Returns:
1320     ///     Response
1321     ///  Examples:
1322     ///  ---------------------------------------------------------------------------------------------------------
1323     ///      rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream");
1324     ///
1325     ///      auto s = lineSplitter("one,\ntwo,\nthree.");
1326     ///      rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream");
1327     ///
1328     ///      auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1329     ///      rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream");
1330     ///
1331     ///      auto f = File("tests/test.txt", "rb");
1332     ///      rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream");
1333     ///  --------------------------------------------------------------------------------------------------------
1334     HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="application/octet-stream")
1335         if ( (rank!R == 1)
1336             || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front))))
1337             || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte)))
1338         )
1339     do {
1340         debug(requests) tracef("started url=%s, this._uri=%s", url, _uri);
1341 
1342         checkURL(url);
1343         if ( _cm is null ) {
1344             _cm = new ConnManager();
1345         }
1346 
1347         NetworkStream _stream;
1348         _method = method;
1349         _response = new HTTPResponse;
1350         _history.length = 0;
1351         _response.uri = _uri;
1352         _response.finalURI = _uri;
1353         bool restartedRequest = false;
1354         bool send_flat;
1355 
1356     connect:
1357         _contentReceived = 0;
1358         _response._startedAt = Clock.currTime;
1359 
1360         assert(_stream is null);
1361 
1362         _stream = _cm.get(_uri.scheme, _uri.host, _uri.port);
1363 
1364         if ( _stream is null ) {
1365             debug(requests) trace("create new connection");
1366             _stream = setupConnection();
1367         } else {
1368             debug(requests) trace("reuse old connection");
1369         }
1370 
1371         assert(_stream !is null);
1372 
1373         if ( !_stream.isConnected ) {
1374             debug(requests) trace("disconnected stream on enter");
1375             if ( !restartedRequest ) {
1376                 debug(requests) trace("disconnected stream on enter: retry");
1377                 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream);
1378 
1379                 _cm.del(_uri.scheme, _uri.host, _uri.port);
1380                 _stream.close();
1381                 _stream = null;
1382 
1383                 restartedRequest = true;
1384                 goto connect;
1385             }
1386             debug(requests) trace("disconnected stream on enter: return response");
1387             //_stream = null;
1388             return _response;
1389         }
1390         _response._connectedAt = Clock.currTime;
1391 
1392         Appender!string req;
1393         req.put(requestString());
1394 
1395         auto h = requestHeaders;
1396         if ( contentType ) {
1397             safeSetHeader(h, _userHeaders.ContentType, "Content-Type", contentType);
1398         }
1399         static if ( rank!R == 1 ) {
1400             safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(content.length));
1401         } else {
1402             if ( _userHeaders.ContentLength ) {
1403                 debug(requests) tracef("User provided content-length for chunked content");
1404                 send_flat = true;
1405             } else {
1406                 h["Transfer-Encoding"] = "chunked";
1407                 send_flat = false;
1408             }
1409         }
1410         h.byKeyValue.
1411             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
1412             each!(h => req.put(h));
1413         req.put("\r\n");
1414 
1415         debug(requests) trace(req.data);
1416         if ( _verbosity >= 1 ) {
1417             req.data.splitLines.each!(a => writeln("> " ~ a));
1418         }
1419 
1420         try {
1421             // send headers
1422             _stream.send(req.data());
1423             // send body
1424             static if ( rank!R == 1) {
1425                 _stream.send(content);
1426             } else {
1427                 if ( send_flat ) {
1428                     sendFlattenContent(_stream, content);
1429                 } else {
1430                     sendChunkedContent(_stream, content);
1431                 }
1432             }
1433             _response._requestSentAt = Clock.currTime;
1434             receiveResponse(_stream);
1435             _response._finishedAt = Clock.currTime;
1436         } catch (NetworkException e) {
1437             _stream.close();
1438             throw new RequestException("Network error during data exchange");
1439         }
1440 
1441         if ( serverPrematurelyClosedConnection()
1442         && !restartedRequest
1443         && isIdempotent(_method)
1444         ) {
1445             ///
1446             /// We didn't receive any data (keepalive connectioin closed?)
1447             /// and we can restart this request.
1448             /// Go ahead.
1449             ///
1450             debug(requests) tracef("Server closed keepalive connection");
1451 
1452             assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream);
1453 
1454             _cm.del(_uri.scheme, _uri.host, _uri.port);
1455             _stream.close();
1456             _stream = null;
1457 
1458             restartedRequest = true;
1459             goto connect;
1460         }
1461 
1462         if ( _useStreaming ) {
1463             if ( _response._receiveAsRange.activated ) {
1464                 debug(requests) trace("streaming_in activated");
1465                 return _response;
1466             } else {
1467                 // this can happen if whole response body received together with headers
1468                 _response._receiveAsRange.data = _response.responseBody.data;
1469             }
1470         }
1471 
1472         close_connection_if_not_keepalive(_stream);
1473         
1474         if ( _verbosity >= 1 ) {
1475             writeln(">> Connect time: ", _response._connectedAt - _response._startedAt);
1476             writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt);
1477             writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt);
1478         }
1479 
1480 
1481         if ( willFollowRedirect ) {
1482             if ( _history.length >= _maxRedirects ) {
1483                 _stream = null;
1484                 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects));
1485             }
1486             // "location" in response already checked in canFollowRedirect
1487             immutable new_location = *("location" in _response.responseHeaders);
1488             immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location);
1489 
1490             // save current response for history
1491             _history ~= _response;
1492 
1493             // prepare new response (for redirected request)
1494             _response = new HTTPResponse;
1495             _response.uri = current_uri;
1496             _response.finalURI = next_uri;
1497 
1498             _stream = null;
1499 
1500             // set new uri
1501             this._uri = next_uri;
1502             debug(requests) tracef("Redirected to %s", next_uri);
1503             if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) {
1504                 // 307 and 308 do not change method
1505                 return this.get();
1506             }
1507             if ( restartedRequest ) {
1508                 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect");
1509                 restartedRequest = false;
1510             }
1511             goto connect;
1512         }
1513 
1514         _response._history = _history;
1515         return _response;
1516     }
1517     ///
1518     /// Send request with parameters.
1519     /// If used for POST or PUT requests then application/x-www-form-urlencoded used.
1520     /// Request parameters will be encoded into request string or placed in request body for POST/PUT
1521     /// requests.
1522     /// Parameters:
1523     ///     url = url
1524     ///     params = request parameters
1525     ///  Returns:
1526     ///     Response
1527     ///  Examples:
1528     ///  ---------------------------------------------------------------------------------
1529     ///     rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]);
1530     ///  ---------------------------------------------------------------------------------
1531     ///
1532     HTTPResponse exec(string method="GET")(string url = null, QueryParam[] params = null)
1533     do {
1534         debug(requests) tracef("started url=%s, this._uri=%s", url, _uri);
1535 
1536         checkURL(url);
1537         if ( _cm is null ) {
1538             _cm = new ConnManager();
1539         }
1540 
1541         NetworkStream _stream;
1542         _method = method;
1543         _response = new HTTPResponse;
1544         _history.length = 0;
1545         _response.uri = _uri;
1546         _response.finalURI = _uri;
1547         bool restartedRequest = false; // True if this is restarted keepAlive request
1548 
1549     connect:
1550         if ( _method == "GET" && _uri in _permanent_redirects ) {
1551             debug(requests) trace("use parmanent redirects cache");
1552             _uri = uriFromLocation(_uri, _permanent_redirects[_uri]);
1553             _response._finalURI = _uri;
1554         }
1555         _contentReceived = 0;
1556         _response._startedAt = Clock.currTime;
1557 
1558         assert(_stream is null);
1559 
1560         _stream = _cm.get(_uri.scheme, _uri.host, _uri.port);
1561 
1562         if ( _stream is null ) {
1563             debug(requests) trace("create new connection");
1564             _stream = setupConnection();
1565         } else {
1566             debug(requests) trace("reuse old connection");
1567         }
1568 
1569         assert(_stream !is null);
1570 
1571         if ( !_stream.isConnected ) {
1572             debug(requests) trace("disconnected stream on enter");
1573             if ( !restartedRequest ) {
1574                 debug(requests) trace("disconnected stream on enter: retry");
1575                 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream);
1576 
1577                 _cm.del(_uri.scheme, _uri.host, _uri.port);
1578                 _stream.close();
1579                 _stream = null;
1580 
1581                 restartedRequest = true;
1582                 goto connect;
1583             }
1584             debug(requests) trace("disconnected stream on enter: return response");
1585             //_stream = null;
1586             return _response;
1587         }
1588         _response._connectedAt = Clock.currTime;
1589 
1590         auto h = requestHeaders();
1591 
1592         Appender!string req;
1593 
1594         string encoded;
1595 
1596         switch (_method) {
1597             case "POST","PUT":
1598                 encoded = params2query(params);
1599                 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "application/x-www-form-urlencoded");
1600                 if ( encoded.length > 0) {
1601                     safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(encoded.length));
1602                 }
1603                 req.put(requestString());
1604                 break;
1605             default:
1606                 req.put(requestString(params));
1607         }
1608 
1609         h.byKeyValue.
1610             map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n").
1611             each!(h => req.put(h));
1612         req.put("\r\n");
1613         if ( encoded ) {
1614             req.put(encoded);
1615         }
1616 
1617         debug(requests) trace(req.data);
1618         if ( _verbosity >= 1 ) {
1619             req.data.splitLines.each!(a => writeln("> " ~ a));
1620         }
1621         //
1622         // Now send request and receive response
1623         //
1624         try {
1625             _stream.send(req.data());
1626             _response._requestSentAt = Clock.currTime;
1627             debug(requests) trace("starting receive response");
1628             receiveResponse(_stream);
1629             debug(requests) trace("done receive response");
1630             _response._finishedAt = Clock.currTime;
1631         }
1632         catch (NetworkException e) {
1633             // On SEND this can means:
1634             // we started to send request to the server, but it closed connection because of keepalive timeout.
1635             // We have to restart request if possible.
1636 
1637             // On RECEIVE - if we received something - then this exception is real and unexpected error.
1638             // If we didn't receive anything - we can restart request again as it can be
1639             debug(requests) tracef("Exception on receive response: %s", e.msg);
1640             if ( _response._responseHeaders.length != 0 )
1641             {
1642                 _stream.close();
1643                 throw new RequestException("Unexpected network error");
1644             }
1645         }
1646 
1647         if ( serverPrematurelyClosedConnection()
1648             && !restartedRequest
1649             && isIdempotent(_method)
1650             ) {
1651             ///
1652             /// We didn't receive any data (keepalive connectioin closed?)
1653             /// and we can restart this request.
1654             /// Go ahead.
1655             ///
1656             debug(requests) tracef("Server closed keepalive connection");
1657 
1658             assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream);
1659 
1660             _cm.del(_uri.scheme, _uri.host, _uri.port);
1661             _stream.close();
1662             _stream = null;
1663             
1664             restartedRequest = true;
1665             goto connect;
1666         }
1667 
1668         if ( _useStreaming ) {
1669             if ( _response._receiveAsRange.activated ) {
1670                 debug(requests) trace("streaming_in activated");
1671                 return _response;
1672             } else {
1673                 // this can happen if whole response body received together with headers
1674                 _response._receiveAsRange.data = _response.responseBody.data;
1675             }
1676         }
1677 
1678         close_connection_if_not_keepalive(_stream);
1679 
1680         if ( _verbosity >= 1 ) {
1681             writeln(">> Connect time: ", _response._connectedAt - _response._startedAt);
1682             writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt);
1683             writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt);
1684         }
1685 
1686         if ( willFollowRedirect ) {
1687             debug(requests) trace("going to follow redirect");
1688             if ( _history.length >= _maxRedirects ) {
1689                 _stream = null;
1690                 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects));
1691             }
1692             // "location" in response already checked in canFollowRedirect
1693             immutable new_location = *("location" in _response.responseHeaders);
1694             immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location);
1695 
1696             if ( _method == "GET" && _response.code == 301 ) {
1697                 _permanent_redirects[_uri] = new_location;
1698             }
1699 
1700             // save current response for history
1701             _history ~= _response;
1702 
1703             // prepare new response (for redirected request)
1704             _response = new HTTPResponse;
1705             _response.uri = current_uri;
1706             _response.finalURI = next_uri;
1707             _stream = null;
1708             
1709             // set new uri
1710             _uri = next_uri;
1711             debug(requests) tracef("Redirected to %s", next_uri);
1712             if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) {
1713                 // 307 and 308 do not change method
1714                 return this.get();
1715             }
1716             if ( restartedRequest ) {
1717                 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect");
1718                 restartedRequest = false;
1719             }
1720             goto connect;
1721         }
1722 
1723         _response._history = _history;
1724         return _response;
1725     }
1726 
1727     /// WRAPPERS
1728     ///
1729     /// send file(s) using POST and multipart form.
1730     /// This wrapper will be deprecated, use post with MultipartForm - it is more general and clear.
1731     /// Parameters:
1732     ///     url = url
1733     ///     files = array of PostFile structures
1734     /// Returns:
1735     ///     Response
1736     /// Each PostFile structure contain path to file, and optional field name and content type.
1737     /// If no field name provided, then basename of the file will be used.
1738     /// application/octet-stream is default when no content type provided.
1739     /// Example:
1740     /// ---------------------------------------------------------------
1741     ///    PostFile[] files = [
1742     ///                   {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"},
1743     ///                   {fileName:"tests/test.txt"}
1744     ///               ];
1745     ///    rs = rq.exec!"POST"("http://httpbin.org/post", files);
1746     /// ---------------------------------------------------------------
1747     ///
1748     HTTPResponse exec(string method="POST")(string url, PostFile[] files) if (method=="POST") {
1749         MultipartForm multipart;
1750         File[]        toClose;
1751         foreach(ref f; files) {
1752             File file = File(f.fileName, "rb");
1753             toClose ~= file;
1754             string fileName = f.fileName ? f.fileName : f.fieldName;
1755             string contentType = f.contentType ? f.contentType : "application/octetstream";
1756             multipart.add(f.fieldName, new FormDataFile(file), ["filename":fileName, "Content-Type": contentType]);
1757         }
1758         auto res = exec!"POST"(url, multipart);
1759         toClose.each!"a.close";
1760         return res;
1761     }
1762     ///
1763     /// exec request with parameters when you can use dictionary (when you have no duplicates in parameter names)
1764     /// Consider switch to exec(url, QueryParams) as it more generic and clear.
1765     /// Parameters:
1766     ///     url = url
1767     ///     params = dictionary with field names as keys and field values as values.
1768     /// Returns:
1769     ///     Response
1770     HTTPResponse exec(string method="GET")(string url, string[string] params) {
1771         return exec!method(url, params.byKeyValue.map!(p => QueryParam(p.key, p.value)).array);
1772     }
1773     ///
1774     /// GET request. Simple wrapper over exec!"GET"
1775     /// Params:
1776     /// args = request parameters. see exec docs.
1777     ///
1778     HTTPResponse get(A...)(A args) {
1779         return exec!"GET"(args);
1780     }
1781     ///
1782     /// POST request. Simple wrapper over exec!"POST"
1783     /// Params:
1784     /// uri = endpoint uri
1785     /// args = request parameters. see exec docs.
1786     ///
1787     HTTPResponse post(A...)(string uri, A args) {
1788         return exec!"POST"(uri, args);
1789     }
1790 }
1791 
1792 version(vibeD) {
1793     import std.json;
1794     package string httpTestServer() {
1795         return "http://httpbin.org/";
1796     }
1797     package string fromJsonArrayToStr(JSONValue v) {
1798         return v.str;
1799     }
1800 }
1801 else {
1802     import std.json;
1803     package string httpTestServer() {
1804         return "http://127.0.0.1:8081/";
1805     }
1806     package string fromJsonArrayToStr(JSONValue v) {
1807         return cast(string)(v.array.map!"cast(ubyte)a.integer".array);
1808     }
1809 }
1810 
1811 
1812 package unittest {
1813     import std.json;
1814     import std.array;
1815 
1816     globalLogLevel(LogLevel.info);
1817 
1818     string httpbinUrl = httpTestServer();
1819     version(vibeD) {
1820     }
1821     else {
1822         import httpbin;
1823         auto server = httpbinApp();
1824         server.start();
1825         scope(exit) {
1826             server.stop();
1827         }
1828     }
1829     HTTPRequest  rq;
1830     HTTPResponse rs;
1831     info("Check GET");
1832     URI uri = URI(httpbinUrl);
1833     rs = rq.get(httpbinUrl);
1834     assert(rs.code==200);
1835     assert(rs.responseBody.length > 0);
1836     assert(rq.format("%m|%h|%p|%P|%q|%U") ==
1837             "GET|%s|%d|%s||%s"
1838             .format(uri.host, uri.port, uri.path, httpbinUrl));
1839     info("Check GET with AA params");
1840     {
1841         rs = rq.get(httpbinUrl ~ "get", ["c":" d", "a":"b"]);
1842         assert(rs.code == 200);
1843         auto json = parseJSON(cast(string)rs.responseBody.data).object["args"].object;
1844         assert(json["c"].str == " d");
1845         assert(json["a"].str == "b");
1846     }
1847     rq.keepAlive = false; // disable keepalive on non-idempotent requests
1848     info("Check POST files");
1849     {
1850         import std.file;
1851         import std.path;
1852         auto tmpd = tempDir();
1853         auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt";
1854         auto f = File(tmpfname, "wb");
1855         f.rawWrite("abcdefgh\n12345678\n");
1856         f.close();
1857         // files
1858         PostFile[] files = [
1859             {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"},
1860             {fileName: tmpfname}
1861         ];
1862         rs = rq.post(httpbinUrl ~ "post", files);
1863         assert(rs.code==200);
1864     }
1865     info("Check POST chunked from file.byChunk");
1866     {
1867         import std.file;
1868         import std.path;
1869         auto tmpd = tempDir();
1870         auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt";
1871         auto f = File(tmpfname, "wb");
1872         f.rawWrite("abcdefgh\n12345678\n");
1873         f.close();
1874         f = File(tmpfname, "rb");
1875         rs = rq.post(httpbinUrl ~ "post", f.byChunk(3), "application/octet-stream");
1876         if (httpbinUrl != "http://httpbin.org/") {
1877             assert(rs.code==200);
1878             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]);
1879             assert(data=="abcdefgh\n12345678\n");
1880         }
1881         f.close();
1882     }
1883     info("Check POST chunked from lineSplitter");
1884     {
1885         auto s = lineSplitter("one,\ntwo,\nthree.");
1886         rs = rq.exec!"POST"(httpbinUrl ~ "post", s, "application/octet-stream");
1887         if (httpbinUrl != "http://httpbin.org/") {
1888             assert(rs.code==200);
1889             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]);
1890             assert(data=="one,two,three.");
1891         }
1892     }
1893     info("Check POST chunked from array");
1894     {
1895         auto s = ["one,", "two,", "three."];
1896         rs = rq.post(httpbinUrl ~ "post", s, "application/octet-stream");
1897         if (httpbinUrl != "http://httpbin.org/") {
1898             assert(rs.code==200);
1899             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]);
1900             assert(data=="one,two,three.");
1901         }
1902     }
1903     info("Check POST chunked using std.range.chunks()");
1904     {
1905         auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1906         rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream");
1907         if (httpbinUrl != "http://httpbin.org/") {
1908             assert(rs.code==200);
1909             auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody.data).object["data"]);
1910             assert(data==s);
1911         }
1912     }
1913     info("Check POST from QueryParams");
1914     {
1915         rs = rq.post(httpbinUrl ~ "post", queryParams("name[]", "first", "name[]", 2));
1916         assert(rs.code==200);
1917         auto data = parseJSON(cast(string)rs.responseBody).object["form"].object;
1918         string[] a;
1919         try {
1920             a = to!(string[])(data["name[]"].str);
1921         }
1922         catch (JSONException e) {
1923             a = data["name[]"].array.map!"a.str".array;
1924         }
1925         assert(equal(["first", "2"], a));
1926     }
1927     info("Check POST from AA");
1928     {
1929         rs = rq.post(httpbinUrl ~ "post", ["a":"b ", "c":"d"]);
1930         assert(rs.code==200);
1931         auto form = parseJSON(cast(string)rs.responseBody.data).object["form"].object;
1932         assert(form["a"].str == "b ");
1933         assert(form["c"].str == "d");
1934     }
1935     info("Check POST json");
1936     {
1937         rs = rq.post(httpbinUrl ~ "post?b=x", `{"a":"a b", "c":[1,2,3]}`, "application/json");
1938         assert(rs.code==200);
1939         auto json = parseJSON(cast(string)rs.responseBody).object["args"].object;
1940         assert(json["b"].str == "x");
1941         json = parseJSON(cast(string)rs.responseBody).object["json"].object;
1942         assert(json["a"].str == "a b");
1943         assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]);
1944     }
1945     info("Check DELETE");
1946     rs = rq.exec!"DELETE"(httpbinUrl ~ "delete");
1947     assert(rs.code==200);
1948     info("Check PUT");
1949     rs = rq.exec!"PUT"(httpbinUrl ~ "put",  `{"a":"b", "c":[1,2,3]}`, "application/json");
1950     assert(rs.code==200);
1951     assert(parseJSON(cast(string)rs.responseBody).object["json"].object["a"].str=="b");
1952     info("Check PATCH");
1953     rs = rq.exec!"PATCH"(httpbinUrl ~ "patch", "привiт, свiт!", "application/octet-stream");
1954     assert(rs.code==200);
1955     info("Check HEAD");
1956     rs = rq.exec!"HEAD"(httpbinUrl);
1957     assert(rs.code==200);
1958 
1959     rq._keepAlive = true;
1960     info("Check compressed content");
1961     rs = rq.get(httpbinUrl ~ "gzip");
1962     assert(rs.code==200);
1963     bool gzipped = parseJSON(cast(string)rs.responseBody).object["gzipped"].type == JSON_TYPE.TRUE;
1964     assert(gzipped);
1965     info("gzip - ok");
1966     rs = rq.get(httpbinUrl ~ "deflate");
1967     assert(rs.code==200);
1968     bool deflated = parseJSON(cast(string)rs.responseBody).object["deflated"].type == JSON_TYPE.TRUE;
1969     assert(deflated);
1970     info("deflate - ok");
1971 
1972     info("Check redirects");
1973     rs = rq.get(httpbinUrl ~ "relative-redirect/2");
1974     assert(rs.history.length == 2);
1975     assert(rs.code==200);
1976     rs = rq.get(httpbinUrl ~ "absolute-redirect/2");
1977     assert(rs.history.length == 2);
1978     assert(rs.code==200);
1979 
1980     rq.maxRedirects = 2;
1981     assertThrown!MaxRedirectsException(rq.get(httpbinUrl ~ "absolute-redirect/3"));
1982 
1983     rq.maxRedirects = 0;
1984     rs = rq.get(httpbinUrl ~ "absolute-redirect/1");
1985     assert(rs.code==302);
1986 
1987     info("Check cookie");
1988     {
1989         rq.maxRedirects = 10;
1990         rs = rq.get(httpbinUrl ~ "cookies/set?A=abcd&b=cdef");
1991         assert(rs.code == 200);
1992         auto json = parseJSON(cast(string)rs.responseBody.data).object["cookies"].object;
1993         assert(json["A"].str == "abcd");
1994         assert(json["b"].str == "cdef");
1995         foreach(c; rq.cookie) {
1996             final switch(c.attr) {
1997                 case "A":
1998                     assert(c.value == "abcd");
1999                     break;
2000                 case "b":
2001                     assert(c.value == "cdef");
2002                     break;
2003             }
2004         }
2005     }
2006     info("Check chunked content");
2007     rs = rq.get(httpbinUrl ~ "range/1024");
2008     assert(rs.code==200);
2009     assert(rs.responseBody.length==1024);
2010 
2011     info("Check basic auth");
2012     rq.authenticator = new BasicAuthentication("user", "passwd");
2013     rs = rq.get(httpbinUrl ~ "basic-auth/user/passwd");
2014     assert(rs.code==200);
2015 
2016     info("Check limits");
2017     rq = HTTPRequest();
2018     rq.maxContentLength = 1;
2019     assertThrown!RequestException(rq.get(httpbinUrl));
2020 
2021     rq = HTTPRequest();
2022     rq.maxHeadersLength = 1;
2023     assertThrown!RequestException(rq.get(httpbinUrl));
2024 
2025     rq = HTTPRequest();
2026     info("Check POST multiPartForm");
2027     {
2028         /// This is example on usage files with MultipartForm data.
2029         /// For this example we have to create files which will be sent.
2030         import std.file;
2031         import std.path;
2032         /// preapare files
2033         auto tmpd = tempDir();
2034         auto tmpfname1 = tmpd ~ dirSeparator ~ "request_test1.txt";
2035         auto f = File(tmpfname1, "wb");
2036         f.rawWrite("file1 content\n");
2037         f.close();
2038         auto tmpfname2 = tmpd ~ dirSeparator ~ "request_test2.txt";
2039         f = File(tmpfname2, "wb");
2040         f.rawWrite("file2 content\n");
2041         f.close();
2042         ///
2043         /// Ok, files ready.
2044         /// Now we will prepare Form data
2045         ///
2046         File f1 = File(tmpfname1, "rb");
2047         File f2 = File(tmpfname2, "rb");
2048         scope(exit) {
2049             f1.close();
2050             f2.close();
2051         }
2052         ///
2053         /// for each part we have to set field name, source (ubyte array or opened file) and optional filename and content-type
2054         ///
2055         MultipartForm mForm = MultipartForm().
2056             add(formData("Field1", cast(ubyte[])"form field from memory")).
2057                 add(formData("Field2", cast(ubyte[])"file field from memory", ["filename":"data2"])).
2058                 add(formData("File1", f1, ["filename":"file1", "Content-Type": "application/octet-stream"])).
2059                 add(formData("File2", f2, ["filename":"file2", "Content-Type": "application/octet-stream"]));
2060         /// everything ready, send request
2061         rs = rq.post(httpbinUrl ~ "post", mForm);
2062     }
2063     info("Check exception handling, error messages and timeous are OK");
2064     rq.timeout = 1.seconds;
2065     assertThrown!TimeoutException(rq.get(httpbinUrl ~ "delay/3"));
2066 //    assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/"));
2067 //    assertThrown!ConnectError(rq.get("http://1.1.1.1/"));
2068 //    assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/"));
2069 }
2070