curb 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 832660904f4d95f97b0795a4e39850f2fca2c70e521a0bb795a9c551f6fdf9f3
4
- data.tar.gz: 903cb16a9a77396e90ea9ab2f33fb94a84d6a811525d23506af43195ab49fec2
3
+ metadata.gz: 48135be870a425c76338a773b41dc9438883b30cab622cc8c2d81394b860047d
4
+ data.tar.gz: e7a439c010c1bdbd3ab26b92f2126a4d518a2c7808d7acc6af2e57a03ec07376
5
5
  SHA512:
6
- metadata.gz: bf99199469304444421b00adbaa2b64707c7eddcdaff9a3c662531ebf5f2220a1b6555145fa516422921ac0980c851e7ce234ff88391d608ca6783211f364ddf
7
- data.tar.gz: 41aec998852a8458ed9f5508f37374ac72fb170c4235a7279e4f9864d2af143281cd81e1dec6099c80765d97b2263d1ee34bebd2b193701b9a4175a024ca5991
6
+ metadata.gz: b5c61d835a10a80211b625dd3ef8cf949fdcf30be9584d2a6793d69e309e3cb283097c2502a8912801ab9ee13679f1e71344034a197c54c88872157e66df74a0
7
+ data.tar.gz: 20fbde1832dc7338dba7f78c08556eceab4532247f41f0bb1c0c15ba11fc0cd2f07aabeaef62525eba24afc95b5e9854dbb3d71f6d6f493097d614ae913818f8
data/README.md CHANGED
@@ -74,11 +74,37 @@ puts "\n=== FTP Directory Listing Example ==="
74
74
  list = Curl::Easy.new('ftp://ftp.example.com/remote/directory/')
75
75
  list.username = 'user'
76
76
  list.password = 'password'
77
- list.dirlistonly = true
77
+ list.set(:dirlistonly, 1)
78
78
  list.perform
79
79
  puts list.body
80
80
  ```
81
81
 
82
+ ### FTP over HTTP proxy tunnel (NLST/LIST)
83
+ When listing directories through an HTTP proxy with `proxy_tunnel` (CONNECT), let libcurl manage the passive data connection. Do not send `PASV`/`EPSV` or `NLST` via `easy.ftp_commands` — QUOTE commands run on the control connection and libcurl will not open the data connection, resulting in 425 errors.
84
+
85
+ To get NLST-like output safely:
86
+
87
+ ```ruby
88
+ list = Curl::Easy.new('ftp://ftp.example.com/remote/directory/')
89
+ list.username = 'user'
90
+ list.password = 'password'
91
+ list.proxy_url = 'http://proxy.example.com:80'
92
+ list.proxy_tunnel = true
93
+
94
+ # Ask libcurl to perform a listing (names only)
95
+ list.set(:dirlistonly, 1)
96
+
97
+ # If the proxy or server has trouble with EPSV/EPRT, you can adjust:
98
+ # list.set(:ftp_use_epsv, 0) # disable EPSV
99
+ # list.set(:ftp_use_eprt, 0) # disable EPRT (stick to IPv4 PASV)
100
+ # list.set(:ftp_skip_pasv_ip, 1) # ignore PASV host, reuse control host
101
+
102
+ list.perform
103
+ puts list.body
104
+ ```
105
+
106
+ If you need a full `LIST` output instead of just names, omit `dirlistonly` and parse the server response accordingly. The key is to let libcurl initiate the data connection (PASV/EPSV) instead of trying to drive it via `ftp_commands`.
107
+
82
108
  ### Advanced FTP Usage with Various Options
83
109
  ```
84
110
  puts "\n=== Advanced FTP Example ==="
@@ -327,6 +353,8 @@ end
327
353
 
328
354
  ### HTTP POST form:
329
355
 
356
+ Note: Instance methods like `easy.http_post(...)` do not accept a URL argument. Set the URL first (for example, `Curl::Easy.new(url)` or `easy.url = url`) and then call `easy.http_post(...)`. If you want to pass the URL directly to the call, use the class/module helpers such as `Curl::Easy.http_post(url, ...)` or `Curl.post(url, ...)`.
357
+
330
358
  ```ruby
331
359
  c = Curl::Easy.http_post("http://my.rails.box/thing/create",
332
360
  Curl::PostField.content('thing[name]', 'box'),
@@ -339,6 +367,19 @@ c = Curl::Easy.http_post("http://my.rails.box/thing/create",
339
367
  c = Curl::Easy.new("http://my.rails.box/files/upload")
340
368
  c.multipart_form_post = true
341
369
  c.http_post(Curl::PostField.file('thing[file]', 'myfile.rb'))
370
+
371
+ ### Custom request target
372
+
373
+ Some advanced scenarios need a request-target that differs from the URL host/path (for example, absolute-form targets or special values like `*`). If your libcurl supports `CURLOPT_REQUEST_TARGET` (libcurl ≥ 7.55), you can override it:
374
+
375
+ ```ruby
376
+ c = Curl::Easy.new("http://127.0.0.1:9129/methods")
377
+ c.request_target = "http://localhost:9129/methods" # absolute-form target
378
+ c.headers = { 'Host' => 'example.com' } # override Host header if needed
379
+ c.perform
380
+ ```
381
+
382
+ For HTTPS, prefer `easy.resolve = ["host:443:IP"]` to keep Host/SNI/certificates aligned.
342
383
  ```
343
384
 
344
385
  ### Using HTTP/2
@@ -418,3 +459,31 @@ end
418
459
  * `on_missing` is called when the response code is 4xx
419
460
  * `on_failure` is called when the response code is 5xx
420
461
  * `on_complete` is called in all cases.
462
+
463
+ ### Cookies
464
+
465
+ - Manual cookies: Set the outgoing `Cookie` header via `easy.cookies = "name=value; other=val"`. This only affects the request header and does not modify libcurl's internal cookie engine.
466
+ - Cookie engine: Enable with `easy.enable_cookies = true`. Optionally set `easy.cookiefile` (to load) and/or `easy.cookiejar` (to persist). Cookies received via `Set-Cookie` go into this engine.
467
+ - Inspect engine cookies: `easy.cookielist` returns an array of strings (Netscape or Set-Cookie format).
468
+ - Modify engine cookies: use `easy.cookielist = ...` or `easy.set(:cookielist, ...)` with either a `Set-Cookie` style string, Netscape cookie lines, or special commands: `"ALL"` (clear), `"SESS"` (remove session cookies), `"FLUSH"` (write to jar), `"RELOAD"` (reload from file).
469
+ - Clearing manual cookies: assign an empty string (`easy.cookies = ''`). Assigning `nil` has no effect in current versions.
470
+
471
+ Examples:
472
+
473
+ ```ruby
474
+ easy = Curl::Easy.new("https://example.com")
475
+
476
+ # Use the cookie engine and persist cookies
477
+ easy.enable_cookies = true
478
+ easy.cookiejar = "/tmp/cookies.txt"
479
+ easy.perform
480
+
481
+ # Later: inspect and tweak engine cookies
482
+ p easy.cookielist
483
+ easy.cookielist = 'ALL' # clear stored cookies
484
+
485
+ # Send custom Cookie header for a single request
486
+ easy.cookies = "flag=1; session_override=abc"
487
+ easy.perform
488
+ easy.cookies = '' # clear manual Cookie header
489
+ ```
data/ext/curb.c CHANGED
@@ -616,6 +616,11 @@ void Init_curb_core() {
616
616
  #if HAVE_CURLOPT_PROXYHEADER
617
617
  CURB_DEFINE(CURLOPT_PROXYHEADER);
618
618
  #endif
619
+ #if HAVE_CURLOPT_REQUEST_TARGET
620
+ /* Allows overriding the Request-URI target used in the request line.
621
+ * Useful for absolute-form requests or special targets like "*". */
622
+ CURB_DEFINE(CURLOPT_REQUEST_TARGET);
623
+ #endif
619
624
  #if HAVE_CURLOPT_HTTP200ALIASES
620
625
  CURB_DEFINE(CURLOPT_HTTP200ALIASES);
621
626
  #endif
data/ext/curb.h CHANGED
@@ -28,11 +28,11 @@
28
28
  #include "curb_macros.h"
29
29
 
30
30
  // These should be managed from the Rake 'release' task.
31
- #define CURB_VERSION "1.2.0"
32
- #define CURB_VER_NUM 1020
31
+ #define CURB_VERSION "1.2.1"
32
+ #define CURB_VER_NUM 1021
33
33
  #define CURB_VER_MAJ 1
34
34
  #define CURB_VER_MIN 2
35
- #define CURB_VER_MIC 0
35
+ #define CURB_VER_MIC 1
36
36
  #define CURB_VER_PATCH 0
37
37
 
38
38
 
data/ext/curb_easy.c CHANGED
@@ -225,6 +225,24 @@ void curl_easy_mark(ruby_curl_easy *rbce) {
225
225
  }
226
226
 
227
227
  static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
228
+ if (!rbce) {
229
+ return;
230
+ }
231
+
232
+ if (!NIL_P(rbce->multi)) {
233
+ VALUE multi_val = rbce->multi;
234
+ ruby_curl_multi *rbcm = NULL;
235
+
236
+ rbce->multi = Qnil;
237
+
238
+ if (!NIL_P(multi_val) && RB_TYPE_P(multi_val, T_DATA)) {
239
+ Data_Get_Struct(multi_val, ruby_curl_multi, rbcm);
240
+ if (rbcm) {
241
+ rb_curl_multi_forget_easy(rbcm, rbce);
242
+ }
243
+ }
244
+ }
245
+
228
246
  if (rbce->curl_headers) {
229
247
  curl_slist_free_all(rbce->curl_headers);
230
248
  }
@@ -310,6 +328,7 @@ static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
310
328
  rbce->verbose = 0;
311
329
  rbce->multipart_form_post = 0;
312
330
  rbce->enable_cookies = 0;
331
+ rbce->cookielist_engine_enabled = 0;
313
332
  rbce->ignore_content_length = 0;
314
333
  rbce->callback_active = 0;
315
334
  rbce->last_result = 0;
@@ -664,7 +683,16 @@ static VALUE ruby_curl_easy_proxypwd_get(VALUE self) {
664
683
  * call-seq:
665
684
  * easy.cookies => "name1=content1; name2=content2;"
666
685
  *
667
- * Obtain the cookies for this Curl::Easy instance.
686
+ * Obtain the manually set Cookie header string for this Curl::Easy instance.
687
+ *
688
+ * Notes:
689
+ * - This corresponds to libcurl's CURLOPT_COOKIE and only affects the outgoing
690
+ * Cookie request header. It does NOT modify the internal libcurl cookie engine
691
+ * that stores cookies received via Set-Cookie.
692
+ * - To inspect or modify cookies stored in the cookie engine, use
693
+ * +easy.cookielist+ (getter) and +easy.cookielist=+ or +easy.set(:cookielist, ...)+ (setter).
694
+ * - To clear a previously set manual Cookie header, assign an empty string.
695
+ * Assigning +nil+ currently has no effect.
668
696
  */
669
697
  static VALUE ruby_curl_easy_cookies_get(VALUE self) {
670
698
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookies);
@@ -674,7 +702,8 @@ static VALUE ruby_curl_easy_cookies_get(VALUE self) {
674
702
  * call-seq:
675
703
  * easy.cookiefile => string
676
704
  *
677
- * Obtain the cookiefile file for this Curl::Easy instance.
705
+ * Obtain the cookiefile path for this Curl::Easy instance (used to load cookies when the
706
+ * cookie engine is enabled).
678
707
  */
679
708
  static VALUE ruby_curl_easy_cookiefile_get(VALUE self) {
680
709
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiefile);
@@ -684,7 +713,8 @@ static VALUE ruby_curl_easy_cookiefile_get(VALUE self) {
684
713
  * call-seq:
685
714
  * easy.cookiejar => string
686
715
  *
687
- * Obtain the cookiejar file to use for this Curl::Easy instance.
716
+ * Obtain the cookiejar path for this Curl::Easy instance (used to persist cookies when the
717
+ * cookie engine is enabled).
688
718
  */
689
719
  static VALUE ruby_curl_easy_cookiejar_get(VALUE self) {
690
720
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiejar);
@@ -984,7 +1014,15 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
984
1014
  * call-seq:
985
1015
  * easy.ftp_commands = ["CWD /", "MKD directory"] => ["CWD /", ...]
986
1016
  *
987
- * Explicitly sets the list of commands to execute on the FTP server when calling perform
1017
+ * Explicitly sets the list of commands to execute on the FTP server when calling perform.
1018
+ *
1019
+ * NOTE:
1020
+ * - This maps to libcurl CURLOPT_QUOTE; it sends commands on the control connection.
1021
+ * - Do not include data-transfer commands like LIST/NLST/RETR/STOR here. libcurl does not
1022
+ * parse PASV/EPSV replies from QUOTE commands and will not establish the required data
1023
+ * connection. For directory listings, set CURLOPT_DIRLISTONLY (via `easy.set(:dirlistonly, true)`)
1024
+ * and request an FTP directory URL (e.g. "ftp://host/path/") so libcurl manages PASV/EPSV
1025
+ * and the data connection for you.
988
1026
  */
989
1027
  static VALUE ruby_curl_easy_ftp_commands_set(VALUE self, VALUE ftp_commands) {
990
1028
  CURB_OBJECT_HSETTER(ruby_curl_easy, ftp_commands);
@@ -1893,7 +1931,12 @@ static VALUE ruby_curl_easy_multipart_form_post_q(VALUE self) {
1893
1931
  * easy.enable_cookies = boolean => boolean
1894
1932
  *
1895
1933
  * Configure whether the libcurl cookie engine is enabled for this Curl::Easy
1896
- * instance.
1934
+ * instance. When enabled, cookies received via Set-Cookie are stored by libcurl
1935
+ * and automatically sent on subsequent matching requests. Use +easy.cookiefile+
1936
+ * to load cookies and +easy.cookiejar+ to persist them.
1937
+ *
1938
+ * This setting is independent from the manual Cookie header set via +easy.cookies+.
1939
+ * The manual header is additive and can be cleared by assigning an empty string.
1897
1940
  */
1898
1941
  static VALUE ruby_curl_easy_enable_cookies_set(VALUE self, VALUE enable_cookies)
1899
1942
  {
@@ -2486,9 +2529,8 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2486
2529
  #endif
2487
2530
  }
2488
2531
 
2489
- /* Set up HTTP cookie handling if necessary
2490
- FIXME this may not get disabled if it's enabled, the disabled again from ruby.
2491
- */
2532
+ /* Set up HTTP cookie handling if necessary */
2533
+ /* Enable/attach cookie engine if requested, or implicitly via COOKIELIST usage */
2492
2534
  if (rbce->enable_cookies) {
2493
2535
  if (!rb_easy_nil("cookiejar")) {
2494
2536
  curl_easy_setopt(curl, CURLOPT_COOKIEJAR, rb_easy_get_str("cookiejar"));
@@ -2499,6 +2541,9 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
2499
2541
  } else {
2500
2542
  curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); /* "" = magic to just enable */
2501
2543
  }
2544
+ } else if (rbce->cookielist_engine_enabled) {
2545
+ /* Ensure cookie engine is enabled even if enable_cookies? is false. */
2546
+ curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
2502
2547
  }
2503
2548
 
2504
2549
  if (!rb_easy_nil("cookies")) {
@@ -3595,6 +3640,12 @@ static VALUE ruby_curl_easy_num_connects_get(VALUE self) {
3595
3640
  * Returned strings are in Netscape cookiejar format or in Set-Cookie format.
3596
3641
  * Since 7.43.0 cookies in the Set-Cookie format without a domain name are not exported.
3597
3642
  *
3643
+ * To modify the cookie engine (add/replace/remove), use +easy.cookielist= string+
3644
+ * or +easy.set(:cookielist, string)+ with one of the following accepted inputs:
3645
+ * - A Set-Cookie style header string: "Set-Cookie: name=value; Domain=example.com; Path=/; Expires=..."
3646
+ * - One or more lines in Netscape cookie file format (tab-separated fields)
3647
+ * - Special commands: "ALL" (clear all), "SESS" (remove session cookies), "FLUSH" (write to jar), "RELOAD" (reload from file)
3648
+ *
3598
3649
  * @see https://curl.se/libcurl/c/CURLINFO_COOKIELIST.html option <code>CURLINFO_COOKIELIST</code> of
3599
3650
  * <code>curl_easy_getopt(3)</code> to see how libcurl behaves.
3600
3651
  * @note requires libcurl 7.14.1 or higher, otherwise +-1+ is always returned
@@ -3656,7 +3707,7 @@ static VALUE ruby_curl_easy_ftp_entry_path_get(VALUE self) {
3656
3707
  return Qnil;
3657
3708
  }
3658
3709
  #else
3659
- rb_warn("Installed libcurl is too old to support num_connects");
3710
+ rb_warn("Installed libcurl is too old to support ftp_entry_path");
3660
3711
  return Qnil;
3661
3712
  #endif
3662
3713
  }
@@ -3815,9 +3866,72 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3815
3866
  VALUE cookiejar = val;
3816
3867
  CURB_OBJECT_HSETTER(ruby_curl_easy, cookiejar);
3817
3868
  } break;
3869
+ #if HAVE_CURLOPT_REQUEST_TARGET
3870
+ case CURLOPT_REQUEST_TARGET: {
3871
+ /* Forward request-target directly to libcurl as a string. */
3872
+ curl_easy_setopt(rbce->curl, CURLOPT_REQUEST_TARGET, NIL_P(val) ? NULL : StringValueCStr(val));
3873
+ } break;
3874
+ #endif
3818
3875
  case CURLOPT_TCP_NODELAY: {
3819
3876
  curl_easy_setopt(rbce->curl, CURLOPT_TCP_NODELAY, NUM2LONG(val));
3820
3877
  } break;
3878
+ /* FTP-specific toggles */
3879
+ #if HAVE_CURLOPT_DIRLISTONLY
3880
+ case CURLOPT_DIRLISTONLY: {
3881
+ int type = rb_type(val);
3882
+ VALUE value;
3883
+ if (type == T_TRUE) {
3884
+ value = rb_int_new(1);
3885
+ } else if (type == T_FALSE) {
3886
+ value = rb_int_new(0);
3887
+ } else {
3888
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3889
+ }
3890
+ curl_easy_setopt(rbce->curl, CURLOPT_DIRLISTONLY, NUM2LONG(value));
3891
+ } break;
3892
+ #endif
3893
+ #if HAVE_CURLOPT_FTP_USE_EPSV
3894
+ case CURLOPT_FTP_USE_EPSV: {
3895
+ int type = rb_type(val);
3896
+ VALUE value;
3897
+ if (type == T_TRUE) {
3898
+ value = rb_int_new(1);
3899
+ } else if (type == T_FALSE) {
3900
+ value = rb_int_new(0);
3901
+ } else {
3902
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3903
+ }
3904
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPSV, NUM2LONG(value));
3905
+ } break;
3906
+ #endif
3907
+ #if HAVE_CURLOPT_FTP_USE_EPRT
3908
+ case CURLOPT_FTP_USE_EPRT: {
3909
+ int type = rb_type(val);
3910
+ VALUE value;
3911
+ if (type == T_TRUE) {
3912
+ value = rb_int_new(1);
3913
+ } else if (type == T_FALSE) {
3914
+ value = rb_int_new(0);
3915
+ } else {
3916
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3917
+ }
3918
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPRT, NUM2LONG(value));
3919
+ } break;
3920
+ #endif
3921
+ #if HAVE_CURLOPT_FTP_SKIP_PASV_IP
3922
+ case CURLOPT_FTP_SKIP_PASV_IP: {
3923
+ int type = rb_type(val);
3924
+ VALUE value;
3925
+ if (type == T_TRUE) {
3926
+ value = rb_int_new(1);
3927
+ } else if (type == T_FALSE) {
3928
+ value = rb_int_new(0);
3929
+ } else {
3930
+ value = rb_funcall(val, rb_intern("to_i"), 0);
3931
+ }
3932
+ curl_easy_setopt(rbce->curl, CURLOPT_FTP_SKIP_PASV_IP, NUM2LONG(value));
3933
+ } break;
3934
+ #endif
3821
3935
  case CURLOPT_RANGE: {
3822
3936
  curl_easy_setopt(rbce->curl, CURLOPT_RANGE, StringValueCStr(val));
3823
3937
  } break;
@@ -3895,8 +4009,23 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
3895
4009
  #endif
3896
4010
  #if HAVE_CURLOPT_COOKIELIST
3897
4011
  case CURLOPT_COOKIELIST: {
4012
+ /* Forward to libcurl */
3898
4013
  curl_easy_setopt(rbce->curl, CURLOPT_COOKIELIST, StringValueCStr(val));
3899
- } break;
4014
+ /* Track whether the cookie engine should be enabled for requests.
4015
+ * According to libcurl docs, CURLOPT_COOKIELIST also enables the cookie engine
4016
+ * when provided with a non-command string. Some environments may still require
4017
+ * an explicit "enable" via CURLOPT_COOKIEFILE="" to send cookies on requests.
4018
+ * We do that in the perform setup when this flag is set.
4019
+ */
4020
+ if (RB_TYPE_P(val, T_STRING)) {
4021
+ const char *s = StringValueCStr(val);
4022
+ if (!(strcmp(s, "ALL") == 0 || strcmp(s, "SESS") == 0 || strcmp(s, "FLUSH") == 0 || strcmp(s, "RELOAD") == 0)) {
4023
+ rbce->cookielist_engine_enabled = 1;
4024
+ }
4025
+ } else {
4026
+ /* Non-string values are unexpected; be conservative and do not enable. */
4027
+ }
4028
+ } break;
3900
4029
  #endif
3901
4030
  #if HAVE_CURLOPT_PROXY_SSL_VERIFYHOST
3902
4031
  case CURLOPT_PROXY_SSL_VERIFYHOST:
data/ext/curb_easy.h CHANGED
@@ -77,6 +77,7 @@ typedef struct {
77
77
  char verbose;
78
78
  char multipart_form_post;
79
79
  char enable_cookies;
80
+ char cookielist_engine_enabled; /* track if CURLOPT_COOKIELIST was used with a non-command to enable engine */
80
81
  char ignore_content_length;
81
82
  char callback_active;
82
83
 
data/ext/curb_multi.c CHANGED
@@ -76,6 +76,10 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy);
76
76
  static void rb_curl_multi_read_info(VALUE self, CURLM *mptr);
77
77
  static void rb_curl_multi_run(VALUE self, CURLM *multi_handle, int *still_running);
78
78
 
79
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg);
80
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm);
81
+ static void curl_multi_mark(void *ptr);
82
+
79
83
  static VALUE callback_exception(VALUE did_raise, VALUE exception) {
80
84
  // TODO: we could have an option to enable exception reporting
81
85
  /* VALUE ret = rb_funcall(exception, rb_intern("message"), 0);
@@ -97,8 +101,67 @@ static VALUE callback_exception(VALUE did_raise, VALUE exception) {
97
101
  return exception;
98
102
  }
99
103
 
104
+ static int detach_easy_entry(st_data_t key, st_data_t val, st_data_t arg) {
105
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)arg;
106
+ VALUE easy = (VALUE)val;
107
+ ruby_curl_easy *rbce = NULL;
108
+
109
+ if (RB_TYPE_P(easy, T_DATA)) {
110
+ Data_Get_Struct(easy, ruby_curl_easy, rbce);
111
+ }
112
+
113
+ if (!rbce) {
114
+ return ST_CONTINUE;
115
+ }
116
+
117
+ if (rbcm && rbcm->handle && rbce->curl) {
118
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
119
+ }
120
+
121
+ rbce->multi = Qnil;
122
+
123
+ return ST_CONTINUE;
124
+ }
125
+
126
+ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr) {
127
+ ruby_curl_easy *rbce = (ruby_curl_easy *)rbce_ptr;
128
+
129
+ if (!rbcm || !rbce || !rbcm->attached) {
130
+ return;
131
+ }
132
+
133
+ st_data_t key = (st_data_t)rbce;
134
+ st_delete(rbcm->attached, &key, NULL);
135
+ }
136
+
137
+ static void rb_curl_multi_detach_all(ruby_curl_multi *rbcm) {
138
+ if (!rbcm || !rbcm->attached) {
139
+ return;
140
+ }
141
+
142
+ st_table *attached = rbcm->attached;
143
+ rbcm->attached = NULL;
144
+
145
+ st_foreach(attached, detach_easy_entry, (st_data_t)rbcm);
146
+
147
+ st_free_table(attached);
148
+
149
+ rbcm->active = 0;
150
+ rbcm->running = 0;
151
+ }
152
+
100
153
  void curl_multi_free(ruby_curl_multi *rbcm) {
101
- curl_multi_cleanup(rbcm->handle);
154
+ if (!rbcm) {
155
+ return;
156
+ }
157
+
158
+ rb_curl_multi_detach_all(rbcm);
159
+
160
+ if (rbcm->handle) {
161
+ curl_multi_cleanup(rbcm->handle);
162
+ rbcm->handle = NULL;
163
+ }
164
+
102
165
  free(rbcm);
103
166
  }
104
167
 
@@ -110,6 +173,18 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
110
173
 
111
174
  rbcm->active = 0;
112
175
  rbcm->running = 0;
176
+
177
+ if (rbcm->attached) {
178
+ st_free_table(rbcm->attached);
179
+ rbcm->attached = NULL;
180
+ }
181
+
182
+ rbcm->attached = st_init_numtable();
183
+ if (!rbcm->attached) {
184
+ curl_multi_cleanup(rbcm->handle);
185
+ rbcm->handle = NULL;
186
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
187
+ }
113
188
  }
114
189
 
115
190
  /*
@@ -124,6 +199,8 @@ VALUE ruby_curl_multi_new(VALUE klass) {
124
199
  rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Multi");
125
200
  }
126
201
 
202
+ MEMZERO(rbcm, ruby_curl_multi, 1);
203
+
127
204
  ruby_curl_multi_init(rbcm);
128
205
 
129
206
  /*
@@ -131,8 +208,8 @@ VALUE ruby_curl_multi_new(VALUE klass) {
131
208
  * If your structure references other Ruby objects, then your mark function needs to
132
209
  * identify these objects using rb_gc_mark(value). If the structure doesn't reference
133
210
  * other Ruby objects, you can simply pass 0 as a function pointer.
134
- */
135
- return Data_Wrap_Struct(klass, 0, curl_multi_free, rbcm);
211
+ */
212
+ return Data_Wrap_Struct(klass, curl_multi_mark, curl_multi_free, rbcm);
136
213
  }
137
214
 
138
215
  /*
@@ -292,6 +369,17 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
292
369
  * If this number is not correct, the next call to curl_multi_perform will correct it. */
293
370
  rbcm->running++;
294
371
 
372
+ if (!rbcm->attached) {
373
+ rbcm->attached = st_init_numtable();
374
+ if (!rbcm->attached) {
375
+ curl_multi_remove_handle(rbcm->handle, rbce->curl);
376
+ ruby_curl_easy_cleanup(easy, rbce);
377
+ rb_raise(rb_eNoMemError, "Failed to allocate multi attachment table");
378
+ }
379
+ }
380
+
381
+ st_insert(rbcm->attached, (st_data_t)rbce, (st_data_t)easy);
382
+
295
383
  /* track a reference to associated multi handle */
296
384
  rbce->multi = self;
297
385
 
@@ -332,9 +420,13 @@ static void rb_curl_multi_remove(ruby_curl_multi *rbcm, VALUE easy) {
332
420
  raise_curl_multi_error_exception(result);
333
421
  }
334
422
 
335
- rbcm->active--;
423
+ if (rbcm->active > 0) {
424
+ rbcm->active--;
425
+ }
336
426
 
337
427
  ruby_curl_easy_cleanup( easy, rbce );
428
+
429
+ rb_curl_multi_forget_easy(rbcm, rbce);
338
430
  }
339
431
 
340
432
  // on_success, on_failure, on_complete
@@ -1158,11 +1250,32 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1158
1250
  VALUE ruby_curl_multi_close(VALUE self) {
1159
1251
  ruby_curl_multi *rbcm;
1160
1252
  Data_Get_Struct(self, ruby_curl_multi, rbcm);
1161
- curl_multi_cleanup(rbcm->handle);
1253
+ rb_curl_multi_detach_all(rbcm);
1254
+
1255
+ if (rbcm->handle) {
1256
+ curl_multi_cleanup(rbcm->handle);
1257
+ rbcm->handle = NULL;
1258
+ }
1259
+
1162
1260
  ruby_curl_multi_init(rbcm);
1163
1261
  return self;
1164
1262
  }
1165
1263
 
1264
+ /* GC mark: keep attached easy VALUEs alive while associated. */
1265
+ static int mark_attached_i(st_data_t key, st_data_t val, st_data_t arg) {
1266
+ VALUE easy = (VALUE)val;
1267
+ if (!NIL_P(easy)) rb_gc_mark(easy);
1268
+ return ST_CONTINUE;
1269
+ }
1270
+
1271
+ static void curl_multi_mark(void *ptr) {
1272
+ ruby_curl_multi *rbcm = (ruby_curl_multi *)ptr;
1273
+ if (!rbcm) return;
1274
+ if (rbcm->attached) {
1275
+ st_foreach(rbcm->attached, mark_attached_i, (st_data_t)0);
1276
+ }
1277
+ }
1278
+
1166
1279
 
1167
1280
  /* =================== INIT LIB =====================*/
1168
1281
  void init_curb_multi() {
data/ext/curb_multi.h CHANGED
@@ -8,18 +8,21 @@
8
8
  #define __CURB_MULTI_H
9
9
 
10
10
  #include "curb.h"
11
- #include "curb_easy.h"
12
11
  #include <curl/multi.h>
13
12
 
13
+ struct st_table;
14
+
14
15
  typedef struct {
15
16
  int active;
16
17
  int running;
17
18
  CURLM *handle;
19
+ struct st_table *attached;
18
20
  } ruby_curl_multi;
19
21
 
20
22
  extern VALUE cCurlMulti;
21
23
  void init_curb_multi();
22
24
  VALUE ruby_curl_multi_new(VALUE klass);
25
+ void rb_curl_multi_forget_easy(ruby_curl_multi *rbcm, void *rbce_ptr);
23
26
 
24
27
 
25
28
  #endif
data/ext/extconf.rb CHANGED
@@ -500,6 +500,7 @@ have_constant "curlusessl_try"
500
500
  have_constant "curlusessl_control"
501
501
  have_constant "curlusessl_all"
502
502
  have_constant "curlopt_resolve"
503
+ have_constant "curlopt_request_target"
503
504
  have_constant "curlopt_sslcert"
504
505
  have_constant "curlopt_sslcerttype"
505
506
  have_constant "curlopt_sslkey"
data/lib/curl/easy.rb CHANGED
@@ -158,6 +158,22 @@ module Curl
158
158
  set :proxy, url
159
159
  end
160
160
 
161
+ #
162
+ # call-seq:
163
+ # easy.request_target = string => string
164
+ #
165
+ # Set the request-target used in the HTTP request line (libcurl CURLOPT_REQUEST_TARGET).
166
+ # Useful for absolute-form request targets (e.g., when speaking to proxies) or
167
+ # special targets like "*" (OPTIONS *). Requires libcurl with CURLOPT_REQUEST_TARGET support.
168
+ #
169
+ def request_target=(value)
170
+ if Curl.const_defined?(:CURLOPT_REQUEST_TARGET)
171
+ set :request_target, value
172
+ else
173
+ raise NotImplementedError, "CURLOPT_REQUEST_TARGET is not supported by this libcurl"
174
+ end
175
+ end
176
+
161
177
  def ssl_verify_host=(value)
162
178
  value = 1 if value.class == TrueClass
163
179
  value = 0 if value.class == FalseClass
@@ -216,9 +232,18 @@ module Curl
216
232
  # call-seq:
217
233
  # easy.cookies = "name1=content1; name2=content2;" => string
218
234
  #
219
- # Set cookies to be sent by this Curl::Easy instance. The format of the string should
220
- # be NAME=CONTENTS, where NAME is the cookie name and CONTENTS is what the cookie should contain.
221
- # Set multiple cookies in one string like this: "name1=content1; name2=content2;" etc.
235
+ # Set the manual Cookie request header for this Curl::Easy instance.
236
+ # The format of the string should be NAME=CONTENTS, where NAME is the cookie name and
237
+ # CONTENTS is what the cookie should contain. Set multiple cookies in one string like this:
238
+ # "name1=content1; name2=content2;".
239
+ #
240
+ # Notes:
241
+ # - This only affects the outgoing Cookie header (libcurl CURLOPT_COOKIE) and does NOT
242
+ # alter the internal libcurl cookie engine (which stores cookies from Set-Cookie).
243
+ # - To change cookies stored in the engine, use {#cookielist} / {#cookielist=} or
244
+ # {#set} with :cookielist.
245
+ # - To clear a previously set manual Cookie header, assign an empty string ('').
246
+ # Assigning +nil+ has no effect in current curb versions.
222
247
  #
223
248
  def cookies=(value)
224
249
  set :cookie, value
@@ -233,6 +258,8 @@ module Curl
233
258
  # *Note* that you must set enable_cookies true to enable the cookie
234
259
  # engine, or this option will be ignored.
235
260
  #
261
+ # Note: assigning +nil+ has no effect; pass a path string to use a cookie file.
262
+ #
236
263
  def cookiefile=(value)
237
264
  set :cookiefile, value
238
265
  end
@@ -241,16 +268,40 @@ module Curl
241
268
  # call-seq:
242
269
  # easy.cookiejar = string => string
243
270
  #
244
- # Set a cookiejar file to use for this Curl::Easy instance.
245
- # Cookies from the response will be written into this file.
271
+ # Set a cookiejar file to use for this Curl::Easy instance. Cookies from the response
272
+ # will be written into this file.
246
273
  #
247
274
  # *Note* that you must set enable_cookies true to enable the cookie
248
275
  # engine, or this option will be ignored.
249
276
  #
277
+ # Note: assigning +nil+ has no effect; pass a path string to persist cookies to a file.
278
+ #
250
279
  def cookiejar=(value)
251
280
  set :cookiejar, value
252
281
  end
253
282
 
283
+ #
284
+ # call-seq:
285
+ # easy.cookielist = string => string
286
+ #
287
+ # Modify cookies in libcurl's internal cookie engine (CURLOPT_COOKIELIST).
288
+ # Accepts a Set-Cookie style string, one or more lines in Netscape cookie file format,
289
+ # or one of the special commands: "ALL" (clear), "SESS" (remove session cookies),
290
+ # "FLUSH" (write to jar), "RELOAD" (reload from file).
291
+ #
292
+ # Examples:
293
+ # easy.cookielist = "Set-Cookie: session=42; Domain=example.com; Path=/;"
294
+ # easy.cookielist = [
295
+ # ['.example.com', 'TRUE', '/', 'FALSE', 0, 'c1', 'v1'].join("\t"),
296
+ # ['.example.com', 'TRUE', '/', 'FALSE', 0, 'c2', 'v2'].join("\t"),
297
+ # ''
298
+ # ].join("\n")
299
+ # easy.cookielist = 'ALL' # clear all cookies in the engine
300
+ #
301
+ def cookielist=(value)
302
+ set :cookielist, value
303
+ end
304
+
254
305
  #
255
306
  # call-seq:
256
307
  # easy = Curl::Easy.new("url") do|c|
@@ -573,6 +573,35 @@ class TestCurbCurlMulti < Test::Unit::TestCase
573
573
  end
574
574
  end
575
575
 
576
+ # Regression test for issue #267 (2015): ensure that when reusing
577
+ # easy handles with constrained concurrency, the callback receives
578
+ # the correct URL for each completed request rather than repeating
579
+ # the first URL.
580
+ def test_multi_easy_http_urls_unique_across_max_connects
581
+ urls = [
582
+ { :url => TestServlet.url + '?q=1', :method => :get },
583
+ { :url => TestServlet.url + '?q=2', :method => :get },
584
+ { :url => TestServlet.url + '?q=3', :method => :get }
585
+ ]
586
+
587
+ [1, 2, 3].each do |max|
588
+ results = []
589
+ Curl::Multi.http(urls.dup, {:pipeline => true, :max_connects => max}) do |easy, code, method|
590
+ assert_equal 200, code
591
+ assert_equal :get, method
592
+ results << easy.last_effective_url
593
+ end
594
+
595
+ # Ensure we saw one completion per input URL
596
+ assert_equal urls.size, results.size, "expected #{urls.size} results with max_connects=#{max}"
597
+
598
+ # And that each URL completed exactly once (no accidental reuse/mis-reporting)
599
+ expected_urls = urls.map { |u| u[:url] }
600
+ assert_equal expected_urls.to_set, results.to_set, "unexpected URLs for max_connects=#{max}: #{results.inspect}"
601
+ assert_equal expected_urls.size, results.uniq.size, "duplicate URLs seen for max_connects=#{max}: #{results.inspect}"
602
+ end
603
+ end
604
+
576
605
  def test_multi_recieves_500
577
606
  m = Curl::Multi.new
578
607
  e = Curl::Easy.new("http://127.0.0.1:9129/methods")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ross Bamford
8
8
  - Todd A. Fisher
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-31 00:00:00.000000000 Z
11
+ date: 2025-09-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
14
14
  for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
@@ -76,11 +76,6 @@ files:
76
76
  - tests/tc_curl_postfield.rb
77
77
  - tests/tc_curl_protocols.rb
78
78
  - tests/tc_fiber_scheduler.rb
79
- - tests/test_basic.rb
80
- - tests/test_fiber_debug.rb
81
- - tests/test_fiber_simple.rb
82
- - tests/test_real_url.rb
83
- - tests/test_simple_fiber.rb
84
79
  - tests/timeout.rb
85
80
  - tests/timeout_server.rb
86
81
  - tests/unittests.rb
@@ -142,11 +137,6 @@ test_files:
142
137
  - tests/tc_curl_postfield.rb
143
138
  - tests/tc_curl_protocols.rb
144
139
  - tests/tc_fiber_scheduler.rb
145
- - tests/test_basic.rb
146
- - tests/test_fiber_debug.rb
147
- - tests/test_fiber_simple.rb
148
- - tests/test_real_url.rb
149
- - tests/test_simple_fiber.rb
150
140
  - tests/timeout.rb
151
141
  - tests/timeout_server.rb
152
142
  - tests/unittests.rb
data/tests/test_basic.rb DELETED
@@ -1,29 +0,0 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
-
3
- class TestBasic < Test::Unit::TestCase
4
- include TestServerMethods
5
-
6
- def setup
7
- server_setup
8
- end
9
-
10
- def test_basic_request
11
- puts "\n=== Testing basic request ==="
12
- easy = Curl::Easy.new(TestServlet.url)
13
- easy.perform
14
- puts "Response code: #{easy.response_code}"
15
- puts "Body (first 100 chars): #{easy.body_str[0..100]}"
16
- assert_equal 200, easy.response_code
17
- end
18
-
19
- def test_slow_request
20
- puts "\n=== Testing slow request ==="
21
- url = TestServlet.url_to("/slow?seconds=0.1")
22
- puts "URL: #{url}"
23
- easy = Curl::Easy.new(url)
24
- easy.perform
25
- puts "Response code: #{easy.response_code}"
26
- puts "Body: #{easy.body_str}"
27
- assert_equal 200, easy.response_code
28
- end
29
- end
@@ -1,69 +0,0 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
- require 'async'
3
-
4
- class TestFiberDebug < Test::Unit::TestCase
5
- include TestServerMethods
6
-
7
- def setup
8
- server_setup
9
- end
10
-
11
- def test_simple_fiber_request
12
- puts "\n=== Starting simple fiber request test ==="
13
-
14
- run_async do |task|
15
- puts "Inside Async block"
16
- puts "Fiber scheduler available: #{Curl::Multi.fiber_scheduler_available?}"
17
-
18
- multi = Curl::Multi.new
19
- easy = Curl::Easy.new(TestServlet.url)
20
- easy.on_complete { |curl| puts "Request completed: #{curl.response_code}" }
21
-
22
- multi.add(easy)
23
- puts "Added easy handle to multi"
24
-
25
- # Perform without block first
26
- puts "Calling perform..."
27
- multi.perform
28
- puts "Perform completed"
29
- end
30
-
31
- puts "Test completed"
32
- end
33
-
34
- def test_fiber_with_block
35
- puts "\n=== Starting fiber with block test ==="
36
-
37
- run_async do |task|
38
- puts "Inside Async block"
39
-
40
- multi = Curl::Multi.new
41
- easy = Curl::Easy.new(TestServlet.url_to("/slow?seconds=0.1"))
42
- easy.on_complete { |curl| puts "Request completed: #{curl.response_code}" }
43
-
44
- multi.add(easy)
45
-
46
- block_calls = 0
47
- multi.perform do
48
- block_calls += 1
49
- puts "Block called: #{block_calls}"
50
- if block_calls < 5 # Limit iterations to prevent infinite loop
51
- Async::Task.yield
52
- end
53
- end
54
-
55
- puts "Perform completed, block called #{block_calls} times"
56
- end
57
-
58
- puts "Test completed"
59
- end
60
-
61
- private
62
- def run_async(&block)
63
- if defined?(Async) && Async.respond_to?(:run)
64
- Async.run(&block)
65
- else
66
- Async(&block)
67
- end
68
- end
69
- end
@@ -1,65 +0,0 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
- require 'async'
3
-
4
- class TestFiberSimple < Test::Unit::TestCase
5
- include TestServerMethods
6
-
7
- def setup
8
- server_setup
9
- end
10
-
11
- def test_simple_concurrent
12
- puts "\n=== Testing simple concurrent requests ==="
13
-
14
- results = []
15
-
16
- if Async.respond_to?(:run)
17
- Async.run do |task|
18
- puts "Fiber scheduler available: #{Curl::Multi.fiber_scheduler_available?}"
19
-
20
- multi = Curl::Multi.new
21
-
22
- # Add 3 requests
23
- 3.times do |i|
24
- easy = Curl::Easy.new(TestServlet.url_to("/slow?seconds=0.2&id=#{i}"))
25
- easy.on_complete { |curl|
26
- results << { id: i, code: curl.response_code }
27
- puts "Request #{i} completed"
28
- }
29
- multi.add(easy)
30
- end
31
-
32
- puts "Starting perform..."
33
- start_time = Time.now
34
- multi.perform # No block
35
- elapsed = Time.now - start_time
36
- puts "Perform completed in #{elapsed.round(2)}s"
37
- end
38
- else
39
- Async do |task|
40
- puts "Fiber scheduler available: #{Curl::Multi.fiber_scheduler_available?}"
41
-
42
- multi = Curl::Multi.new
43
-
44
- # Add 3 requests
45
- 3.times do |i|
46
- easy = Curl::Easy.new(TestServlet.url_to("/slow?seconds=0.2&id=#{i}"))
47
- easy.on_complete { |curl|
48
- results << { id: i, code: curl.response_code }
49
- puts "Request #{i} completed"
50
- }
51
- multi.add(easy)
52
- end
53
-
54
- puts "Starting perform..."
55
- start_time = Time.now
56
- multi.perform # No block
57
- elapsed = Time.now - start_time
58
- puts "Perform completed in #{elapsed.round(2)}s"
59
- end
60
- end
61
-
62
- assert_equal 3, results.size
63
- results.each { |r| assert_equal 200, r[:code] }
64
- end
65
- end
@@ -1,65 +0,0 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '../lib/curb'))
2
- require 'async'
3
-
4
- puts "Testing fiber scheduler with real URLs..."
5
-
6
- # Test without fiber scheduler
7
- puts "\n1. Without fiber scheduler:"
8
- start = Time.now
9
- multi = Curl::Multi.new
10
-
11
- easies = []
12
- 3.times do |i|
13
- easy = Curl::Easy.new("https://httpbin.org/delay/1")
14
- easy.on_complete { |curl| puts "Request #{i} completed: #{curl.response_code}" }
15
- multi.add(easy)
16
- easies << easy
17
- end
18
-
19
- multi.perform
20
- elapsed = Time.now - start
21
- puts "Total time: #{elapsed.round(2)}s"
22
-
23
- # Test with fiber scheduler
24
- puts "\n2. With fiber scheduler:"
25
- if Async.respond_to?(:run)
26
- Async.run do
27
- puts "Fiber scheduler available: #{Curl::Multi.fiber_scheduler_available?}"
28
-
29
- start = Time.now
30
- multi = Curl::Multi.new
31
-
32
- easies = []
33
- 3.times do |i|
34
- easy = Curl::Easy.new("https://httpbin.org/delay/1")
35
- easy.on_complete { |curl| puts "Request #{i} completed: #{curl.response_code}" }
36
- multi.add(easy)
37
- easies << easy
38
- end
39
-
40
- multi.perform
41
- elapsed = Time.now - start
42
- puts "Total time: #{elapsed.round(2)}s"
43
- end
44
- else
45
- Async do
46
- puts "Fiber scheduler available: #{Curl::Multi.fiber_scheduler_available?}"
47
-
48
- start = Time.now
49
- multi = Curl::Multi.new
50
-
51
- easies = []
52
- 3.times do |i|
53
- easy = Curl::Easy.new("https://httpbin.org/delay/1")
54
- easy.on_complete { |curl| puts "Request #{i} completed: #{curl.response_code}" }
55
- multi.add(easy)
56
- easies << easy
57
- end
58
-
59
- multi.perform
60
- elapsed = Time.now - start
61
- puts "Total time: #{elapsed.round(2)}s"
62
- end
63
- end
64
-
65
- puts "\nDone!"
@@ -1,34 +0,0 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '../lib/curb'))
2
- require 'async'
3
-
4
- puts "Testing simple fiber scheduler..."
5
-
6
- if Async.respond_to?(:run)
7
- Async.run do
8
- puts "Fiber scheduler available: #{Curl::Multi.fiber_scheduler_available?}"
9
-
10
- multi = Curl::Multi.new
11
- easy = Curl::Easy.new("https://httpbin.org/delay/1")
12
- easy.on_complete { |curl| puts "Request completed: #{curl.response_code}" }
13
-
14
- multi.add(easy)
15
- puts "Starting perform..."
16
- multi.perform
17
- puts "Perform completed"
18
- end
19
- else
20
- Async do
21
- puts "Fiber scheduler available: #{Curl::Multi.fiber_scheduler_available?}"
22
-
23
- multi = Curl::Multi.new
24
- easy = Curl::Easy.new("https://httpbin.org/delay/1")
25
- easy.on_complete { |curl| puts "Request completed: #{curl.response_code}" }
26
-
27
- multi.add(easy)
28
- puts "Starting perform..."
29
- multi.perform
30
- puts "Perform completed"
31
- end
32
- end
33
-
34
- puts "Done!"