1 module hunt.httpclient.Request; 2 3 import hunt.Functions; 4 import hunt.http.client; 5 import hunt.httpclient.Response : Response; 6 import hunt.logging.ConsoleLogger; 7 import hunt.util.MimeTypeUtils; 8 9 import std.json; 10 import std.range; 11 import core.thread; 12 import core.time; 13 14 private alias HuntHttpClient = hunt.http.client.HttpClient.HttpClient; 15 16 private struct PendingFile { 17 string name; 18 // const(ubyte)[] contents; 19 string filename; 20 string[string] headers; 21 } 22 23 private struct BodyFormat { 24 enum string JSON = "json"; 25 enum string FormParams = "form_params"; 26 enum string Multipart = "multipart"; 27 } 28 29 /** 30 * 31 */ 32 class Request { 33 34 private string _contentType; // = MimeType.TEXT_PLAIN_VALUE; 35 private string[string] _headers; 36 private Duration _timeout; 37 private PendingFile[] _pendingFiles; 38 private string _bodyFormat; 39 private MimeTypeUtils _mimeUtil = new MimeTypeUtils(); 40 private string[string] _formData; 41 42 /** 43 * The number of times to try the request. 44 */ 45 private int _tries = 1; 46 47 /** 48 * The number of milliseconds to wait between retries. 49 */ 50 private Duration _retryDelay = 100.msecs; 51 52 this() { 53 } 54 55 /** 56 * Indicate the request contains JSON. 57 */ 58 Request asJson() { 59 _bodyFormat = BodyFormat.JSON; 60 contentType(MimeType.APPLICATION_JSON_VALUE); 61 return this; 62 } 63 64 /** 65 * Indicate the request contains form parameters. 66 */ 67 Request asForm() { 68 _bodyFormat = BodyFormat.FormParams; 69 contentType(MimeType.APPLICATION_X_WWW_FORM_VALUE); 70 return this; 71 } 72 73 /** 74 * Specify the body format of the request. 75 */ 76 Request bodyFormat(string value) { 77 _bodyFormat = value; 78 return this; 79 } 80 81 /** 82 * Indicate the request is a multi-part form request. 83 */ 84 Request asMultipart() { 85 _bodyFormat = BodyFormat.Multipart; 86 return this; 87 } 88 89 Request attach(string name, string filename = null, string[string] headers = null) { 90 _pendingFiles ~= PendingFile(name, filename, headers); 91 asMultipart(); 92 return this; 93 } 94 95 Request formData(string[string] data) { 96 _formData = data; 97 asMultipart(); 98 return this; 99 } 100 101 /** 102 * Indicate the type of content that should be returned by the server. 103 */ 104 Request accept(string contentType) { 105 _headers[HttpHeader.ACCEPT.toString()] = contentType; 106 return this; 107 } 108 109 /** 110 * Indicate that JSON should be returned by the server. 111 */ 112 Request acceptJson(string contentType) { 113 return accept(MimeType.APPLICATION_JSON_VALUE); 114 } 115 116 /** 117 * Specify the request's content type. 118 */ 119 Request contentType(string value) { 120 _contentType = value; 121 return this; 122 } 123 124 Request withHeaders(string[string] headers) { 125 foreach (string key, string value; headers) { 126 _headers[key] = value; 127 } 128 return this; 129 } 130 131 Request withCookies(Cookie[] cookies...) { 132 string cookie = HttpHeader.COOKIE.toString(); 133 _headers[cookie] = generateCookies(cookies); 134 return this; 135 } 136 137 /** 138 * Specify an authorization token for the request. 139 */ 140 Request withToken(string value, string type = "Bearer") { 141 _headers[HttpHeader.AUTHORIZATION.toString()] = type ~ " " ~ value; 142 return this; 143 } 144 145 Request timeout(Duration dur) { 146 _timeout = dur; 147 return this; 148 } 149 150 Request retry(int timers, Duration delay) { 151 _tries = timers; 152 _retryDelay = delay; 153 return this; 154 } 155 156 Response get(string url) { 157 Response res; 158 .retry(_tries, (int attempts) { res = doGet(url); }, _retryDelay); 159 return res; 160 } 161 162 private Response doGet(string url) { 163 HttpClientOptions options = new HttpClientOptions(); 164 if (_timeout > Duration.zero) { 165 options.getTcpConfiguration().setIdleTimeout(_timeout); 166 options.getTcpConfiguration().setConnectTimeout(_timeout); 167 } 168 169 HuntHttpClient client = new HuntHttpClient(options); 170 scope (exit) { 171 client.close(); 172 } 173 174 RequestBuilder builder = new RequestBuilder(); 175 176 if (_headers !is null) { 177 foreach (string name, string value; _headers) { 178 builder.addHeader(name, value); 179 } 180 } 181 182 HttpClientRequest request = builder.url(url).build(); 183 HttpClientResponse response = client.newCall(request).execute(); 184 185 return new Response(response); 186 } 187 188 /* #region private methods */ 189 190 private HttpBody buildBody() { 191 HttpBody httpBody; 192 if (_bodyFormat == BodyFormat.Multipart) { 193 MultipartBody.Builder builder = new MultipartBody.Builder(); 194 195 foreach (string key, string value; _formData) { 196 builder.addFormDataPart(key, value, MimeType.TEXT_PLAIN_VALUE); 197 } 198 199 foreach (ref PendingFile file; _pendingFiles) { 200 string mimeType = _mimeUtil.getMimeByExtension(file.filename); 201 if (mimeType.empty) 202 mimeType = MimeType.TEXT_PLAIN_VALUE; 203 204 builder.addFormDataPart(file.name, file.filename, 205 HttpBody.createFromFile(mimeType, file.filename)); 206 207 // TODO: Tasks pending completion -@zhangxueping at 2020-05-15T16:41:19+08:00 208 // Add headers 209 } 210 211 httpBody = builder.build(); 212 } else { 213 httpBody = HttpBody.create(_contentType, cast(const(ubyte)[]) null); 214 } 215 216 return httpBody; 217 } 218 219 private Response perform(string url, HttpBody httpBody, HttpMethod method = HttpMethod.POST) { 220 HttpClientOptions options = new HttpClientOptions(); 221 if (_timeout > Duration.zero) { 222 options.getTcpConfiguration().setIdleTimeout(_timeout); 223 options.getTcpConfiguration().setConnectTimeout(_timeout); 224 } 225 226 HuntHttpClient client = new HuntHttpClient(options); 227 scope (exit) { 228 client.close(); 229 } 230 231 RequestBuilder builder = new RequestBuilder(); 232 233 if (_headers !is null) { 234 foreach (string name, string value; _headers) { 235 builder.addHeader(name, value); 236 } 237 } 238 239 builder.url(url); 240 241 if(method == HttpMethod.POST) 242 builder.post(httpBody); 243 else if(method == HttpMethod.PUT) 244 builder.put(httpBody); 245 else if(method == HttpMethod.DELETE) 246 builder.del(httpBody); 247 else if(method == HttpMethod.PATCH) 248 builder.patch(httpBody); 249 else 250 throw new Exception("Unsupported method: " ~ method.toString()); 251 252 HttpClientRequest request = builder.build(); 253 HttpClientResponse response = client.newCall(request).execute(); 254 255 return new Response(response); 256 } 257 258 /* #endregion */ 259 260 /* #region POST */ 261 262 Response post(string url) { 263 return post(url, buildBody()); 264 } 265 266 Response post(string url, string[string] data) { 267 assert(data !is null, "No data avaliable!"); 268 269 UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm); 270 foreach (string name, string value; data) { 271 encoder.put(name, value); 272 } 273 string content = encoder.encode(); 274 if (_contentType.empty) 275 _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE; 276 return post(url, _contentType, cast(const(ubyte)[]) content); 277 } 278 279 Response post(string url, JSONValue json) { 280 if (_contentType.empty) 281 _contentType = MimeType.APPLICATION_JSON_VALUE; 282 return post(url, _contentType, cast(const(ubyte)[]) json.toString()); 283 } 284 285 Response post(string url, string contentType, const(ubyte)[] content) { 286 Response res; 287 .retry(_tries, 288 (int attempts) { 289 res = perform(url, HttpBody.create(contentType, content), HttpMethod.POST); 290 }, 291 _retryDelay); 292 return res; 293 } 294 295 Response post(string url, HttpBody httpBody) { 296 Response res; 297 .retry(_tries, 298 (int attempts) { 299 res = perform(url, httpBody, HttpMethod.POST); 300 }, 301 _retryDelay); 302 return res; 303 } 304 305 /* #endregion */ 306 307 308 /* #region PUT */ 309 310 Response put(string url) { 311 return put(url, buildBody()); 312 } 313 314 Response put(string url, string[string] data) { 315 assert(data !is null, "No data avaliable!"); 316 317 UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm); 318 foreach (string name, string value; data) { 319 encoder.put(name, value); 320 } 321 string content = encoder.encode(); 322 if (_contentType.empty) 323 _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE; 324 return put(url, _contentType, cast(const(ubyte)[]) content); 325 } 326 327 Response put(string url, JSONValue json) { 328 if (_contentType.empty) 329 _contentType = MimeType.APPLICATION_JSON_VALUE; 330 return put(url, _contentType, cast(const(ubyte)[]) json.toString()); 331 } 332 333 Response put(string url, string contentType, const(ubyte)[] content) { 334 Response res; 335 .retry(_tries, 336 (int attempts) { 337 res = perform(url, HttpBody.create(contentType, content), HttpMethod.PUT); 338 }, 339 _retryDelay); 340 return res; 341 } 342 343 Response put(string url, HttpBody httpBody) { 344 Response res; 345 .retry(_tries, 346 (int attempts) { 347 res = perform(url, httpBody, HttpMethod.PUT); 348 }, 349 _retryDelay); 350 return res; 351 } 352 353 /* #endregion */ 354 355 356 /* #region DELETE */ 357 358 Response del(string url) { 359 return del(url, buildBody()); 360 } 361 362 Response del(string url, string[string] data) { 363 assert(data !is null, "No data avaliable!"); 364 365 UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm); 366 foreach (string name, string value; data) { 367 encoder.put(name, value); 368 } 369 string content = encoder.encode(); 370 if (_contentType.empty) 371 _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE; 372 return del(url, _contentType, cast(const(ubyte)[]) content); 373 } 374 375 Response del(string url, JSONValue json) { 376 if (_contentType.empty) 377 _contentType = MimeType.APPLICATION_JSON_VALUE; 378 return del(url, _contentType, cast(const(ubyte)[]) json.toString()); 379 } 380 381 Response del(string url, string contentType, const(ubyte)[] content) { 382 Response res; 383 .retry(_tries, 384 (int attempts) { 385 res = perform(url, HttpBody.create(contentType, content), HttpMethod.DELETE); 386 }, 387 _retryDelay); 388 return res; 389 } 390 391 Response del(string url, HttpBody httpBody) { 392 Response res; 393 .retry(_tries, 394 (int attempts) { 395 res = perform(url, httpBody, HttpMethod.DELETE); 396 }, 397 _retryDelay); 398 return res; 399 } 400 401 /* #endregion */ 402 403 404 /* #region PATCH */ 405 406 Response patch(string url) { 407 return patch(url, buildBody()); 408 } 409 410 Response patch(string url, string[string] data) { 411 assert(data !is null, "No data avaliable!"); 412 413 UrlEncoded encoder = new UrlEncoded(UrlEncodeStyle.HtmlForm); 414 foreach (string name, string value; data) { 415 encoder.put(name, value); 416 } 417 string content = encoder.encode(); 418 if (_contentType.empty) 419 _contentType = MimeType.APPLICATION_X_WWW_FORM_VALUE; 420 return patch(url, _contentType, cast(const(ubyte)[]) content); 421 } 422 423 Response patch(string url, JSONValue json) { 424 if (_contentType.empty) 425 _contentType = MimeType.APPLICATION_JSON_VALUE; 426 return patch(url, _contentType, cast(const(ubyte)[]) json.toString()); 427 } 428 429 Response patch(string url, string contentType, const(ubyte)[] content) { 430 Response res; 431 .retry(_tries, 432 (int attempts) { 433 res = perform(url, HttpBody.create(contentType, content), HttpMethod.PATCH); 434 }, 435 _retryDelay); 436 return res; 437 } 438 439 Response patch(string url, HttpBody httpBody) { 440 Response res; 441 .retry(_tries, 442 (int attempts) { 443 res = perform(url, httpBody, HttpMethod.PATCH); 444 }, 445 _retryDelay); 446 return res; 447 } 448 449 /* #endregion */ 450 } 451 452 /** 453 * Retry an operation a given number of times. 454 */ 455 private void retry(int times, Action1!int handler, Duration delay = Duration.zero, 456 Func1!(Exception, bool) precondition = null) { 457 458 assert(handler !is null, "The handler can't be null"); 459 int attempts = 0; 460 461 while (true) { 462 attempts++; 463 464 try { 465 handler(attempts); 466 break; 467 } catch (Exception ex) { 468 version (HUNT_HTTP_DEBUG) 469 tracef("Retrying %d / %d", attempts, times); 470 if (attempts >= times || precondition !is null && !precondition(ex)) { 471 throw ex; 472 } 473 474 if (delay > Duration.zero) { 475 Thread.sleep(delay); 476 } 477 } 478 } 479 }