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 // on exit 799 // place created connection to conn. manager 800 // close connection purged from manager (if any) 801 // 802 scope(exit) { 803 if ( stream ) 804 { 805 if ( auto purged_connection = _cm.put(_uri.scheme, _uri.host, _uri.port, stream) ) 806 { 807 debug(requests) tracef("closing purged connection %s", purged_connection); 808 purged_connection.close(); 809 } 810 } 811 } 812 813 if ( _socketFactory ) 814 { 815 debug(requests) tracef("use socketFactory"); 816 stream = _socketFactory(_uri.scheme, _uri.host, _uri.port); 817 } 818 819 if ( stream ) // socket factory created connection 820 { 821 return stream; 822 } 823 824 URI uri; // this URI will be used temporarry if we need proxy 825 string actual_proxy = select_proxy(_uri.scheme); 826 final switch (_uri.scheme) { 827 case"http": 828 if ( actual_proxy ) { 829 uri.uri_parse(actual_proxy); 830 uri.idn_encode(); 831 } else { 832 // use original uri 833 uri = _uri; 834 } 835 stream = new TCPStream(); 836 stream.bind(_bind); 837 stream.connect(uri.host, uri.port, _timeout); 838 break ; 839 case"https": 840 if ( actual_proxy ) { 841 uri.uri_parse(actual_proxy); 842 uri.idn_encode(); 843 stream = new TCPStream(); 844 stream.bind(_bind); 845 stream.connect(uri.host, uri.port, _timeout); 846 if ( verbosity>=1 ) { 847 writeln("> CONNECT %s:%d HTTP/1.1".format(_uri.host, _uri.port)); 848 } 849 stream.send("CONNECT %s:%d HTTP/1.1\r\n\r\n".format(_uri.host, _uri.port)); 850 while ( stream.isConnected ) { 851 ubyte[1024] b; 852 auto read = stream.receive(b); 853 if ( verbosity>=1) { 854 writefln("< %s", cast(string)b[0..read]); 855 } 856 debug(requests) tracef("read: %d", read); 857 if ( b[0..read].canFind("\r\n\r\n") || b[0..read].canFind("\n\n") ) { 858 debug(requests) tracef("proxy connection ready"); 859 // convert connection to ssl 860 stream = new SSLStream(stream, _sslOptions, _uri.host); 861 break ; 862 } else { 863 debug(requests) tracef("still wait for proxy connection"); 864 } 865 } 866 } else { 867 uri = _uri; 868 stream = new SSLStream(_sslOptions); 869 stream.bind(_bind); 870 stream.connect(uri.host, uri.port, _timeout); 871 debug(requests) tracef("ssl connection to origin server ready"); 872 } 873 break ; 874 } 875 876 return stream; 877 } 878 /// 879 /// Request sent, now receive response. 880 /// Find headers, split on headers and body, continue to receive body 881 /// 882 private void receiveResponse(NetworkStream _stream) { 883 884 try { 885 _stream.readTimeout = timeout; 886 } catch (Exception e) { 887 debug(requests) tracef("Failed to set read timeout for stream: %s", e.msg); 888 return; 889 } 890 // Commented this out as at exit we can have alreade closed socket 891 // scope(exit) { 892 // if ( _stream && _stream.isOpen ) { 893 // _stream.readTimeout = 0.seconds; 894 // } 895 // } 896 897 _bodyDecoder = new DataPipe!ubyte(); 898 scope(exit) { 899 if ( !_useStreaming ) { 900 _bodyDecoder = null; 901 _unChunker = null; 902 } 903 } 904 905 auto buffer = Buffer!ubyte(); 906 Buffer!ubyte partialBody; 907 ptrdiff_t read; 908 string separator; 909 910 while(true) { 911 912 auto b = new ubyte[_bufferSize]; 913 read = _stream.receive(b); 914 915 debug(requests) tracef("read: %d", read); 916 if ( read == 0 ) { 917 break; 918 } 919 auto data = b[0..read]; 920 buffer.putNoCopy(data); 921 if ( verbosity>=3 ) { 922 writeln(data.dump.join("\n")); 923 } 924 925 if ( buffer.length > maxHeadersLength ) { 926 throw new RequestException("Headers length > maxHeadersLength (%d > %d)".format(buffer.length, maxHeadersLength)); 927 } 928 if ( headersHaveBeenReceived(data, buffer, separator) ) { 929 auto s = buffer.data.findSplit(separator); 930 auto ResponseHeaders = s[0]; 931 partialBody = Buffer!ubyte(s[2]); 932 _contentReceived += partialBody.length; 933 parseResponseHeaders(ResponseHeaders); 934 break; 935 } 936 } 937 938 analyzeHeaders(_response._responseHeaders); 939 940 _bodyDecoder.putNoCopy(partialBody.data); 941 942 auto v = _bodyDecoder.get(); 943 _response._responseBody.putNoCopy(v); 944 945 if ( _verbosity >= 2 ) writefln("< %d bytes of body received", partialBody.length); 946 947 if ( _method == "HEAD" ) { 948 // HEAD response have ContentLength, but have no body 949 return; 950 } 951 952 while( true ) { 953 if ( _contentLength >= 0 && _contentReceived >= _contentLength ) { 954 debug(requests) trace("Body received."); 955 break; 956 } 957 if ( _unChunker && _unChunker.done ) { 958 break; 959 } 960 961 if ( _useStreaming && _response._responseBody.length && !redirectCodes.canFind(_response.code) ) { 962 debug(requests) trace("streaming requested"); 963 // save _stream in closure 964 auto stream = _stream; 965 // set up response 966 _response.receiveAsRange.activated = true; 967 _response.receiveAsRange.data = _response._responseBody.data; 968 _response.receiveAsRange.read = delegate ubyte[] () { 969 970 while(true) { 971 // check if we received everything we need 972 if ( ( _unChunker && _unChunker.done ) 973 || !stream.isConnected() 974 || (_contentLength > 0 && _contentReceived >= _contentLength) ) 975 { 976 debug(requests) trace("streaming_in receive completed"); 977 _bodyDecoder.flush(); 978 return _bodyDecoder.get(); 979 } 980 // have to continue 981 auto b = new ubyte[_bufferSize]; 982 try { 983 read = stream.receive(b); 984 } 985 catch (Exception e) { 986 throw new RequestException("streaming_in error reading from socket", __FILE__, __LINE__, e); 987 } 988 debug(requests) tracef("streaming_in received %d bytes", read); 989 990 if ( read == 0 ) { 991 debug(requests) tracef("streaming_in: server closed connection"); 992 _bodyDecoder.flush(); 993 return _bodyDecoder.get(); 994 } 995 996 if ( verbosity>=3 ) { 997 writeln(b[0..read].dump.join("\n")); 998 } 999 1000 _contentReceived += read; 1001 _bodyDecoder.putNoCopy(b[0..read]); 1002 auto res = _bodyDecoder.getNoCopy(); 1003 if ( res.length == 0 ) { 1004 // there were nothing to produce (beginning of the chunk or no decompressed data) 1005 continue; 1006 } 1007 if (res.length == 1) { 1008 return res[0]; 1009 } 1010 // 1011 // I'd like to "return _bodyDecoder.getNoCopy().join;" but it is slower 1012 // 1013 auto total = res.map!(b=>b.length).sum; 1014 // create buffer for joined bytes 1015 ubyte[] joined = new ubyte[total]; 1016 size_t p; 1017 // memcopy 1018 foreach(ref _; res) { 1019 joined[p .. p + _.length] = _; 1020 p += _.length; 1021 } 1022 return joined; 1023 } 1024 assert(0); 1025 }; 1026 // we prepared for streaming 1027 return; 1028 } 1029 1030 auto b = new ubyte[_bufferSize]; 1031 read = _stream.receive(b); 1032 1033 if ( read == 0 ) { 1034 debug(requests) trace("read done"); 1035 break; 1036 } 1037 if ( _verbosity >= 2 ) { 1038 writefln("< %d bytes of body received", read); 1039 } 1040 1041 if ( verbosity>=3 ) { 1042 writeln(b[0..read].dump.join("\n")); 1043 } 1044 1045 debug(requests) tracef("read: %d", read); 1046 _contentReceived += read; 1047 if ( _maxContentLength && _contentReceived > _maxContentLength ) { 1048 throw new RequestException("ContentLength > maxContentLength (%d>%d)". 1049 format(_contentLength, _maxContentLength)); 1050 } 1051 1052 _bodyDecoder.putNoCopy(b[0..read]); // send buffer to all decoders 1053 1054 _bodyDecoder.getNoCopy. // fetch result and place to body 1055 each!(b => _response._responseBody.putNoCopy(b)); 1056 1057 debug(requests) tracef("receivedTotal: %d, contentLength: %d, bodyLength: %d", _contentReceived, _contentLength, _response._responseBody.length); 1058 1059 } 1060 _bodyDecoder.flush(); 1061 _response._responseBody.putNoCopy(_bodyDecoder.get()); 1062 } 1063 /// 1064 /// Check that we received anything. 1065 /// Server can close previous connection (keepalive or not) 1066 /// 1067 private bool serverPrematurelyClosedConnection() pure @safe { 1068 immutable server_closed_connection = _response._responseHeaders.length == 0 && _response._status_line.length == 0; 1069 // debug(requests) tracef("server closed connection = %s (headers.length=%s, status_line.length=%s)", 1070 // server_closed_connection, _response._responseHeaders.length, _response._status_line.length); 1071 return server_closed_connection; 1072 } 1073 private bool isIdempotent(in string method) pure @safe nothrow { 1074 return ["GET", "HEAD"].canFind(method); 1075 } 1076 /// 1077 /// If we do not want keepalive request, 1078 /// or server signalled to close connection, 1079 /// then close it 1080 /// 1081 void close_connection_if_not_keepalive(NetworkStream _stream) { 1082 auto connection = "connection" in _response._responseHeaders; 1083 if ( !_keepAlive ) { 1084 _stream.close(); 1085 } else switch(_response._version) { 1086 case HTTP11: 1087 // HTTP/1.1 defines the "close" connection option for the sender to signal that the connection 1088 // will be closed after completion of the response. For example, 1089 // Connection: close 1090 // in either the request or the response header fields indicates that the connection 1091 // SHOULD NOT be considered `persistent' (section 8.1) after the current request/response is complete. 1092 // HTTP/1.1 applications that do not support persistent connections MUST include the "close" connection 1093 // option in every message. 1094 if ( connection && (*connection).toLower.split(",").canFind("close") ) { 1095 _stream.close(); 1096 } 1097 break; 1098 default: 1099 // for anything else close connection if there is no keep-alive in Connection 1100 if ( connection && !(*connection).toLower.split(",").canFind("keep-alive") ) { 1101 _stream.close(); 1102 } 1103 break; 1104 } 1105 } 1106 /// 1107 /// Send multipart for request. 1108 /// You would like to use this method for sending large portions of mixed data or uploading files to forms. 1109 /// 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) 1110 /// Params: 1111 /// url = url 1112 /// sources = array of sources. 1113 HTTPResponse exec(string method="POST")(string url, MultipartForm sources) { 1114 import std.uuid; 1115 import std.file; 1116 1117 checkURL(url); 1118 if ( _cm is null ) { 1119 _cm = new ConnManager(); 1120 } 1121 1122 NetworkStream _stream; 1123 _method = method; 1124 _response = new HTTPResponse; 1125 _response.uri = _uri; 1126 _response.finalURI = _uri; 1127 bool restartedRequest = false; 1128 1129 connect: 1130 _contentReceived = 0; 1131 _response._startedAt = Clock.currTime; 1132 1133 assert(_stream is null); 1134 1135 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1136 1137 if ( _stream is null ) { 1138 debug(requests) trace("create new connection"); 1139 _stream = setupConnection(); 1140 } else { 1141 debug(requests) trace("reuse old connection"); 1142 } 1143 1144 assert(_stream !is null); 1145 1146 if ( !_stream.isConnected ) { 1147 debug(requests) trace("disconnected stream on enter"); 1148 if ( !restartedRequest ) { 1149 debug(requests) trace("disconnected stream on enter: retry"); 1150 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1151 1152 _cm.del(_uri.scheme, _uri.host, _uri.port); 1153 _stream.close(); 1154 _stream = null; 1155 1156 restartedRequest = true; 1157 goto connect; 1158 } 1159 debug(requests) trace("disconnected stream on enter: return response"); 1160 //_stream = null; 1161 return _response; 1162 } 1163 _response._connectedAt = Clock.currTime; 1164 1165 Appender!string req; 1166 req.put(requestString()); 1167 1168 string boundary = randomUUID().toString; 1169 string[] partHeaders; 1170 size_t contentLength; 1171 1172 foreach(ref part; sources._sources) { 1173 string h = "--" ~ boundary ~ "\r\n"; 1174 string disposition = `form-data; name="%s"`.format(part.name); 1175 string optionals = part. 1176 parameters.byKeyValue(). 1177 filter!(p => p.key!="Content-Type"). 1178 map! (p => "%s=%s".format(p.key, p.value)). 1179 join("; "); 1180 1181 h ~= `Content-Disposition: ` ~ [disposition, optionals].join("; ") ~ "\r\n"; 1182 1183 auto contentType = "Content-Type" in part.parameters; 1184 if ( contentType ) { 1185 h ~= "Content-Type: " ~ *contentType ~ "\r\n"; 1186 } 1187 1188 h ~= "\r\n"; 1189 partHeaders ~= h; 1190 contentLength += h.length + part.input.getSize() + "\r\n".length; 1191 } 1192 contentLength += "--".length + boundary.length + "--\r\n".length; 1193 1194 auto h = requestHeaders(); 1195 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "multipart/form-data; boundary=" ~ boundary); 1196 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(contentLength)); 1197 1198 h.byKeyValue. 1199 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1200 each!(h => req.put(h)); 1201 req.put("\r\n"); 1202 1203 debug(requests) trace(req.data); 1204 if ( _verbosity >= 1 ) req.data.splitLines.each!(a => writeln("> " ~ a)); 1205 1206 try { 1207 _stream.send(req.data()); 1208 foreach(ref source; sources._sources) { 1209 debug(requests) tracef("sending part headers <%s>", partHeaders.front); 1210 _stream.send(partHeaders.front); 1211 partHeaders.popFront; 1212 while (true) { 1213 auto chunk = source.input.read(); 1214 if ( chunk.length <= 0 ) { 1215 break; 1216 } 1217 _stream.send(chunk); 1218 } 1219 _stream.send("\r\n"); 1220 } 1221 _stream.send("--" ~ boundary ~ "--\r\n"); 1222 _response._requestSentAt = Clock.currTime; 1223 receiveResponse(_stream); 1224 _response._finishedAt = Clock.currTime; 1225 } 1226 catch (NetworkException e) { 1227 errorf("Error sending request: ", e.msg); 1228 _stream.close(); 1229 return _response; 1230 } 1231 1232 if ( serverPrematurelyClosedConnection() 1233 && !restartedRequest 1234 && isIdempotent(_method) 1235 ) { 1236 /// 1237 /// We didn't receive any data (keepalive connectioin closed?) 1238 /// and we can restart this request. 1239 /// Go ahead. 1240 /// 1241 debug(requests) tracef("Server closed keepalive connection"); 1242 1243 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1244 1245 _cm.del(_uri.scheme, _uri.host, _uri.port); 1246 _stream.close(); 1247 _stream = null; 1248 1249 restartedRequest = true; 1250 goto connect; 1251 } 1252 1253 if ( _useStreaming ) { 1254 if ( _response._receiveAsRange.activated ) { 1255 debug(requests) trace("streaming_in activated"); 1256 return _response; 1257 } else { 1258 // this can happen if whole response body received together with headers 1259 _response._receiveAsRange.data = _response.responseBody.data; 1260 } 1261 } 1262 1263 close_connection_if_not_keepalive(_stream); 1264 1265 if ( _verbosity >= 1 ) { 1266 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1267 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1268 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1269 } 1270 1271 if ( willFollowRedirect ) { 1272 if ( _history.length >= _maxRedirects ) { 1273 _stream = null; 1274 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 1275 } 1276 // "location" in response already checked in canFollowRedirect 1277 immutable new_location = *("location" in _response.responseHeaders); 1278 immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location); 1279 1280 // save current response for history 1281 _history ~= _response; 1282 1283 // prepare new response (for redirected request) 1284 _response = new HTTPResponse; 1285 _response.uri = current_uri; 1286 _response.finalURI = next_uri; 1287 _stream = null; 1288 1289 // set new uri 1290 this._uri = next_uri; 1291 debug(requests) tracef("Redirected to %s", next_uri); 1292 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1293 // 307 and 308 do not change method 1294 return this.get(); 1295 } 1296 if ( restartedRequest ) { 1297 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 1298 restartedRequest = false; 1299 } 1300 goto connect; 1301 } 1302 1303 _response._history = _history; 1304 return _response; 1305 } 1306 1307 // we use this if we send from ubyte[][] and user provided Content-Length 1308 private void sendFlattenContent(T)(NetworkStream _stream, T content) { 1309 while ( !content.empty ) { 1310 auto chunk = content.front; 1311 _stream.send(chunk); 1312 content.popFront; 1313 } 1314 debug(requests) tracef("sent"); 1315 } 1316 // we use this if we send from ubyte[][] as chunked content 1317 private void sendChunkedContent(T)(NetworkStream _stream, T content) { 1318 while ( !content.empty ) { 1319 auto chunk = content.front; 1320 auto chunkHeader = "%x\r\n".format(chunk.length); 1321 debug(requests) tracef("sending %s%s", chunkHeader, chunk); 1322 _stream.send(chunkHeader); 1323 _stream.send(chunk); 1324 _stream.send("\r\n"); 1325 content.popFront; 1326 } 1327 debug(requests) tracef("sent"); 1328 _stream.send("0\r\n\r\n"); 1329 } 1330 /// 1331 /// POST/PUT/... data from some string(with Content-Length), or from range of strings/bytes (use Transfer-Encoding: chunked). 1332 /// 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. 1333 /// 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. 1334 /// 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. 1335 /// 1336 /// Parameters: 1337 /// url = url 1338 /// content = string or input range 1339 /// contentType = content type 1340 /// Returns: 1341 /// Response 1342 /// Examples: 1343 /// --------------------------------------------------------------------------------------------------------- 1344 /// rs = rq.exec!"POST"("http://httpbin.org/post", "привiт, свiт!", "application/octet-stream"); 1345 /// 1346 /// auto s = lineSplitter("one,\ntwo,\nthree."); 1347 /// rs = rq.exec!"POST"("http://httpbin.org/post", s, "application/octet-stream"); 1348 /// 1349 /// auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 1350 /// rs = rq.exec!"POST"("http://httpbin.org/post", s.representation.chunks(10), "application/octet-stream"); 1351 /// 1352 /// auto f = File("tests/test.txt", "rb"); 1353 /// rs = rq.exec!"POST"("http://httpbin.org/post", f.byChunk(3), "application/octet-stream"); 1354 /// -------------------------------------------------------------------------------------------------------- 1355 HTTPResponse exec(string method="POST", R)(string url, R content, string contentType="application/octet-stream") 1356 if ( (rank!R == 1) 1357 || (rank!R == 2 && isSomeChar!(Unqual!(typeof(content.front.front)))) 1358 || (rank!R == 2 && (is(Unqual!(typeof(content.front.front)) == ubyte))) 1359 ) 1360 do { 1361 debug(requests) tracef("started url=%s, this._uri=%s", url, _uri); 1362 1363 checkURL(url); 1364 if ( _cm is null ) { 1365 _cm = new ConnManager(); 1366 } 1367 1368 NetworkStream _stream; 1369 _method = method; 1370 _response = new HTTPResponse; 1371 _history.length = 0; 1372 _response.uri = _uri; 1373 _response.finalURI = _uri; 1374 bool restartedRequest = false; 1375 bool send_flat; 1376 1377 connect: 1378 _contentReceived = 0; 1379 _response._startedAt = Clock.currTime; 1380 1381 assert(_stream is null); 1382 1383 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1384 1385 if ( _stream is null ) { 1386 debug(requests) trace("create new connection"); 1387 _stream = setupConnection(); 1388 } else { 1389 debug(requests) trace("reuse old connection"); 1390 } 1391 1392 assert(_stream !is null); 1393 1394 if ( !_stream.isConnected ) { 1395 debug(requests) trace("disconnected stream on enter"); 1396 if ( !restartedRequest ) { 1397 debug(requests) trace("disconnected stream on enter: retry"); 1398 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1399 1400 _cm.del(_uri.scheme, _uri.host, _uri.port); 1401 _stream.close(); 1402 _stream = null; 1403 1404 restartedRequest = true; 1405 goto connect; 1406 } 1407 debug(requests) trace("disconnected stream on enter: return response"); 1408 //_stream = null; 1409 return _response; 1410 } 1411 _response._connectedAt = Clock.currTime; 1412 1413 Appender!string req; 1414 req.put(requestString()); 1415 1416 auto h = requestHeaders; 1417 if ( contentType ) { 1418 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", contentType); 1419 } 1420 static if ( rank!R == 1 ) { 1421 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(content.length)); 1422 } else { 1423 if ( _userHeaders.ContentLength ) { 1424 debug(requests) tracef("User provided content-length for chunked content"); 1425 send_flat = true; 1426 } else { 1427 h["Transfer-Encoding"] = "chunked"; 1428 send_flat = false; 1429 } 1430 } 1431 h.byKeyValue. 1432 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1433 each!(h => req.put(h)); 1434 req.put("\r\n"); 1435 1436 debug(requests) trace(req.data); 1437 if ( _verbosity >= 1 ) { 1438 req.data.splitLines.each!(a => writeln("> " ~ a)); 1439 } 1440 1441 try { 1442 // send headers 1443 _stream.send(req.data()); 1444 // send body 1445 static if ( rank!R == 1) { 1446 _stream.send(content); 1447 } else { 1448 if ( send_flat ) { 1449 sendFlattenContent(_stream, content); 1450 } else { 1451 sendChunkedContent(_stream, content); 1452 } 1453 } 1454 _response._requestSentAt = Clock.currTime; 1455 debug(requests) trace("starting receive response"); 1456 receiveResponse(_stream); 1457 debug(requests) trace("finished receive response"); 1458 _response._finishedAt = Clock.currTime; 1459 } catch (NetworkException e) { 1460 _stream.close(); 1461 throw new RequestException("Network error during data exchange"); 1462 } 1463 1464 if ( serverPrematurelyClosedConnection() 1465 && !restartedRequest 1466 && isIdempotent(_method) 1467 ) { 1468 /// 1469 /// We didn't receive any data (keepalive connectioin closed?) 1470 /// and we can restart this request. 1471 /// Go ahead. 1472 /// 1473 debug(requests) tracef("Server closed keepalive connection"); 1474 1475 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1476 1477 _cm.del(_uri.scheme, _uri.host, _uri.port); 1478 _stream.close(); 1479 _stream = null; 1480 1481 restartedRequest = true; 1482 goto connect; 1483 } 1484 1485 if ( _useStreaming ) { 1486 if ( _response._receiveAsRange.activated ) { 1487 debug(requests) trace("streaming_in activated"); 1488 return _response; 1489 } else { 1490 // this can happen if whole response body received together with headers 1491 _response._receiveAsRange.data = _response.responseBody.data; 1492 } 1493 } 1494 1495 close_connection_if_not_keepalive(_stream); 1496 1497 if ( _verbosity >= 1 ) { 1498 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1499 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1500 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1501 } 1502 1503 1504 if ( willFollowRedirect ) { 1505 if ( _history.length >= _maxRedirects ) { 1506 _stream = null; 1507 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 1508 } 1509 // "location" in response already checked in canFollowRedirect 1510 immutable new_location = *("location" in _response.responseHeaders); 1511 immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location); 1512 1513 // save current response for history 1514 _history ~= _response; 1515 1516 // prepare new response (for redirected request) 1517 _response = new HTTPResponse; 1518 _response.uri = current_uri; 1519 _response.finalURI = next_uri; 1520 1521 _stream = null; 1522 1523 // set new uri 1524 this._uri = next_uri; 1525 debug(requests) tracef("Redirected to %s", next_uri); 1526 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1527 // 307 and 308 do not change method 1528 return this.get(); 1529 } 1530 if ( restartedRequest ) { 1531 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 1532 restartedRequest = false; 1533 } 1534 goto connect; 1535 } 1536 1537 _response._history = _history; 1538 return _response; 1539 } 1540 /// 1541 /// Send request with parameters. 1542 /// If used for POST or PUT requests then application/x-www-form-urlencoded used. 1543 /// Request parameters will be encoded into request string or placed in request body for POST/PUT 1544 /// requests. 1545 /// Parameters: 1546 /// url = url 1547 /// params = request parameters 1548 /// Returns: 1549 /// Response 1550 /// Examples: 1551 /// --------------------------------------------------------------------------------- 1552 /// rs = Request().exec!"GET"("http://httpbin.org/get", ["c":"d", "a":"b"]); 1553 /// --------------------------------------------------------------------------------- 1554 /// 1555 HTTPResponse exec(string method="GET")(string url = null, QueryParam[] params = null) 1556 do { 1557 debug(requests) tracef("started url=%s, this._uri=%s", url, _uri); 1558 1559 checkURL(url); 1560 if ( _cm is null ) { 1561 _cm = new ConnManager(); 1562 } 1563 1564 NetworkStream _stream; 1565 _method = method; 1566 _response = new HTTPResponse; 1567 _history.length = 0; 1568 _response.uri = _uri; 1569 _response.finalURI = _uri; 1570 bool restartedRequest = false; // True if this is restarted keepAlive request 1571 1572 connect: 1573 if ( _method == "GET" && _uri in _permanent_redirects ) { 1574 debug(requests) trace("use parmanent redirects cache"); 1575 _uri = uriFromLocation(_uri, _permanent_redirects[_uri]); 1576 _response._finalURI = _uri; 1577 } 1578 _contentReceived = 0; 1579 _response._startedAt = Clock.currTime; 1580 1581 assert(_stream is null); 1582 1583 _stream = _cm.get(_uri.scheme, _uri.host, _uri.port); 1584 1585 if ( _stream is null ) { 1586 debug(requests) trace("create new connection"); 1587 _stream = setupConnection(); 1588 } else { 1589 debug(requests) trace("reuse old connection"); 1590 } 1591 1592 assert(_stream !is null); 1593 1594 if ( !_stream.isConnected ) { 1595 debug(requests) trace("disconnected stream on enter"); 1596 if ( !restartedRequest ) { 1597 debug(requests) trace("disconnected stream on enter: retry"); 1598 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1599 1600 _cm.del(_uri.scheme, _uri.host, _uri.port); 1601 _stream.close(); 1602 _stream = null; 1603 1604 restartedRequest = true; 1605 goto connect; 1606 } 1607 debug(requests) trace("disconnected stream on enter: return response"); 1608 //_stream = null; 1609 return _response; 1610 } 1611 _response._connectedAt = Clock.currTime; 1612 1613 auto h = requestHeaders(); 1614 1615 Appender!string req; 1616 1617 string encoded; 1618 1619 switch (_method) { 1620 case "POST","PUT": 1621 encoded = params2query(params); 1622 safeSetHeader(h, _userHeaders.ContentType, "Content-Type", "application/x-www-form-urlencoded"); 1623 if ( encoded.length > 0) { 1624 safeSetHeader(h, _userHeaders.ContentLength, "Content-Length", to!string(encoded.length)); 1625 } 1626 req.put(requestString()); 1627 break; 1628 default: 1629 req.put(requestString(params)); 1630 } 1631 1632 h.byKeyValue. 1633 map!(kv => kv.key ~ ": " ~ kv.value ~ "\r\n"). 1634 each!(h => req.put(h)); 1635 req.put("\r\n"); 1636 if ( encoded ) { 1637 req.put(encoded); 1638 } 1639 1640 debug(requests) trace(req.data); 1641 if ( _verbosity >= 1 ) { 1642 req.data.splitLines.each!(a => writeln("> " ~ a)); 1643 } 1644 // 1645 // Now send request and receive response 1646 // 1647 try { 1648 _stream.send(req.data()); 1649 _response._requestSentAt = Clock.currTime; 1650 debug(requests) trace("starting receive response"); 1651 receiveResponse(_stream); 1652 debug(requests) trace("done receive response"); 1653 _response._finishedAt = Clock.currTime; 1654 } 1655 catch (NetworkException e) { 1656 // On SEND this can means: 1657 // we started to send request to the server, but it closed connection because of keepalive timeout. 1658 // We have to restart request if possible. 1659 1660 // On RECEIVE - if we received something - then this exception is real and unexpected error. 1661 // If we didn't receive anything - we can restart request again as it can be 1662 debug(requests) tracef("Exception on receive response: %s", e.msg); 1663 if ( _response._responseHeaders.length != 0 ) 1664 { 1665 _stream.close(); 1666 throw new RequestException("Unexpected network error"); 1667 } 1668 } 1669 1670 if ( serverPrematurelyClosedConnection() 1671 && !restartedRequest 1672 && isIdempotent(_method) 1673 ) { 1674 /// 1675 /// We didn't receive any data (keepalive connectioin closed?) 1676 /// and we can restart this request. 1677 /// Go ahead. 1678 /// 1679 debug(requests) tracef("Server closed keepalive connection"); 1680 1681 assert(_cm.get(_uri.scheme, _uri.host, _uri.port) == _stream); 1682 1683 _cm.del(_uri.scheme, _uri.host, _uri.port); 1684 _stream.close(); 1685 _stream = null; 1686 1687 restartedRequest = true; 1688 goto connect; 1689 } 1690 1691 if ( _useStreaming ) { 1692 if ( _response._receiveAsRange.activated ) { 1693 debug(requests) trace("streaming_in activated"); 1694 return _response; 1695 } else { 1696 // this can happen if whole response body received together with headers 1697 _response._receiveAsRange.data = _response.responseBody.data; 1698 } 1699 } 1700 1701 close_connection_if_not_keepalive(_stream); 1702 1703 if ( _verbosity >= 1 ) { 1704 writeln(">> Connect time: ", _response._connectedAt - _response._startedAt); 1705 writeln(">> Request send time: ", _response._requestSentAt - _response._connectedAt); 1706 writeln(">> Response recv time: ", _response._finishedAt - _response._requestSentAt); 1707 } 1708 1709 if ( willFollowRedirect ) { 1710 debug(requests) trace("going to follow redirect"); 1711 if ( _history.length >= _maxRedirects ) { 1712 _stream = null; 1713 throw new MaxRedirectsException("%d redirects reached maxRedirects %d.".format(_history.length, _maxRedirects)); 1714 } 1715 // "location" in response already checked in canFollowRedirect 1716 immutable new_location = *("location" in _response.responseHeaders); 1717 immutable current_uri = _uri, next_uri = uriFromLocation(_uri, new_location); 1718 1719 if ( _method == "GET" && _response.code == 301 ) { 1720 _permanent_redirects[_uri] = new_location; 1721 } 1722 1723 // save current response for history 1724 _history ~= _response; 1725 1726 // prepare new response (for redirected request) 1727 _response = new HTTPResponse; 1728 _response.uri = current_uri; 1729 _response.finalURI = next_uri; 1730 _stream = null; 1731 1732 // set new uri 1733 _uri = next_uri; 1734 debug(requests) tracef("Redirected to %s", next_uri); 1735 if ( _method != "GET" && _response.code != 307 && _response.code != 308 ) { 1736 // 307 and 308 do not change method 1737 return this.get(); 1738 } 1739 if ( restartedRequest ) { 1740 debug(requests) trace("Rare event: clearing 'restartedRequest' on redirect"); 1741 restartedRequest = false; 1742 } 1743 goto connect; 1744 } 1745 1746 _response._history = _history; 1747 return _response; 1748 } 1749 1750 /// WRAPPERS 1751 /// 1752 /// send file(s) using POST and multipart form. 1753 /// This wrapper will be deprecated, use post with MultipartForm - it is more general and clear. 1754 /// Parameters: 1755 /// url = url 1756 /// files = array of PostFile structures 1757 /// Returns: 1758 /// Response 1759 /// Each PostFile structure contain path to file, and optional field name and content type. 1760 /// If no field name provided, then basename of the file will be used. 1761 /// application/octet-stream is default when no content type provided. 1762 /// Example: 1763 /// --------------------------------------------------------------- 1764 /// PostFile[] files = [ 1765 /// {fileName:"tests/abc.txt", fieldName:"abc", contentType:"application/octet-stream"}, 1766 /// {fileName:"tests/test.txt"} 1767 /// ]; 1768 /// rs = rq.exec!"POST"("http://httpbin.org/post", files); 1769 /// --------------------------------------------------------------- 1770 /// 1771 HTTPResponse exec(string method="POST")(string url, PostFile[] files) if (method=="POST") { 1772 MultipartForm multipart; 1773 File[] toClose; 1774 foreach(ref f; files) { 1775 File file = File(f.fileName, "rb"); 1776 toClose ~= file; 1777 string fileName = f.fileName ? f.fileName : f.fieldName; 1778 string contentType = f.contentType ? f.contentType : "application/octetstream"; 1779 multipart.add(f.fieldName, new FormDataFile(file), ["filename":fileName, "Content-Type": contentType]); 1780 } 1781 auto res = exec!"POST"(url, multipart); 1782 toClose.each!"a.close"; 1783 return res; 1784 } 1785 /// 1786 /// exec request with parameters when you can use dictionary (when you have no duplicates in parameter names) 1787 /// Consider switch to exec(url, QueryParams) as it more generic and clear. 1788 /// Parameters: 1789 /// url = url 1790 /// params = dictionary with field names as keys and field values as values. 1791 /// Returns: 1792 /// Response 1793 HTTPResponse exec(string method="GET")(string url, string[string] params) { 1794 return exec!method(url, params.byKeyValue.map!(p => QueryParam(p.key, p.value)).array); 1795 } 1796 /// 1797 /// GET request. Simple wrapper over exec!"GET" 1798 /// Params: 1799 /// args = request parameters. see exec docs. 1800 /// 1801 HTTPResponse get(A...)(A args) { 1802 return exec!"GET"(args); 1803 } 1804 /// 1805 /// POST request. Simple wrapper over exec!"POST" 1806 /// Params: 1807 /// uri = endpoint uri 1808 /// args = request parameters. see exec docs. 1809 /// 1810 HTTPResponse post(A...)(string uri, A args) { 1811 return exec!"POST"(uri, args); 1812 } 1813 } 1814 1815 version(vibeD) { 1816 import std.json; 1817 package string httpTestServer() { 1818 return "http://httpbin.org/"; 1819 } 1820 package string fromJsonArrayToStr(JSONValue v) { 1821 return v.str; 1822 } 1823 } 1824 else { 1825 import std.json; 1826 package string httpTestServer() { 1827 return "http://127.0.0.1:8081/"; 1828 } 1829 package string fromJsonArrayToStr(JSONValue v) { 1830 return cast(string)(v.array.map!"cast(ubyte)a.integer".array); 1831 } 1832 } 1833 1834 1835 package unittest { 1836 import std.json; 1837 import std.array; 1838 1839 globalLogLevel(LogLevel.info); 1840 1841 string httpbinUrl = httpTestServer(); 1842 version(vibeD) { 1843 } 1844 else { 1845 import httpbin; 1846 auto server = httpbinApp(); 1847 server.start(); 1848 scope(exit) { 1849 server.stop(); 1850 } 1851 } 1852 HTTPRequest rq; 1853 HTTPResponse rs; 1854 info("Check GET"); 1855 URI uri = URI(httpbinUrl); 1856 rs = rq.get(httpbinUrl); 1857 assert(rs.code==200); 1858 assert(rs.responseBody.length > 0); 1859 assert(rq.format("%m|%h|%p|%P|%q|%U") == 1860 "GET|%s|%d|%s||%s" 1861 .format(uri.host, uri.port, uri.path, httpbinUrl)); 1862 info("Check GET with AA params"); 1863 { 1864 rs = rq.get(httpbinUrl ~ "get", ["c":" d", "a":"b"]); 1865 assert(rs.code == 200); 1866 auto json = parseJSON(cast(string)rs.responseBody.data).object["args"].object; 1867 assert(json["c"].str == " d"); 1868 assert(json["a"].str == "b"); 1869 } 1870 rq.keepAlive = false; // disable keepalive on non-idempotent requests 1871 info("Check POST files"); 1872 { 1873 import std.file; 1874 import std.path; 1875 auto tmpd = tempDir(); 1876 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 1877 auto f = File(tmpfname, "wb"); 1878 f.rawWrite("abcdefgh\n12345678\n"); 1879 f.close(); 1880 // files 1881 PostFile[] files = [ 1882 {fileName: tmpfname, fieldName:"abc", contentType:"application/octet-stream"}, 1883 {fileName: tmpfname} 1884 ]; 1885 rs = rq.post(httpbinUrl ~ "post", files); 1886 assert(rs.code==200); 1887 } 1888 info("Check POST chunked from file.byChunk"); 1889 { 1890 import std.file; 1891 import std.path; 1892 auto tmpd = tempDir(); 1893 auto tmpfname = tmpd ~ dirSeparator ~ "request_test.txt"; 1894 auto f = File(tmpfname, "wb"); 1895 f.rawWrite("abcdefgh\n12345678\n"); 1896 f.close(); 1897 f = File(tmpfname, "rb"); 1898 rs = rq.post(httpbinUrl ~ "post", f.byChunk(3), "application/octet-stream"); 1899 if (httpbinUrl != "http://httpbin.org/") { 1900 assert(rs.code==200); 1901 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]); 1902 assert(data=="abcdefgh\n12345678\n"); 1903 } 1904 f.close(); 1905 } 1906 info("Check POST chunked from lineSplitter"); 1907 { 1908 auto s = lineSplitter("one,\ntwo,\nthree."); 1909 rs = rq.exec!"POST"(httpbinUrl ~ "post", s, "application/octet-stream"); 1910 if (httpbinUrl != "http://httpbin.org/") { 1911 assert(rs.code==200); 1912 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]); 1913 assert(data=="one,two,three."); 1914 } 1915 } 1916 info("Check POST chunked from array"); 1917 { 1918 auto s = ["one,", "two,", "three."]; 1919 rs = rq.post(httpbinUrl ~ "post", s, "application/octet-stream"); 1920 if (httpbinUrl != "http://httpbin.org/") { 1921 assert(rs.code==200); 1922 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody).object["data"]); 1923 assert(data=="one,two,three."); 1924 } 1925 } 1926 info("Check POST chunked using std.range.chunks()"); 1927 { 1928 auto s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 1929 rs = rq.post(httpbinUrl ~ "post", s.representation.chunks(10), "application/octet-stream"); 1930 if (httpbinUrl != "http://httpbin.org/") { 1931 assert(rs.code==200); 1932 auto data = fromJsonArrayToStr(parseJSON(cast(string)rs.responseBody.data).object["data"]); 1933 assert(data==s); 1934 } 1935 } 1936 info("Check POST from QueryParams"); 1937 { 1938 rs = rq.post(httpbinUrl ~ "post", queryParams("name[]", "first", "name[]", 2)); 1939 assert(rs.code==200); 1940 auto data = parseJSON(cast(string)rs.responseBody).object["form"].object; 1941 string[] a; 1942 try { 1943 a = to!(string[])(data["name[]"].str); 1944 } 1945 catch (JSONException e) { 1946 a = data["name[]"].array.map!"a.str".array; 1947 } 1948 assert(equal(["first", "2"], a)); 1949 } 1950 info("Check POST from AA"); 1951 { 1952 rs = rq.post(httpbinUrl ~ "post", ["a":"b ", "c":"d"]); 1953 assert(rs.code==200); 1954 auto form = parseJSON(cast(string)rs.responseBody.data).object["form"].object; 1955 assert(form["a"].str == "b "); 1956 assert(form["c"].str == "d"); 1957 } 1958 info("Check POST json"); 1959 { 1960 rs = rq.post(httpbinUrl ~ "post?b=x", `{"a":"a b", "c":[1,2,3]}`, "application/json"); 1961 assert(rs.code==200); 1962 auto json = parseJSON(cast(string)rs.responseBody).object["args"].object; 1963 assert(json["b"].str == "x"); 1964 json = parseJSON(cast(string)rs.responseBody).object["json"].object; 1965 assert(json["a"].str == "a b"); 1966 assert(json["c"].array.map!(a=>a.integer).array == [1,2,3]); 1967 } 1968 info("Check DELETE"); 1969 rs = rq.exec!"DELETE"(httpbinUrl ~ "delete"); 1970 assert(rs.code==200); 1971 info("Check PUT"); 1972 rs = rq.exec!"PUT"(httpbinUrl ~ "put", `{"a":"b", "c":[1,2,3]}`, "application/json"); 1973 assert(rs.code==200); 1974 assert(parseJSON(cast(string)rs.responseBody).object["json"].object["a"].str=="b"); 1975 info("Check PATCH"); 1976 rs = rq.exec!"PATCH"(httpbinUrl ~ "patch", "привiт, свiт!", "application/octet-stream"); 1977 assert(rs.code==200); 1978 info("Check HEAD"); 1979 rs = rq.exec!"HEAD"(httpbinUrl); 1980 assert(rs.code==200); 1981 1982 rq._keepAlive = true; 1983 info("Check compressed content"); 1984 rs = rq.get(httpbinUrl ~ "gzip"); 1985 assert(rs.code==200); 1986 bool gzipped = parseJSON(cast(string)rs.responseBody).object["gzipped"].type == JSON_TYPE.TRUE; 1987 assert(gzipped); 1988 info("gzip - ok"); 1989 rs = rq.get(httpbinUrl ~ "deflate"); 1990 assert(rs.code==200); 1991 bool deflated = parseJSON(cast(string)rs.responseBody).object["deflated"].type == JSON_TYPE.TRUE; 1992 assert(deflated); 1993 info("deflate - ok"); 1994 1995 info("Check redirects"); 1996 rs = rq.get(httpbinUrl ~ "relative-redirect/2"); 1997 assert(rs.history.length == 2); 1998 assert(rs.code==200); 1999 rs = rq.get(httpbinUrl ~ "absolute-redirect/2"); 2000 assert(rs.history.length == 2); 2001 assert(rs.code==200); 2002 2003 rq.maxRedirects = 2; 2004 assertThrown!MaxRedirectsException(rq.get(httpbinUrl ~ "absolute-redirect/3")); 2005 2006 rq.maxRedirects = 0; 2007 rs = rq.get(httpbinUrl ~ "absolute-redirect/1"); 2008 assert(rs.code==302); 2009 2010 info("Check cookie"); 2011 { 2012 rq.maxRedirects = 10; 2013 rs = rq.get(httpbinUrl ~ "cookies/set?A=abcd&b=cdef"); 2014 assert(rs.code == 200); 2015 auto json = parseJSON(cast(string)rs.responseBody.data).object["cookies"].object; 2016 assert(json["A"].str == "abcd"); 2017 assert(json["b"].str == "cdef"); 2018 foreach(c; rq.cookie) { 2019 final switch(c.attr) { 2020 case "A": 2021 assert(c.value == "abcd"); 2022 break; 2023 case "b": 2024 assert(c.value == "cdef"); 2025 break; 2026 } 2027 } 2028 } 2029 info("Check chunked content"); 2030 rs = rq.get(httpbinUrl ~ "range/1024"); 2031 assert(rs.code==200); 2032 assert(rs.responseBody.length==1024); 2033 2034 info("Check basic auth"); 2035 rq.authenticator = new BasicAuthentication("user", "passwd"); 2036 rs = rq.get(httpbinUrl ~ "basic-auth/user/passwd"); 2037 assert(rs.code==200); 2038 2039 info("Check limits"); 2040 rq = HTTPRequest(); 2041 rq.maxContentLength = 1; 2042 assertThrown!RequestException(rq.get(httpbinUrl)); 2043 2044 rq = HTTPRequest(); 2045 rq.maxHeadersLength = 1; 2046 assertThrown!RequestException(rq.get(httpbinUrl)); 2047 2048 rq = HTTPRequest(); 2049 info("Check POST multiPartForm"); 2050 { 2051 /// This is example on usage files with MultipartForm data. 2052 /// For this example we have to create files which will be sent. 2053 import std.file; 2054 import std.path; 2055 /// preapare files 2056 auto tmpd = tempDir(); 2057 auto tmpfname1 = tmpd ~ dirSeparator ~ "request_test1.txt"; 2058 auto f = File(tmpfname1, "wb"); 2059 f.rawWrite("file1 content\n"); 2060 f.close(); 2061 auto tmpfname2 = tmpd ~ dirSeparator ~ "request_test2.txt"; 2062 f = File(tmpfname2, "wb"); 2063 f.rawWrite("file2 content\n"); 2064 f.close(); 2065 /// 2066 /// Ok, files ready. 2067 /// Now we will prepare Form data 2068 /// 2069 File f1 = File(tmpfname1, "rb"); 2070 File f2 = File(tmpfname2, "rb"); 2071 scope(exit) { 2072 f1.close(); 2073 f2.close(); 2074 } 2075 /// 2076 /// for each part we have to set field name, source (ubyte array or opened file) and optional filename and content-type 2077 /// 2078 MultipartForm mForm = MultipartForm(). 2079 add(formData("Field1", cast(ubyte[])"form field from memory")). 2080 add(formData("Field2", cast(ubyte[])"file field from memory", ["filename":"data2"])). 2081 add(formData("File1", f1, ["filename":"file1", "Content-Type": "application/octet-stream"])). 2082 add(formData("File2", f2, ["filename":"file2", "Content-Type": "application/octet-stream"])); 2083 /// everything ready, send request 2084 rs = rq.post(httpbinUrl ~ "post", mForm); 2085 } 2086 info("Check exception handling, error messages and timeous are OK"); 2087 rq.timeout = 1.seconds; 2088 assertThrown!TimeoutException(rq.get(httpbinUrl ~ "delay/3")); 2089 // assertThrown!ConnectError(rq.get("http://0.0.0.0:65000/")); 2090 // assertThrown!ConnectError(rq.get("http://1.1.1.1/")); 2091 // assertThrown!ConnectError(rq.get("http://gkhgkhgkjhgjhgfjhgfjhgf/")); 2092 } 2093