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