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