fast_curl 0.3.1 → 0.5.0

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.
@@ -11,7 +11,10 @@
11
11
  #define FAST_CURL_HAVE_FIBER_SCHEDULER 1
12
12
  #endif
13
13
  #include <curl/curl.h>
14
+ #include <ctype.h>
14
15
  #include <limits.h>
16
+ #include <pthread.h>
17
+ #include <stdint.h>
15
18
  #include <stdlib.h>
16
19
  #include <string.h>
17
20
  #include <strings.h>
@@ -23,20 +26,25 @@
23
26
  #define FAST_CURL_NORETURN
24
27
  #endif
25
28
 
26
- #define MAX_RESPONSE_SIZE (100 * 1024 * 1024)
27
- #define MAX_REDIRECTS 5
28
- #define MAX_TIMEOUT 300
29
- #define MAX_RETRIES 10
30
- #define MAX_REQUESTS 10000
31
- #define MAX_CONNECTIONS 100
32
- #define MAX_RETRY_DELAY_MS 30000
33
- #define DEFAULT_RETRIES 1
34
- #define DEFAULT_RETRY_DELAY 0
35
- #define INITIAL_BUF_CAP 8192
36
- #define INITIAL_HEADER_CAP 16
37
- #define POLL_TIMEOUT_MS 50
38
- #define FIBER_POLL_TIMEOUT_MS 10
39
- #define HEADER_LINE_BUF_SIZE 512
29
+ #define MAX_RESPONSE_SIZE (100 * 1024 * 1024)
30
+ #define MAX_REDIRECTS 5
31
+ #define MAX_TIMEOUT 300
32
+ #define MAX_RETRIES 10
33
+ #define MAX_REQUESTS 10000
34
+ #define MAX_CONNECTIONS 100
35
+ #define MAX_BUFFER 10000
36
+ #define MAX_RETRY_DELAY_MS 30000
37
+ #define DEFAULT_RETRIES 1
38
+ #define DEFAULT_RETRY_DELAY 100
39
+ #define DEFAULT_CONNECT_TIMEOUT_MS 10000L
40
+ #define MAX_CONNECT_TIMEOUT_MS 300000L
41
+ #define MAX_TOTAL_TIMEOUT_MS 3600000L
42
+ #define INITIAL_BUF_CAP 8192
43
+ #define INITIAL_HEADER_CAP 16
44
+ #define POLL_TIMEOUT_MS 50
45
+ #define POLL_SLICE_MS 2000
46
+ #define FIBER_POLL_TIMEOUT_MS 10
47
+ #define HEADER_LINE_BUF_SIZE 512
40
48
 
41
49
  static const CURLcode DEFAULT_RETRYABLE_CURLE[] = {
42
50
  CURLE_COULDNT_CONNECT, CURLE_OPERATION_TIMEDOUT, CURLE_SEND_ERROR, CURLE_RECV_ERROR,
@@ -57,6 +65,16 @@ typedef enum {
57
65
  KEY_RETRIES,
58
66
  KEY_RETRY_DELAY,
59
67
  KEY_RETRY_CODES,
68
+ KEY_CONNECT_TIMEOUT,
69
+ KEY_TOTAL_TIMEOUT,
70
+ KEY_RETRY_NON_IDEMPOTENT,
71
+ KEY_FOLLOW_REDIRECTS,
72
+ KEY_MAX_REDIRECTS,
73
+ KEY_EFFECTIVE_URL,
74
+ KEY_ERROR,
75
+ KEY_ATTEMPTS,
76
+ KEY_BUFFER,
77
+ KEY_ACCEPT,
60
78
  KEY_LAST
61
79
  } key_id_t;
62
80
 
@@ -66,10 +84,113 @@ static VALUE fast_syms[KEY_LAST];
66
84
  #define SYM(key) fast_syms[key]
67
85
 
68
86
  static const char *const KEY_NAMES[KEY_LAST] = {
69
- "status", "headers", "body", "error_code", "url", "method",
70
- "timeout", "connections", "count", "retries", "retry_delay", "retry_codes",
87
+ "status",
88
+ "headers",
89
+ "body",
90
+ "error_code",
91
+ "url",
92
+ "method",
93
+ "timeout",
94
+ "connections",
95
+ "count",
96
+ "retries",
97
+ "retry_delay",
98
+ "retry_codes",
99
+ "connect_timeout",
100
+ "total_timeout",
101
+ "retry_non_idempotent",
102
+ "follow_redirects",
103
+ "max_redirects",
104
+ "effective_url",
105
+ "error",
106
+ "attempts",
107
+ "buffer",
108
+ "accept",
71
109
  };
72
110
 
111
+ typedef enum {
112
+ ERR_CURL,
113
+ ERR_INVALID_REQUEST,
114
+ ERR_NOT_COMPLETED,
115
+ ERR_DEADLINE,
116
+ ERR_LAST
117
+ } error_kind_t;
118
+
119
+ static VALUE error_syms[ERR_LAST];
120
+
121
+ static const char *const ERROR_KIND_NAMES[ERR_LAST] = {
122
+ "curl_error",
123
+ "invalid_request",
124
+ "not_completed",
125
+ "deadline_exceeded",
126
+ };
127
+
128
+ static long long fast_now_ms(void) {
129
+ struct timespec ts;
130
+ #ifdef CLOCK_MONOTONIC
131
+ clock_gettime(CLOCK_MONOTONIC, &ts);
132
+ #else
133
+ clock_gettime(CLOCK_REALTIME, &ts);
134
+ #endif
135
+ return (long long)ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
136
+ }
137
+
138
+ static uint64_t fast_rand_state;
139
+
140
+ /* Small xorshift PRNG; only used to jitter retry delays. */
141
+ static uint32_t fast_rand(void) {
142
+ uint64_t x = fast_rand_state;
143
+ if (x == 0)
144
+ x = (uint64_t)fast_now_ms() | 1ULL;
145
+ x ^= x << 13;
146
+ x ^= x >> 7;
147
+ x ^= x << 17;
148
+ fast_rand_state = x;
149
+ return (uint32_t)(x >> 32);
150
+ }
151
+
152
+ /* Process-wide DNS and TLS session cache, so repeated calls to the same host
153
+ skip resolution and can resume TLS instead of doing a full handshake. */
154
+ static CURLSH *fast_share = NULL;
155
+ static pthread_mutex_t fast_share_locks[CURL_LOCK_DATA_LAST];
156
+
157
+ static void fast_share_lock(CURL *handle, curl_lock_data data, curl_lock_access access,
158
+ void *userptr) {
159
+ (void)handle;
160
+ (void)access;
161
+ (void)userptr;
162
+ if ((int)data > 0 && (int)data < CURL_LOCK_DATA_LAST)
163
+ pthread_mutex_lock(&fast_share_locks[data]);
164
+ }
165
+
166
+ static void fast_share_unlock(CURL *handle, curl_lock_data data, void *userptr) {
167
+ (void)handle;
168
+ (void)userptr;
169
+ if ((int)data > 0 && (int)data < CURL_LOCK_DATA_LAST)
170
+ pthread_mutex_unlock(&fast_share_locks[data]);
171
+ }
172
+
173
+ static void fast_share_init(void) {
174
+ for (int i = 0; i < CURL_LOCK_DATA_LAST; i++)
175
+ pthread_mutex_init(&fast_share_locks[i], NULL);
176
+
177
+ fast_share = curl_share_init();
178
+ if (!fast_share)
179
+ return;
180
+
181
+ curl_share_setopt(fast_share, CURLSHOPT_LOCKFUNC, fast_share_lock);
182
+ curl_share_setopt(fast_share, CURLSHOPT_UNLOCKFUNC, fast_share_unlock);
183
+ curl_share_setopt(fast_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
184
+ curl_share_setopt(fast_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
185
+
186
+ /* CURL_LOCK_DATA_CONNECT is deliberately NOT shared. Sharing the connection
187
+ cache between multi handles running concurrently in different threads
188
+ deadlocks against CURLMOPT_MAX_TOTAL_CONNECTIONS, and removing that limit
189
+ turns the deadlock into a segfault inside curl_multi_perform (reproduced
190
+ on libcurl 8.5.0 with two threads x 25 requests). Cross-call TCP reuse
191
+ needs a persistent multi handle per thread, not a shared cache. */
192
+ }
193
+
73
194
  typedef struct {
74
195
  char *data;
75
196
  size_t len;
@@ -204,7 +325,7 @@ static size_t header_callback(char *ptr, size_t size, size_t nmemb, void *userda
204
325
 
205
326
  typedef struct {
206
327
  CURL *easy;
207
- int index;
328
+ long index;
208
329
  buffer_t body;
209
330
  header_list_t headers;
210
331
  struct curl_slist *req_headers;
@@ -212,9 +333,20 @@ typedef struct {
212
333
  int active;
213
334
  CURLcode curl_result;
214
335
  long http_status;
336
+ int idempotent;
337
+ int attempts;
338
+ const char *setup_error;
339
+ int setup_error_fatal;
215
340
  } request_ctx_t;
216
341
 
217
- static inline void request_ctx_init(request_ctx_t *ctx, int index) {
342
+ static VALUE fast_validation_error = Qnil;
343
+ #define SET_SETUP_ERROR(ctx, msg, fatal) \
344
+ do { \
345
+ (ctx)->setup_error = (msg); \
346
+ (ctx)->setup_error_fatal = (fatal); \
347
+ } while (0)
348
+
349
+ static inline void request_ctx_init(request_ctx_t *ctx, long index) {
218
350
  ctx->easy = NULL;
219
351
  ctx->index = index;
220
352
  buffer_init(&ctx->body);
@@ -224,6 +356,10 @@ static inline void request_ctx_init(request_ctx_t *ctx, int index) {
224
356
  ctx->active = 0;
225
357
  ctx->curl_result = CURLE_OK;
226
358
  ctx->http_status = 0;
359
+ ctx->idempotent = 1;
360
+ ctx->attempts = 0;
361
+ ctx->setup_error = NULL;
362
+ ctx->setup_error_fatal = 0;
227
363
  }
228
364
 
229
365
  static void request_ctx_free(request_ctx_t *ctx) {
@@ -267,6 +403,8 @@ static int request_ctx_reset_for_retry(request_ctx_t *ctx) {
267
403
  ctx->active = 0;
268
404
  ctx->curl_result = CURLE_OK;
269
405
  ctx->http_status = 0;
406
+ ctx->setup_error = NULL;
407
+ ctx->setup_error_fatal = 0;
270
408
  return 1;
271
409
  }
272
410
 
@@ -291,8 +429,18 @@ typedef struct {
291
429
  long retry_delay_ms;
292
430
  int *retry_http_codes;
293
431
  int retry_http_count;
432
+ int retry_non_idempotent;
294
433
  } retry_config_t;
295
434
 
435
+ typedef struct {
436
+ long timeout_sec;
437
+ long connect_timeout_ms;
438
+ long total_timeout_ms;
439
+ long follow_redirects;
440
+ long max_redirects;
441
+ long long deadline_ms;
442
+ } request_options_t;
443
+
296
444
  static int contains_header_injection(const char *str, long len) {
297
445
  for (long i = 0; i < len; i++) {
298
446
  if (str[i] == '\r' || str[i] == '\n' || str[i] == '\0')
@@ -392,24 +540,37 @@ static void run_via_fiber_worker(VALUE scheduler, void *(*func)(void *), void *a
392
540
  }
393
541
  #endif
394
542
 
395
- static void headers_hash_store(VALUE headers_hash, VALUE key, VALUE val) {
543
+ static VALUE fast_cHeaders = Qnil;
544
+ static VALUE new_headers_hash(void) {
545
+ if (!NIL_P(fast_cHeaders))
546
+ return rb_obj_alloc(fast_cHeaders);
547
+ return rb_hash_new();
548
+ }
549
+
550
+ static void headers_hash_store(VALUE headers_hash, VALUE key, VALUE val, int always_array) {
396
551
  VALUE existing = rb_hash_aref(headers_hash, key);
397
552
 
398
553
  if (NIL_P(existing)) {
399
- rb_hash_aset(headers_hash, key, val);
400
- } else if (RB_TYPE_P(existing, T_ARRAY)) {
554
+ rb_hash_aset(headers_hash, key, always_array ? rb_ary_new_from_args(1, val) : val);
555
+ return;
556
+ }
557
+
558
+ if (RB_TYPE_P(existing, T_ARRAY)) {
401
559
  rb_ary_push(existing, val);
402
- } else {
403
- VALUE values = rb_ary_new_from_args(2, existing, val);
404
- rb_hash_aset(headers_hash, key, values);
560
+ return;
405
561
  }
562
+
563
+ VALUE joined = rb_str_dup(existing);
564
+ rb_str_cat_cstr(joined, ", ");
565
+ rb_str_append(joined, val);
566
+ rb_hash_aset(headers_hash, key, joined);
406
567
  }
407
568
 
408
569
  static VALUE build_response(request_ctx_t *ctx) {
409
570
  long status = 0;
410
571
  curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &status);
411
572
 
412
- VALUE headers_hash = rb_hash_new();
573
+ VALUE headers_hash = new_headers_hash();
413
574
  for (int i = 0; i < ctx->headers.count; i++) {
414
575
  const char *hdr = ctx->headers.entries[i].str;
415
576
  size_t hdr_len = ctx->headers.entries[i].len;
@@ -418,6 +579,12 @@ static VALUE build_response(request_ctx_t *ctx) {
418
579
  continue;
419
580
 
420
581
  VALUE key = rb_str_new(hdr, colon - hdr);
582
+ char *kp = RSTRING_PTR(key);
583
+ long klen = RSTRING_LEN(key);
584
+ for (long k = 0; k < klen; k++)
585
+ kp[k] = (char)tolower((unsigned char)kp[k]);
586
+ int always_array = (klen == 10 && memcmp(kp, "set-cookie", 10) == 0);
587
+
421
588
  const char *vs = colon + 1;
422
589
  const char *ve = hdr + hdr_len;
423
590
 
@@ -427,7 +594,7 @@ static VALUE build_response(request_ctx_t *ctx) {
427
594
  ve--;
428
595
 
429
596
  VALUE val = rb_str_new(vs, ve - vs);
430
- headers_hash_store(headers_hash, key, val);
597
+ headers_hash_store(headers_hash, key, val, always_array);
431
598
  }
432
599
 
433
600
  VALUE body_str =
@@ -437,19 +604,33 @@ static VALUE build_response(request_ctx_t *ctx) {
437
604
  rb_hash_aset(result, SYM(KEY_STATUS), LONG2NUM(status));
438
605
  rb_hash_aset(result, SYM(KEY_HEADERS), headers_hash);
439
606
  rb_hash_aset(result, SYM(KEY_BODY), body_str);
607
+ rb_hash_aset(result, SYM(KEY_ERROR), Qnil);
608
+ rb_hash_aset(result, SYM(KEY_ERROR_CODE), Qnil);
609
+ rb_hash_aset(result, SYM(KEY_ATTEMPTS), INT2NUM(ctx->attempts));
610
+
611
+ char *effective = NULL;
612
+ if (curl_easy_getinfo(ctx->easy, CURLINFO_EFFECTIVE_URL, &effective) == CURLE_OK && effective)
613
+ rb_hash_aset(result, SYM(KEY_EFFECTIVE_URL), rb_str_new_cstr(effective));
614
+ else
615
+ rb_hash_aset(result, SYM(KEY_EFFECTIVE_URL), Qnil);
616
+
440
617
  return result;
441
618
  }
442
619
 
443
- static VALUE build_error_response(const char *message) {
620
+ static VALUE build_error_response(const char *message, error_kind_t kind, int attempts) {
444
621
  VALUE r = rb_hash_new();
445
622
  rb_hash_aset(r, SYM(KEY_STATUS), INT2NUM(0));
446
623
  rb_hash_aset(r, SYM(KEY_HEADERS), Qnil);
447
624
  rb_hash_aset(r, SYM(KEY_BODY), rb_str_new_cstr(message));
625
+ rb_hash_aset(r, SYM(KEY_ERROR), error_syms[kind]);
626
+ rb_hash_aset(r, SYM(KEY_ERROR_CODE), Qnil);
627
+ rb_hash_aset(r, SYM(KEY_EFFECTIVE_URL), Qnil);
628
+ rb_hash_aset(r, SYM(KEY_ATTEMPTS), INT2NUM(attempts));
448
629
  return r;
449
630
  }
450
631
 
451
- static VALUE build_error_response_with_code(const char *message, int error_code) {
452
- VALUE r = build_error_response(message);
632
+ static VALUE build_error_response_with_code(const char *message, int error_code, int attempts) {
633
+ VALUE r = build_error_response(message, ERR_CURL, attempts);
453
634
  rb_hash_aset(r, SYM(KEY_ERROR_CODE), INT2NUM(error_code));
454
635
  return r;
455
636
  }
@@ -474,20 +655,33 @@ static int is_valid_url(const char *url) {
474
655
  return _r; \
475
656
  } while (0)
476
657
 
477
- static CURLcode setup_basic_options(CURL *easy, const char *url_str, long timeout_sec,
658
+ static CURLcode setup_basic_options(CURL *easy, const char *url_str, const request_options_t *opts,
478
659
  request_ctx_t *ctx) {
660
+ long timeout_ms = opts->timeout_sec * 1000L;
661
+
662
+ if (opts->deadline_ms > 0) {
663
+ long long remaining = opts->deadline_ms - fast_now_ms();
664
+ if (remaining < 1)
665
+ remaining = 1;
666
+ if (remaining < (long long)timeout_ms)
667
+ timeout_ms = (long)remaining;
668
+ }
669
+
479
670
  CURL_SETOPT_CHECK(easy, CURLOPT_URL, url_str);
480
671
  CURL_SETOPT_CHECK(easy, CURLOPT_WRITEFUNCTION, write_callback);
481
672
  CURL_SETOPT_CHECK(easy, CURLOPT_WRITEDATA, &ctx->body);
482
673
  CURL_SETOPT_CHECK(easy, CURLOPT_HEADERFUNCTION, header_callback);
483
674
  CURL_SETOPT_CHECK(easy, CURLOPT_HEADERDATA, &ctx->headers);
484
- CURL_SETOPT_CHECK(easy, CURLOPT_TIMEOUT, timeout_sec);
675
+ CURL_SETOPT_CHECK(easy, CURLOPT_TIMEOUT_MS, timeout_ms);
676
+ CURL_SETOPT_CHECK(easy, CURLOPT_CONNECTTIMEOUT_MS, opts->connect_timeout_ms);
485
677
  CURL_SETOPT_CHECK(easy, CURLOPT_NOSIGNAL, 1L);
486
- CURL_SETOPT_CHECK(easy, CURLOPT_FOLLOWLOCATION, 1L);
487
- CURL_SETOPT_CHECK(easy, CURLOPT_MAXREDIRS, MAX_REDIRECTS);
678
+ CURL_SETOPT_CHECK(easy, CURLOPT_FOLLOWLOCATION, opts->follow_redirects);
679
+ CURL_SETOPT_CHECK(easy, CURLOPT_MAXREDIRS, opts->max_redirects);
488
680
  CURL_SETOPT_CHECK(easy, CURLOPT_ACCEPT_ENCODING, "");
489
681
  CURL_SETOPT_CHECK(easy, CURLOPT_PRIVATE, (char *)ctx);
490
682
  CURL_SETOPT_CHECK(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
683
+ if (fast_share)
684
+ CURL_SETOPT_CHECK(easy, CURLOPT_SHARE, fast_share);
491
685
  return CURLE_OK;
492
686
  }
493
687
 
@@ -518,12 +712,13 @@ typedef struct {
518
712
  int post;
519
713
  int nobody;
520
714
  int allows_body;
715
+ int idempotent;
521
716
  } http_method_t;
522
717
 
523
718
  static const http_method_t HTTP_METHODS[] = {
524
- {"GET", NULL, 0, 0, 0}, {"POST", NULL, 1, 0, 1}, {"PUT", "PUT", 0, 0, 1},
525
- {"DELETE", "DELETE", 0, 0, 1}, {"PATCH", "PATCH", 0, 0, 1}, {"HEAD", NULL, 0, 1, 0},
526
- {"OPTIONS", "OPTIONS", 0, 0, 0},
719
+ {"GET", NULL, 0, 0, 0, 1}, {"POST", NULL, 1, 0, 1, 0}, {"PUT", "PUT", 0, 0, 1, 1},
720
+ {"DELETE", "DELETE", 0, 0, 1, 1}, {"PATCH", "PATCH", 0, 0, 1, 0}, {"HEAD", NULL, 0, 1, 0, 1},
721
+ {"OPTIONS", "OPTIONS", 0, 0, 0, 1},
527
722
  };
528
723
 
529
724
  static const http_method_t *find_http_method(const char *name) {
@@ -544,29 +739,52 @@ static CURLcode apply_http_method(CURL *easy, const http_method_t *method) {
544
739
  return CURLE_OK;
545
740
  }
546
741
 
547
- static CURLcode setup_method_and_body(CURL *easy, VALUE method_value, VALUE body) {
548
- const char *name = NIL_P(method_value) ? "GET" : StringValueCStr(method_value);
549
- const http_method_t *method = find_http_method(name);
742
+ static int setup_method_and_body(request_ctx_t *ctx, VALUE method_value, VALUE body) {
743
+ const http_method_t *method;
550
744
  int has_body = !NIL_P(body);
745
+ char name[32];
551
746
 
552
- if (!method)
553
- rb_raise(rb_eArgError, "Unsupported HTTP method: %s", name);
554
- if (has_body && !method->allows_body)
555
- rb_raise(rb_eArgError, "%s requests must not include a body", method->name);
747
+ if (NIL_P(method_value)) {
748
+ memcpy(name, "GET", 4);
749
+ } else {
750
+ if (!RB_TYPE_P(method_value, T_STRING)) {
751
+ SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
752
+ return 0;
753
+ }
754
+ long len = RSTRING_LEN(method_value);
755
+ if (len <= 0 || len >= (long)sizeof(name)) {
756
+ SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
757
+ return 0;
758
+ }
759
+ memcpy(name, RSTRING_PTR(method_value), (size_t)len);
760
+ name[len] = '\0';
761
+ }
762
+
763
+ method = find_http_method(name);
764
+ if (!method) {
765
+ SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
766
+ return 0;
767
+ }
768
+ if (has_body && !method->allows_body) {
769
+ SET_SETUP_ERROR(ctx, "This HTTP method must not include a body", 1);
770
+ return 0;
771
+ }
772
+
773
+ ctx->idempotent = method->idempotent;
556
774
 
557
- CURLcode res = apply_http_method(easy, method);
558
- if (res != CURLE_OK)
559
- return res;
775
+ if (apply_http_method(ctx->easy, method) != CURLE_OK) {
776
+ SET_SETUP_ERROR(ctx, "Failed to configure HTTP method", 0);
777
+ return 0;
778
+ }
560
779
 
561
- if (has_body) {
562
- res = set_body(easy, body);
563
- if (res != CURLE_OK)
564
- return res;
780
+ if (has_body && set_body(ctx->easy, body) != CURLE_OK) {
781
+ SET_SETUP_ERROR(ctx, "Failed to configure request body", 0);
782
+ return 0;
565
783
  }
566
784
 
567
785
  RB_GC_GUARD(method_value);
568
786
  RB_GC_GUARD(body);
569
- return CURLE_OK;
787
+ return 1;
570
788
  }
571
789
 
572
790
  static void append_request_header(request_ctx_t *ctx, const char *buf) {
@@ -608,23 +826,42 @@ static void append_formatted_header(request_ctx_t *ctx, const char *key, long ke
608
826
  free(line);
609
827
  }
610
828
 
829
+ static VALUE header_part_to_str(VALUE v) {
830
+ if (RB_TYPE_P(v, T_STRING))
831
+ return v;
832
+ if (SYMBOL_P(v))
833
+ return rb_sym2str(v);
834
+ return rb_String(v);
835
+ }
836
+
611
837
  static int header_iter_cb(VALUE key, VALUE val, VALUE arg) {
612
838
  request_ctx_t *ctx = (request_ctx_t *)arg;
613
- VALUE key_str = rb_String(key);
614
- VALUE val_str = NIL_P(val) ? Qnil : rb_String(val);
615
- const char *k = RSTRING_PTR(key_str);
616
- long klen = RSTRING_LEN(key_str);
839
+ VALUE key_str, val_str;
840
+ const char *k;
841
+ long klen;
617
842
  const char *v = NULL;
618
843
  long vlen = 0;
619
844
 
620
- if (!is_valid_header_name(k, klen) || contains_header_injection(k, klen))
621
- rb_raise(rb_eArgError, "Invalid HTTP header name");
845
+ if (ctx->setup_error)
846
+ return ST_STOP;
847
+
848
+ key_str = header_part_to_str(key);
849
+ val_str = NIL_P(val) ? Qnil : header_part_to_str(val);
850
+ k = RSTRING_PTR(key_str);
851
+ klen = RSTRING_LEN(key_str);
852
+
853
+ if (!is_valid_header_name(k, klen) || contains_header_injection(k, klen)) {
854
+ SET_SETUP_ERROR(ctx, "Invalid HTTP header name", 1);
855
+ return ST_STOP;
856
+ }
622
857
 
623
858
  if (!NIL_P(val_str) && RSTRING_LEN(val_str) > 0) {
624
859
  v = RSTRING_PTR(val_str);
625
860
  vlen = RSTRING_LEN(val_str);
626
- if (contains_header_injection(v, vlen))
627
- rb_raise(rb_eArgError, "Invalid HTTP header value");
861
+ if (contains_header_injection(v, vlen)) {
862
+ SET_SETUP_ERROR(ctx, "Invalid HTTP header value", 1);
863
+ return ST_STOP;
864
+ }
628
865
  }
629
866
 
630
867
  append_formatted_header(ctx, k, klen, v, vlen);
@@ -634,40 +871,58 @@ static int header_iter_cb(VALUE key, VALUE val, VALUE arg) {
634
871
  return ST_CONTINUE;
635
872
  }
636
873
 
637
- static int setup_easy_handle(request_ctx_t *ctx, VALUE request, long timeout_sec) {
638
- Check_Type(request, T_HASH);
874
+ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, const request_options_t *opts) {
875
+ if (!RB_TYPE_P(request, T_HASH)) {
876
+ SET_SETUP_ERROR(ctx, "Request must be a Hash", 1);
877
+ return 0;
878
+ }
639
879
 
640
880
  VALUE url = hash_aref_key(request, KEY_URL);
641
881
  VALUE method = hash_aref_key(request, KEY_METHOD);
642
882
  VALUE headers = hash_aref_key(request, KEY_HEADERS);
643
883
  VALUE body = hash_aref_key(request, KEY_BODY);
644
884
 
645
- if (NIL_P(url))
885
+ if (NIL_P(url)) {
886
+ SET_SETUP_ERROR(ctx, "Missing :url", 0);
887
+ return 0;
888
+ }
889
+ if (!RB_TYPE_P(url, T_STRING) || memchr(RSTRING_PTR(url), '\0', RSTRING_LEN(url))) {
890
+ SET_SETUP_ERROR(ctx, "Invalid URL", 1);
646
891
  return 0;
892
+ }
647
893
 
648
894
  const char *url_str = StringValueCStr(url);
649
- if (!is_valid_url(url_str))
650
- rb_raise(rb_eArgError, "Invalid URL: %s", url_str);
895
+ if (!is_valid_url(url_str)) {
896
+ SET_SETUP_ERROR(ctx, "Invalid URL (expected http:// or https://, max 2048 bytes)", 1);
897
+ return 0;
898
+ }
651
899
 
652
- CURLcode res = setup_basic_options(ctx->easy, url_str, timeout_sec, ctx);
653
- if (res != CURLE_OK)
900
+ if (setup_basic_options(ctx->easy, url_str, opts, ctx) != CURLE_OK) {
901
+ SET_SETUP_ERROR(ctx, "Failed to configure request", 0);
654
902
  return 0;
903
+ }
655
904
 
656
- res = setup_security_options(ctx->easy);
657
- if (res != CURLE_OK)
905
+ if (setup_security_options(ctx->easy) != CURLE_OK) {
906
+ SET_SETUP_ERROR(ctx, "Failed to configure TLS options", 0);
658
907
  return 0;
908
+ }
659
909
 
660
- res = setup_method_and_body(ctx->easy, method, body);
661
- if (res != CURLE_OK)
910
+ if (!setup_method_and_body(ctx, method, body))
662
911
  return 0;
663
912
 
664
913
  if (!NIL_P(headers)) {
665
- Check_Type(headers, T_HASH);
914
+ if (!RB_TYPE_P(headers, T_HASH)) {
915
+ SET_SETUP_ERROR(ctx, ":headers must be a Hash", 1);
916
+ return 0;
917
+ }
666
918
  rb_hash_foreach(headers, header_iter_cb, (VALUE)ctx);
919
+ if (ctx->setup_error)
920
+ return 0;
667
921
  if (ctx->req_headers) {
668
- res = curl_easy_setopt(ctx->easy, CURLOPT_HTTPHEADER, ctx->req_headers);
669
- if (res != CURLE_OK)
922
+ if (curl_easy_setopt(ctx->easy, CURLOPT_HTTPHEADER, ctx->req_headers) != CURLE_OK) {
923
+ SET_SETUP_ERROR(ctx, "Failed to set request headers", 0);
670
924
  return 0;
925
+ }
671
926
  }
672
927
  }
673
928
 
@@ -679,14 +934,25 @@ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, long timeout_sec
679
934
  return 1;
680
935
  }
681
936
 
937
+ /* Poll until something actually completes, or the slice budget expires.
938
+ Previously this returned after every single 50ms poll, which under a fiber
939
+ scheduler meant a fresh OS thread twenty times a second. Cancellation still
940
+ breaks out immediately via curl_multi_wakeup. */
682
941
  static void *poll_without_gvl(void *arg) {
683
942
  multi_session_t *s = (multi_session_t *)arg;
684
- if (s->cancelled)
685
- return NULL;
943
+ long long started = fast_now_ms();
944
+ int before = s->still_running;
945
+
946
+ while (!s->cancelled) {
947
+ int numfds = 0;
948
+ curl_multi_poll(s->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
949
+ curl_multi_perform(s->multi, &s->still_running);
686
950
 
687
- int numfds = 0;
688
- curl_multi_poll(s->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
689
- curl_multi_perform(s->multi, &s->still_running);
951
+ if (s->still_running == 0 || s->still_running < before)
952
+ break;
953
+ if (fast_now_ms() - started >= POLL_SLICE_MS)
954
+ break;
955
+ }
690
956
  return NULL;
691
957
  }
692
958
 
@@ -700,28 +966,52 @@ static void unblock_perform(void *arg) {
700
966
 
701
967
  typedef struct {
702
968
  VALUE results;
969
+ VALUE accept;
703
970
  int completed;
704
971
  int target;
705
972
  int stream;
706
973
  } completion_ctx_t;
707
974
 
708
- static VALUE build_result_pair(int index, VALUE response) {
709
- return rb_ary_new_from_args(2, INT2NUM(index), response);
975
+ static VALUE build_result_pair(long index, VALUE response) {
976
+ return rb_ary_new_from_args(2, LONG2NUM(index), response);
710
977
  }
711
978
 
712
- static int record_immediate_error(completion_ctx_t *cctx, int index, const char *message) {
713
- if (cctx->stream || cctx->target > 0) {
714
- VALUE pair = build_result_pair(index, build_error_response(message));
979
+ static int response_accepted(completion_ctx_t *cctx, VALUE response) {
980
+ if (cctx->target <= 0 || NIL_P(cctx->accept))
981
+ return 1;
715
982
 
716
- if (cctx->stream)
717
- rb_yield(pair);
718
- else
719
- rb_ary_push(cctx->results, pair);
983
+ return RTEST(rb_funcall(cctx->accept, rb_intern("call"), 1, response));
984
+ }
985
+
986
+ static int record_response(completion_ctx_t *cctx, long index, VALUE response) {
987
+ VALUE pair = build_result_pair(index, response);
720
988
 
989
+ if (cctx->stream) {
990
+ rb_yield(pair);
721
991
  cctx->completed++;
722
- if (cctx->target > 0 && cctx->completed >= cctx->target)
723
- return 1;
992
+ return 0;
993
+ }
994
+
995
+ if (cctx->target > 0) {
996
+ if (!response_accepted(cctx, response))
997
+ return 0;
998
+
999
+ rb_ary_push(cctx->results, pair);
1000
+ cctx->completed++;
1001
+ return cctx->completed >= cctx->target;
724
1002
  }
1003
+
1004
+ rb_ary_store(cctx->results, index, pair);
1005
+ cctx->completed++;
1006
+ return 0;
1007
+ }
1008
+
1009
+ static int record_immediate_error(completion_ctx_t *cctx, long index, const char *message,
1010
+ int attempts) {
1011
+ if (cctx->stream || cctx->target > 0)
1012
+ return record_response(cctx, index,
1013
+ build_error_response(message, ERR_INVALID_REQUEST, attempts));
1014
+
725
1015
  return 0;
726
1016
  }
727
1017
 
@@ -751,21 +1041,16 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
751
1041
  curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &ctx->http_status);
752
1042
 
753
1043
  if (cctx->stream || cctx->target > 0) {
754
- VALUE response = (msg->data.result == CURLE_OK)
755
- ? build_response(ctx)
756
- : build_error_response_with_code(
757
- curl_easy_strerror(msg->data.result), (int)msg->data.result);
758
- VALUE pair = build_result_pair(ctx->index, response);
759
-
760
- if (cctx->stream)
761
- rb_yield(pair);
762
- else
763
- rb_ary_push(cctx->results, pair);
1044
+ VALUE response =
1045
+ (msg->data.result == CURLE_OK)
1046
+ ? build_response(ctx)
1047
+ : build_error_response_with_code(curl_easy_strerror(msg->data.result),
1048
+ (int)msg->data.result, ctx->attempts);
1049
+ if (record_response(cctx, ctx->index, response))
1050
+ return 1;
1051
+ } else {
1052
+ cctx->completed++;
764
1053
  }
765
-
766
- cctx->completed++;
767
- if (cctx->target > 0 && cctx->completed >= cctx->target)
768
- return 1;
769
1054
  }
770
1055
 
771
1056
  return 0;
@@ -782,30 +1067,34 @@ static int next_pending_index(multi_session_t *session) {
782
1067
  }
783
1068
 
784
1069
  static int activate_request(multi_session_t *session, VALUE requests, int idx, int *invalid,
785
- long timeout_sec) {
1070
+ const request_options_t *opts) {
786
1071
  request_ctx_t *ctx = &session->requests[idx];
787
1072
 
788
1073
  if (invalid[idx] || ctx->done)
789
1074
  return 0;
790
1075
 
791
- if (!request_ctx_prepare_easy(ctx))
1076
+ if (!request_ctx_prepare_easy(ctx)) {
1077
+ SET_SETUP_ERROR(ctx, "Failed to allocate a curl handle", 0);
792
1078
  return 0;
1079
+ }
793
1080
 
794
- if (!setup_easy_handle(ctx, rb_ary_entry(requests, idx), timeout_sec))
1081
+ if (!setup_easy_handle(ctx, rb_ary_entry(requests, idx), opts))
795
1082
  return 0;
796
1083
 
797
- CURLMcode mc = curl_multi_add_handle(session->multi, ctx->easy);
798
- if (mc != CURLM_OK)
1084
+ if (curl_multi_add_handle(session->multi, ctx->easy) != CURLM_OK) {
1085
+ SET_SETUP_ERROR(ctx, "Failed to schedule the request", 0);
799
1086
  return 0;
1087
+ }
800
1088
 
1089
+ ctx->attempts++;
801
1090
  ctx->active = 1;
802
1091
  ctx->done = 0;
803
1092
  session->active_count++;
804
1093
  return 1;
805
1094
  }
806
1095
 
807
- static int fill_slots(multi_session_t *session, VALUE requests, int *invalid, long timeout_sec,
808
- completion_ctx_t *cctx) {
1096
+ static int fill_slots(multi_session_t *session, VALUE requests, int *invalid,
1097
+ const request_options_t *opts, completion_ctx_t *cctx) {
809
1098
  while (session->active_count < session->max_connections) {
810
1099
  int idx = next_pending_index(session);
811
1100
  if (idx < 0)
@@ -813,11 +1102,14 @@ static int fill_slots(multi_session_t *session, VALUE requests, int *invalid, lo
813
1102
 
814
1103
  request_ctx_t *ctx = &session->requests[idx];
815
1104
 
816
- if (!activate_request(session, requests, idx, invalid, timeout_sec)) {
1105
+ if (!activate_request(session, requests, idx, invalid, opts)) {
817
1106
  invalid[idx] = 1;
818
1107
  ctx->done = 1;
819
1108
  ctx->active = 0;
820
- if (record_immediate_error(cctx, idx, "Invalid request configuration"))
1109
+ if (record_immediate_error(cctx, idx,
1110
+ ctx->setup_error ? ctx->setup_error
1111
+ : "Invalid request configuration",
1112
+ ctx->attempts))
821
1113
  return 1;
822
1114
  }
823
1115
  }
@@ -838,13 +1130,14 @@ static void prepare_pending(multi_session_t *session, int *indices, int count) {
838
1130
  }
839
1131
 
840
1132
  static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VALUE requests,
841
- int *invalid, long timeout_sec, int *indices, int indices_count) {
1133
+ int *invalid, const request_options_t *opts, int *indices,
1134
+ int indices_count) {
842
1135
  #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
843
1136
  VALUE scheduler = current_fiber_scheduler();
844
1137
  #endif
845
1138
  prepare_pending(session, indices, indices_count);
846
1139
 
847
- if (fill_slots(session, requests, invalid, timeout_sec, cctx))
1140
+ if (fill_slots(session, requests, invalid, opts, cctx))
848
1141
  return;
849
1142
 
850
1143
  curl_multi_perform(session->multi, &session->still_running);
@@ -852,7 +1145,10 @@ static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VAL
852
1145
  return;
853
1146
 
854
1147
  while (!session->cancelled && (session->active_count > 0 || pending_remaining(session))) {
855
- if (fill_slots(session, requests, invalid, timeout_sec, cctx))
1148
+ if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
1149
+ break;
1150
+
1151
+ if (fill_slots(session, requests, invalid, opts, cctx))
856
1152
  return;
857
1153
 
858
1154
  if (session->active_count == 0)
@@ -880,6 +1176,9 @@ static int is_default_retryable_curle(CURLcode code) {
880
1176
  }
881
1177
 
882
1178
  static int should_retry(request_ctx_t *ctx, retry_config_t *rc) {
1179
+ if (!ctx->idempotent && !rc->retry_non_idempotent)
1180
+ return 0;
1181
+
883
1182
  if (ctx->curl_result != CURLE_OK)
884
1183
  return is_default_retryable_curle(ctx->curl_result);
885
1184
 
@@ -930,6 +1229,26 @@ static void retry_config_init(retry_config_t *retry_cfg) {
930
1229
  retry_cfg->retry_delay_ms = DEFAULT_RETRY_DELAY;
931
1230
  retry_cfg->retry_http_codes = NULL;
932
1231
  retry_cfg->retry_http_count = 0;
1232
+ retry_cfg->retry_non_idempotent = 0;
1233
+ }
1234
+
1235
+ static long retry_backoff_ms(const retry_config_t *rc, int attempt) {
1236
+ long delay = rc->retry_delay_ms;
1237
+ long half;
1238
+
1239
+ if (delay <= 0)
1240
+ return 0;
1241
+
1242
+ for (int i = 0; i < attempt && delay < MAX_RETRY_DELAY_MS; i++)
1243
+ delay *= 2;
1244
+ if (delay > MAX_RETRY_DELAY_MS)
1245
+ delay = MAX_RETRY_DELAY_MS;
1246
+
1247
+ half = delay / 2;
1248
+ if (half > 0)
1249
+ delay = half + (long)(fast_rand() % (uint32_t)(half + 1));
1250
+
1251
+ return delay;
933
1252
  }
934
1253
 
935
1254
  static long parse_long_option(VALUE options, key_id_t key, const char *name, long min, long max,
@@ -986,8 +1305,16 @@ static void parse_retry_codes(VALUE options, retry_config_t *retry_cfg) {
986
1305
  retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(codes, i));
987
1306
  }
988
1307
 
989
- static void parse_options(VALUE options, long *timeout, int *max_conn, retry_config_t *retry_cfg) {
990
- *timeout = 30;
1308
+ static void parse_options(VALUE options, request_options_t *opts, int *max_conn,
1309
+ retry_config_t *retry_cfg) {
1310
+ VALUE flag;
1311
+
1312
+ opts->timeout_sec = 30;
1313
+ opts->connect_timeout_ms = DEFAULT_CONNECT_TIMEOUT_MS;
1314
+ opts->total_timeout_ms = 0;
1315
+ opts->follow_redirects = 1;
1316
+ opts->max_redirects = MAX_REDIRECTS;
1317
+ opts->deadline_ms = 0;
991
1318
  *max_conn = 20;
992
1319
  retry_config_init(retry_cfg);
993
1320
 
@@ -995,7 +1322,24 @@ static void parse_options(VALUE options, long *timeout, int *max_conn, retry_con
995
1322
  return;
996
1323
 
997
1324
  Check_Type(options, T_HASH);
998
- *timeout = parse_long_option(options, KEY_TIMEOUT, "timeout", 1, MAX_TIMEOUT, *timeout, NULL);
1325
+ opts->timeout_sec =
1326
+ parse_long_option(options, KEY_TIMEOUT, "timeout", 1, MAX_TIMEOUT, opts->timeout_sec, NULL);
1327
+ opts->connect_timeout_ms =
1328
+ parse_long_option(options, KEY_CONNECT_TIMEOUT, "connect_timeout", 1,
1329
+ MAX_CONNECT_TIMEOUT_MS, opts->connect_timeout_ms, NULL);
1330
+ opts->total_timeout_ms = parse_long_option(options, KEY_TOTAL_TIMEOUT, "total_timeout", 1,
1331
+ MAX_TOTAL_TIMEOUT_MS, opts->total_timeout_ms, NULL);
1332
+ opts->max_redirects = parse_long_option(options, KEY_MAX_REDIRECTS, "max_redirects", 0, 100,
1333
+ opts->max_redirects, NULL);
1334
+
1335
+ flag = hash_aref_key(options, KEY_FOLLOW_REDIRECTS);
1336
+ if (!NIL_P(flag))
1337
+ opts->follow_redirects = RTEST(flag) ? 1L : 0L;
1338
+
1339
+ flag = hash_aref_key(options, KEY_RETRY_NON_IDEMPOTENT);
1340
+ if (!NIL_P(flag))
1341
+ retry_cfg->retry_non_idempotent = RTEST(flag) ? 1 : 0;
1342
+
999
1343
  *max_conn = parse_int_option(options, KEY_CONNECTIONS, "connections", 1, MAX_CONNECTIONS,
1000
1344
  *max_conn, NULL);
1001
1345
  retry_cfg->max_retries = parse_int_option(options, KEY_RETRIES, "retries", 0, MAX_RETRIES,
@@ -1074,7 +1418,7 @@ typedef struct {
1074
1418
  multi_session_t *session;
1075
1419
  int *invalid;
1076
1420
  retry_config_t *retry_cfg;
1077
- long timeout_sec;
1421
+ request_options_t *opts;
1078
1422
  } execute_args_t;
1079
1423
 
1080
1424
  static VALUE internal_execute_body(VALUE arg) {
@@ -1083,7 +1427,8 @@ static VALUE internal_execute_body(VALUE arg) {
1083
1427
  multi_session_t *session = ea->session;
1084
1428
  int *invalid = ea->invalid;
1085
1429
  retry_config_t *retry_cfg = ea->retry_cfg;
1086
- long timeout_sec = ea->timeout_sec;
1430
+ request_options_t *opts = ea->opts;
1431
+ int deadline_hit = 0;
1087
1432
  int count = session->count;
1088
1433
  int target = ea->target;
1089
1434
  int stream = ea->stream;
@@ -1093,19 +1438,31 @@ static VALUE internal_execute_body(VALUE arg) {
1093
1438
 
1094
1439
  completion_ctx_t cctx;
1095
1440
  cctx.results = stream ? Qnil : ((target > 0) ? rb_ary_new2(target) : rb_ary_new2(count));
1441
+ cctx.accept = target > 0 && !NIL_P(ea->options) ? hash_aref_key(ea->options, KEY_ACCEPT) : Qnil;
1096
1442
  cctx.completed = 0;
1097
1443
  cctx.target = target;
1098
1444
  cctx.stream = stream;
1099
1445
 
1446
+ if (!NIL_P(cctx.accept) && !rb_respond_to(cctx.accept, rb_intern("call")))
1447
+ rb_raise(rb_eArgError, "accept must respond to #call");
1448
+
1100
1449
  if (!stream && target <= 0) {
1101
1450
  for (int i = 0; i < count; i++)
1102
1451
  rb_ary_store(cctx.results, i, Qnil);
1103
1452
  }
1104
1453
 
1105
- run_multi_loop(session, &cctx, requests, invalid, timeout_sec, NULL, count);
1454
+ if (opts->total_timeout_ms > 0)
1455
+ opts->deadline_ms = fast_now_ms() + opts->total_timeout_ms;
1456
+
1457
+ run_multi_loop(session, &cctx, requests, invalid, opts, NULL, count);
1106
1458
 
1107
1459
  if (!stream && target <= 0 && retry_cfg->max_retries > 0) {
1108
1460
  for (int attempt = 0; attempt < retry_cfg->max_retries; attempt++) {
1461
+ if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms) {
1462
+ deadline_hit = 1;
1463
+ break;
1464
+ }
1465
+
1109
1466
  int *retry_indices = malloc(sizeof(int) * (size_t)count);
1110
1467
  if (!retry_indices)
1111
1468
  rb_raise(rb_eNoMemError, "failed to allocate retry index array");
@@ -1123,7 +1480,18 @@ static VALUE internal_execute_body(VALUE arg) {
1123
1480
  break;
1124
1481
  }
1125
1482
 
1126
- retry_delay_sleep(retry_cfg->retry_delay_ms);
1483
+ long backoff = retry_backoff_ms(retry_cfg, attempt);
1484
+ if (opts->deadline_ms > 0) {
1485
+ long long left = opts->deadline_ms - fast_now_ms();
1486
+ if (left <= 0) {
1487
+ deadline_hit = 1;
1488
+ free(retry_indices);
1489
+ break;
1490
+ }
1491
+ if (backoff > (long)left)
1492
+ backoff = (long)left;
1493
+ }
1494
+ retry_delay_sleep(backoff);
1127
1495
 
1128
1496
  int runnable_count = 0;
1129
1497
  for (int r = 0; r < retry_count; r++) {
@@ -1133,6 +1501,7 @@ static VALUE internal_execute_body(VALUE arg) {
1133
1501
  if (!request_ctx_reset_for_retry(rc)) {
1134
1502
  invalid[idx] = 1;
1135
1503
  rc->done = 1;
1504
+ rc->setup_error = "Failed to allocate a curl handle for the retry";
1136
1505
  continue;
1137
1506
  }
1138
1507
 
@@ -1141,7 +1510,7 @@ static VALUE internal_execute_body(VALUE arg) {
1141
1510
 
1142
1511
  if (runnable_count > 0) {
1143
1512
  cctx.completed = 0;
1144
- run_multi_loop(session, &cctx, requests, invalid, timeout_sec, retry_indices,
1513
+ run_multi_loop(session, &cctx, requests, invalid, opts, retry_indices,
1145
1514
  runnable_count);
1146
1515
  }
1147
1516
 
@@ -1155,14 +1524,20 @@ static VALUE internal_execute_body(VALUE arg) {
1155
1524
  VALUE response;
1156
1525
 
1157
1526
  if (invalid[i]) {
1158
- response = build_error_response("Invalid request configuration");
1527
+ response = build_error_response(rc->setup_error ? rc->setup_error
1528
+ : "Invalid request configuration",
1529
+ ERR_INVALID_REQUEST, rc->attempts);
1159
1530
  } else if (!rc->done) {
1160
- response = build_error_response("Request was not completed");
1531
+ response =
1532
+ deadline_hit || (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
1533
+ ? build_error_response("Total timeout exceeded", ERR_DEADLINE, rc->attempts)
1534
+ : build_error_response("Request was not completed", ERR_NOT_COMPLETED,
1535
+ rc->attempts);
1161
1536
  } else if (rc->curl_result == CURLE_OK) {
1162
1537
  response = build_response(rc);
1163
1538
  } else {
1164
1539
  response = build_error_response_with_code(curl_easy_strerror(rc->curl_result),
1165
- (int)rc->curl_result);
1540
+ (int)rc->curl_result, rc->attempts);
1166
1541
  }
1167
1542
 
1168
1543
  rb_ary_store(cctx.results, i, build_result_pair(i, response));
@@ -1172,6 +1547,482 @@ static VALUE internal_execute_body(VALUE arg) {
1172
1547
  return stream ? Qnil : cctx.results;
1173
1548
  }
1174
1549
 
1550
+ typedef enum {
1551
+ LAZY_SLOT_FREE,
1552
+ LAZY_SLOT_PENDING,
1553
+ LAZY_SLOT_ACTIVE,
1554
+ LAZY_SLOT_RETRY_WAIT
1555
+ } lazy_slot_state_t;
1556
+
1557
+ typedef struct {
1558
+ multi_session_t session;
1559
+ request_options_t opts;
1560
+ retry_config_t retry_cfg;
1561
+ completion_ctx_t cctx;
1562
+ VALUE source;
1563
+ VALUE anchors;
1564
+ int *states;
1565
+ int *free_slots;
1566
+ int free_count;
1567
+ int *pending_slots;
1568
+ int pending_head;
1569
+ int pending_tail;
1570
+ int pending_count;
1571
+ long long *retry_at_ms;
1572
+ int capacity;
1573
+ int buffer_size;
1574
+ int occupied_count;
1575
+ long next_index;
1576
+ int deadline_hit;
1577
+ int stop;
1578
+ } lazy_execute_ctx_t;
1579
+
1580
+ static int lazy_pending_push(lazy_execute_ctx_t *ctx, int slot) {
1581
+ if (ctx->pending_count >= ctx->capacity)
1582
+ return 0;
1583
+
1584
+ ctx->pending_slots[ctx->pending_tail] = slot;
1585
+ ctx->pending_tail = (ctx->pending_tail + 1) % ctx->capacity;
1586
+ ctx->pending_count++;
1587
+ return 1;
1588
+ }
1589
+
1590
+ static int lazy_pending_pop(lazy_execute_ctx_t *ctx) {
1591
+ if (ctx->pending_count == 0)
1592
+ return -1;
1593
+
1594
+ int slot = ctx->pending_slots[ctx->pending_head];
1595
+ ctx->pending_head = (ctx->pending_head + 1) % ctx->capacity;
1596
+ ctx->pending_count--;
1597
+ return slot;
1598
+ }
1599
+
1600
+ static void lazy_release_slot(lazy_execute_ctx_t *ctx, int slot) {
1601
+ request_ctx_free(&ctx->session.requests[slot]);
1602
+ rb_ary_store(ctx->anchors, slot, Qnil);
1603
+ ctx->states[slot] = LAZY_SLOT_FREE;
1604
+ ctx->retry_at_ms[slot] = 0;
1605
+ ctx->free_slots[ctx->free_count++] = slot;
1606
+ if (ctx->occupied_count > 0)
1607
+ ctx->occupied_count--;
1608
+ }
1609
+
1610
+ static FAST_CURL_NORETURN void lazy_raise_setup_error(request_ctx_t *request) {
1611
+ VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
1612
+ rb_raise(klass, "%s (request %ld)",
1613
+ request->setup_error ? request->setup_error : "Invalid request configuration",
1614
+ request->index);
1615
+ }
1616
+
1617
+ static int lazy_finalize_error(lazy_execute_ctx_t *ctx, int slot, const char *message) {
1618
+ request_ctx_t *request = &ctx->session.requests[slot];
1619
+ VALUE response = build_error_response(message, ERR_INVALID_REQUEST, request->attempts);
1620
+ int stop = record_response(&ctx->cctx, request->index, response);
1621
+ lazy_release_slot(ctx, slot);
1622
+ return stop;
1623
+ }
1624
+
1625
+ static int lazy_activate_pending(lazy_execute_ctx_t *ctx) {
1626
+ while (ctx->session.active_count < ctx->session.max_connections && ctx->pending_count > 0) {
1627
+ int slot = lazy_pending_pop(ctx);
1628
+ request_ctx_t *request = &ctx->session.requests[slot];
1629
+ VALUE request_value = rb_ary_entry(ctx->anchors, slot);
1630
+
1631
+ if (!request_ctx_prepare_easy(request)) {
1632
+ if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle"))
1633
+ return 1;
1634
+ continue;
1635
+ }
1636
+
1637
+ if (!setup_easy_handle(request, request_value, &ctx->opts)) {
1638
+ if (request->setup_error_fatal)
1639
+ lazy_raise_setup_error(request);
1640
+ if (lazy_finalize_error(ctx, slot,
1641
+ request->setup_error ? request->setup_error
1642
+ : "Invalid request configuration"))
1643
+ return 1;
1644
+ continue;
1645
+ }
1646
+
1647
+ if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
1648
+ if (lazy_finalize_error(ctx, slot, "Failed to schedule the request"))
1649
+ return 1;
1650
+ continue;
1651
+ }
1652
+
1653
+ request->attempts++;
1654
+ request->active = 1;
1655
+ request->done = 0;
1656
+ ctx->states[slot] = LAZY_SLOT_ACTIVE;
1657
+ ctx->session.active_count++;
1658
+ }
1659
+
1660
+ return 0;
1661
+ }
1662
+
1663
+ static int lazy_activate_retries(lazy_execute_ctx_t *ctx) {
1664
+ long long now = fast_now_ms();
1665
+
1666
+ for (int slot = 0;
1667
+ slot < ctx->capacity && ctx->session.active_count < ctx->session.max_connections; slot++) {
1668
+ if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT || ctx->retry_at_ms[slot] > now)
1669
+ continue;
1670
+
1671
+ request_ctx_t *request = &ctx->session.requests[slot];
1672
+ VALUE request_value = rb_ary_entry(ctx->anchors, slot);
1673
+
1674
+ if (!request_ctx_reset_for_retry(request)) {
1675
+ if (lazy_finalize_error(ctx, slot, "Failed to allocate a curl handle for the retry"))
1676
+ return 1;
1677
+ continue;
1678
+ }
1679
+
1680
+ if (!setup_easy_handle(request, request_value, &ctx->opts)) {
1681
+ if (request->setup_error_fatal)
1682
+ lazy_raise_setup_error(request);
1683
+ if (lazy_finalize_error(ctx, slot,
1684
+ request->setup_error ? request->setup_error
1685
+ : "Invalid request configuration"))
1686
+ return 1;
1687
+ continue;
1688
+ }
1689
+
1690
+ if (curl_multi_add_handle(ctx->session.multi, request->easy) != CURLM_OK) {
1691
+ if (lazy_finalize_error(ctx, slot, "Failed to schedule the retry"))
1692
+ return 1;
1693
+ continue;
1694
+ }
1695
+
1696
+ request->attempts++;
1697
+ request->active = 1;
1698
+ request->done = 0;
1699
+ ctx->states[slot] = LAZY_SLOT_ACTIVE;
1700
+ ctx->retry_at_ms[slot] = 0;
1701
+ ctx->session.active_count++;
1702
+ }
1703
+
1704
+ return 0;
1705
+ }
1706
+
1707
+ static long long lazy_next_retry_at(const lazy_execute_ctx_t *ctx) {
1708
+ long long next = 0;
1709
+ for (int slot = 0; slot < ctx->capacity; slot++) {
1710
+ if (ctx->states[slot] != LAZY_SLOT_RETRY_WAIT)
1711
+ continue;
1712
+ if (next == 0 || ctx->retry_at_ms[slot] < next)
1713
+ next = ctx->retry_at_ms[slot];
1714
+ }
1715
+ return next;
1716
+ }
1717
+
1718
+ static int lazy_process_completed(lazy_execute_ctx_t *ctx) {
1719
+ CURLMsg *msg;
1720
+ int msgs_left;
1721
+
1722
+ while ((msg = curl_multi_info_read(ctx->session.multi, &msgs_left))) {
1723
+ if (msg->msg != CURLMSG_DONE)
1724
+ continue;
1725
+
1726
+ request_ctx_t *request = NULL;
1727
+ curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **)&request);
1728
+ if (!request || request->done)
1729
+ continue;
1730
+
1731
+ int slot = (int)(request - ctx->session.requests);
1732
+ if (slot < 0 || slot >= ctx->capacity)
1733
+ continue;
1734
+
1735
+ if (request->active) {
1736
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
1737
+ request->active = 0;
1738
+ if (ctx->session.active_count > 0)
1739
+ ctx->session.active_count--;
1740
+ }
1741
+
1742
+ request->done = 1;
1743
+ request->curl_result = msg->data.result;
1744
+ if (msg->data.result == CURLE_OK)
1745
+ curl_easy_getinfo(request->easy, CURLINFO_RESPONSE_CODE, &request->http_status);
1746
+
1747
+ if (request->attempts <= ctx->retry_cfg.max_retries &&
1748
+ should_retry(request, &ctx->retry_cfg)) {
1749
+ ctx->states[slot] = LAZY_SLOT_RETRY_WAIT;
1750
+ ctx->retry_at_ms[slot] =
1751
+ fast_now_ms() + retry_backoff_ms(&ctx->retry_cfg, request->attempts - 1);
1752
+ continue;
1753
+ }
1754
+
1755
+ VALUE response =
1756
+ (msg->data.result == CURLE_OK)
1757
+ ? build_response(request)
1758
+ : build_error_response_with_code(curl_easy_strerror(msg->data.result),
1759
+ (int)msg->data.result, request->attempts);
1760
+ int stop = record_response(&ctx->cctx, request->index, response);
1761
+ lazy_release_slot(ctx, slot);
1762
+ if (stop)
1763
+ return 1;
1764
+ }
1765
+
1766
+ return 0;
1767
+ }
1768
+
1769
+ typedef struct {
1770
+ multi_session_t *session;
1771
+ long long wake_at_ms;
1772
+ } lazy_poll_args_t;
1773
+
1774
+ static void *lazy_poll_without_gvl(void *arg) {
1775
+ lazy_poll_args_t *poll = (lazy_poll_args_t *)arg;
1776
+ multi_session_t *session = poll->session;
1777
+ long long started = fast_now_ms();
1778
+ int before = session->still_running;
1779
+
1780
+ while (!session->cancelled) {
1781
+ int numfds = 0;
1782
+ curl_multi_poll(session->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
1783
+ curl_multi_perform(session->multi, &session->still_running);
1784
+
1785
+ if (session->still_running == 0 || session->still_running < before)
1786
+ break;
1787
+ if (poll->wake_at_ms > 0 && fast_now_ms() >= poll->wake_at_ms)
1788
+ break;
1789
+ if (fast_now_ms() - started >= POLL_SLICE_MS)
1790
+ break;
1791
+ }
1792
+
1793
+ return NULL;
1794
+ }
1795
+
1796
+ static void lazy_wait(lazy_execute_ctx_t *ctx) {
1797
+ long long next_retry = lazy_next_retry_at(ctx);
1798
+
1799
+ if (ctx->session.active_count > 0) {
1800
+ lazy_poll_args_t poll = {.session = &ctx->session, .wake_at_ms = next_retry};
1801
+ #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1802
+ VALUE scheduler = current_fiber_scheduler();
1803
+ if (scheduler != Qnil)
1804
+ run_via_fiber_worker(scheduler, lazy_poll_without_gvl, &poll);
1805
+ else
1806
+ #endif
1807
+ rb_thread_call_without_gvl(lazy_poll_without_gvl, &poll, unblock_perform,
1808
+ &ctx->session);
1809
+ return;
1810
+ }
1811
+
1812
+ if (next_retry > 0) {
1813
+ long long delay = next_retry - fast_now_ms();
1814
+ if (delay > 0)
1815
+ retry_delay_sleep((long)delay);
1816
+ }
1817
+ }
1818
+
1819
+ static int lazy_deadline_reached(lazy_execute_ctx_t *ctx) {
1820
+ if (ctx->opts.deadline_ms <= 0 || fast_now_ms() < ctx->opts.deadline_ms)
1821
+ return 0;
1822
+
1823
+ ctx->deadline_hit = 1;
1824
+ ctx->stop = 1;
1825
+ return 1;
1826
+ }
1827
+
1828
+ static void lazy_progress(lazy_execute_ctx_t *ctx) {
1829
+ if (ctx->stop || lazy_deadline_reached(ctx))
1830
+ return;
1831
+
1832
+ if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
1833
+ ctx->stop = 1;
1834
+ return;
1835
+ }
1836
+
1837
+ curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
1838
+ if (lazy_process_completed(ctx)) {
1839
+ ctx->stop = 1;
1840
+ return;
1841
+ }
1842
+
1843
+ if (ctx->occupied_count == 0)
1844
+ return;
1845
+
1846
+ lazy_wait(ctx);
1847
+ if (lazy_process_completed(ctx))
1848
+ ctx->stop = 1;
1849
+ }
1850
+
1851
+ static VALUE lazy_each_request(VALUE yielded, VALUE arg, int argc, const VALUE *argv,
1852
+ VALUE blockarg) {
1853
+ (void)blockarg;
1854
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
1855
+ VALUE request = argc == 1 ? argv[0] : yielded;
1856
+
1857
+ while (!ctx->stop && ctx->occupied_count >= ctx->capacity)
1858
+ lazy_progress(ctx);
1859
+
1860
+ if (ctx->stop || lazy_deadline_reached(ctx))
1861
+ rb_iter_break();
1862
+
1863
+ if (ctx->next_index > INT_MAX)
1864
+ rb_raise(rb_eArgError, "lazy request index exceeds supported range");
1865
+
1866
+ int slot = ctx->free_slots[--ctx->free_count];
1867
+ request_ctx_init(&ctx->session.requests[slot], ctx->next_index++);
1868
+ rb_ary_store(ctx->anchors, slot, request);
1869
+ ctx->states[slot] = LAZY_SLOT_PENDING;
1870
+ ctx->occupied_count++;
1871
+ lazy_pending_push(ctx, slot);
1872
+
1873
+ if (lazy_activate_retries(ctx) || lazy_activate_pending(ctx)) {
1874
+ ctx->stop = 1;
1875
+ rb_iter_break();
1876
+ }
1877
+
1878
+ curl_multi_perform(ctx->session.multi, &ctx->session.still_running);
1879
+ if (lazy_process_completed(ctx)) {
1880
+ ctx->stop = 1;
1881
+ rb_iter_break();
1882
+ }
1883
+
1884
+ return Qnil;
1885
+ }
1886
+
1887
+ static void lazy_finalize_deadline(lazy_execute_ctx_t *ctx) {
1888
+ if (ctx->cctx.stream || ctx->cctx.target > 0)
1889
+ return;
1890
+
1891
+ for (int slot = 0; slot < ctx->capacity; slot++) {
1892
+ if (ctx->states[slot] == LAZY_SLOT_FREE)
1893
+ continue;
1894
+
1895
+ request_ctx_t *request = &ctx->session.requests[slot];
1896
+ if (request->active) {
1897
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
1898
+ request->active = 0;
1899
+ if (ctx->session.active_count > 0)
1900
+ ctx->session.active_count--;
1901
+ }
1902
+
1903
+ VALUE response =
1904
+ build_error_response("Total timeout exceeded", ERR_DEADLINE, request->attempts);
1905
+ record_response(&ctx->cctx, request->index, response);
1906
+ lazy_release_slot(ctx, slot);
1907
+ }
1908
+ }
1909
+
1910
+ static VALUE lazy_execute_body(VALUE arg) {
1911
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
1912
+
1913
+ if (ctx->opts.total_timeout_ms > 0)
1914
+ ctx->opts.deadline_ms = fast_now_ms() + ctx->opts.total_timeout_ms;
1915
+
1916
+ rb_block_call(ctx->source, rb_intern("each"), 0, NULL, lazy_each_request, (VALUE)ctx);
1917
+
1918
+ while (!ctx->stop && ctx->occupied_count > 0)
1919
+ lazy_progress(ctx);
1920
+
1921
+ if (ctx->deadline_hit)
1922
+ lazy_finalize_deadline(ctx);
1923
+
1924
+ RB_GC_GUARD(ctx->source);
1925
+ RB_GC_GUARD(ctx->anchors);
1926
+ return ctx->cctx.stream ? Qnil : ctx->cctx.results;
1927
+ }
1928
+
1929
+ static VALUE cleanup_lazy_execute(VALUE arg) {
1930
+ lazy_execute_ctx_t *ctx = (lazy_execute_ctx_t *)arg;
1931
+
1932
+ if (ctx->session.requests) {
1933
+ for (int slot = 0; slot < ctx->capacity; slot++) {
1934
+ request_ctx_t *request = &ctx->session.requests[slot];
1935
+ if (request->easy && request->active && ctx->session.multi)
1936
+ curl_multi_remove_handle(ctx->session.multi, request->easy);
1937
+ request_ctx_free(request);
1938
+ }
1939
+ free(ctx->session.requests);
1940
+ ctx->session.requests = NULL;
1941
+ }
1942
+
1943
+ free(ctx->states);
1944
+ ctx->states = NULL;
1945
+ free(ctx->free_slots);
1946
+ ctx->free_slots = NULL;
1947
+ free(ctx->pending_slots);
1948
+ ctx->pending_slots = NULL;
1949
+ free(ctx->retry_at_ms);
1950
+ ctx->retry_at_ms = NULL;
1951
+
1952
+ if (ctx->session.multi) {
1953
+ curl_multi_cleanup(ctx->session.multi);
1954
+ ctx->session.multi = NULL;
1955
+ }
1956
+
1957
+ if (ctx->retry_cfg.retry_http_codes) {
1958
+ free(ctx->retry_cfg.retry_http_codes);
1959
+ ctx->retry_cfg.retry_http_codes = NULL;
1960
+ }
1961
+
1962
+ return Qnil;
1963
+ }
1964
+
1965
+ static VALUE internal_lazy_execute(VALUE requests, VALUE options, int target, int stream) {
1966
+ if (!rb_respond_to(requests, rb_intern("each")))
1967
+ rb_raise(rb_eArgError, "requests must be an Array or respond to #each");
1968
+
1969
+ lazy_execute_ctx_t ctx;
1970
+ memset(&ctx, 0, sizeof(ctx));
1971
+
1972
+ int max_conn;
1973
+ parse_options(options, &ctx.opts, &max_conn, &ctx.retry_cfg);
1974
+ ctx.buffer_size = NIL_P(options) ? max_conn
1975
+ : parse_int_option(options, KEY_BUFFER, "buffer", 1,
1976
+ MAX_BUFFER, max_conn, NULL);
1977
+
1978
+ if (stream || target > 0) {
1979
+ if (ctx.retry_cfg.retries_explicit && ctx.retry_cfg.max_retries > 0 && stream)
1980
+ rb_warn(
1981
+ "FastCurl: retries are not supported in stream_execute, ignoring retries option");
1982
+ if (ctx.retry_cfg.retries_explicit && ctx.retry_cfg.max_retries > 0 && target > 0)
1983
+ rb_warn(
1984
+ "FastCurl: retries are not supported in first_execute, ignoring retries option");
1985
+ ctx.retry_cfg.max_retries = 0;
1986
+ }
1987
+
1988
+ ctx.capacity = max_conn + ctx.buffer_size;
1989
+ ctx.source = requests;
1990
+ ctx.anchors = rb_ary_new2(ctx.capacity);
1991
+ ctx.next_index = 0;
1992
+
1993
+ ctx.cctx.results = stream ? Qnil : rb_ary_new();
1994
+ ctx.cctx.accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
1995
+ ctx.cctx.completed = 0;
1996
+ ctx.cctx.target = target;
1997
+ ctx.cctx.stream = stream;
1998
+
1999
+ if (!NIL_P(ctx.cctx.accept) && !rb_respond_to(ctx.cctx.accept, rb_intern("call")))
2000
+ rb_raise(rb_eArgError, "accept must respond to #call");
2001
+
2002
+ multi_session_init(&ctx.session, curl_multi_init(), ctx.capacity, max_conn,
2003
+ ctx.opts.timeout_sec);
2004
+ if (!ctx.session.multi)
2005
+ rb_raise(rb_eNoMemError, "failed to initialize curl multi handle");
2006
+ multi_session_configure(ctx.session.multi, max_conn);
2007
+
2008
+ ctx.session.requests = calloc((size_t)ctx.capacity, sizeof(request_ctx_t));
2009
+ ctx.states = calloc((size_t)ctx.capacity, sizeof(int));
2010
+ ctx.free_slots = malloc(sizeof(int) * (size_t)ctx.capacity);
2011
+ ctx.pending_slots = malloc(sizeof(int) * (size_t)ctx.capacity);
2012
+ ctx.retry_at_ms = calloc((size_t)ctx.capacity, sizeof(long long));
2013
+
2014
+ if (!ctx.session.requests || !ctx.states || !ctx.free_slots || !ctx.pending_slots ||
2015
+ !ctx.retry_at_ms) {
2016
+ cleanup_lazy_execute((VALUE)&ctx);
2017
+ rb_raise(rb_eNoMemError, "failed to allocate lazy request state");
2018
+ }
2019
+
2020
+ for (int slot = 0; slot < ctx.capacity; slot++)
2021
+ ctx.free_slots[ctx.free_count++] = slot;
2022
+
2023
+ return rb_ensure(lazy_execute_body, (VALUE)&ctx, cleanup_lazy_execute, (VALUE)&ctx);
2024
+ }
2025
+
1175
2026
  #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1176
2027
  typedef struct {
1177
2028
  execute_args_t *ea;
@@ -1247,6 +2098,9 @@ static VALUE execute_with_fiber_scheduler(VALUE arg) {
1247
2098
  #endif
1248
2099
 
1249
2100
  static VALUE internal_execute(VALUE requests, VALUE options, int target, int stream) {
2101
+ if (!RB_TYPE_P(requests, T_ARRAY))
2102
+ return internal_lazy_execute(requests, options, target, stream);
2103
+
1250
2104
  Check_Type(requests, T_ARRAY);
1251
2105
 
1252
2106
  long count_long = RARRAY_LEN(requests);
@@ -1262,10 +2116,37 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1262
2116
  if (target > 0 && target > count)
1263
2117
  target = count;
1264
2118
 
1265
- long timeout_sec;
2119
+ request_options_t opts;
1266
2120
  int max_conn;
1267
2121
  retry_config_t retry_cfg;
1268
- parse_options(options, &timeout_sec, &max_conn, &retry_cfg);
2122
+ parse_options(options, &opts, &max_conn, &retry_cfg);
2123
+
2124
+ if (target > 0 && !NIL_P(options)) {
2125
+ VALUE accept = hash_aref_key(options, KEY_ACCEPT);
2126
+ if (!NIL_P(accept) && !rb_respond_to(accept, rb_intern("call")))
2127
+ rb_raise(rb_eArgError, "accept must respond to #call");
2128
+ }
2129
+
2130
+ for (int i = 0; i < count; i++) {
2131
+ request_ctx_t probe;
2132
+ request_ctx_init(&probe, i);
2133
+ if (!request_ctx_prepare_easy(&probe)) {
2134
+ request_ctx_free(&probe);
2135
+ continue;
2136
+ }
2137
+ setup_easy_handle(&probe, rb_ary_entry(requests, i), &opts);
2138
+ int fatal = probe.setup_error_fatal;
2139
+ const char *message = probe.setup_error;
2140
+ char buffer[128];
2141
+ if (fatal && message) {
2142
+ snprintf(buffer, sizeof(buffer), "%s (request %d)", message, i);
2143
+ }
2144
+ request_ctx_free(&probe);
2145
+ if (fatal) {
2146
+ VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
2147
+ rb_raise(klass, "%s", buffer);
2148
+ }
2149
+ }
1269
2150
 
1270
2151
  if (stream || target > 0) {
1271
2152
  if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && stream)
@@ -1279,7 +2160,7 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1279
2160
 
1280
2161
  multi_session_t session;
1281
2162
  int *invalid = NULL;
1282
- multi_session_init(&session, curl_multi_init(), count, max_conn, timeout_sec);
2163
+ multi_session_init(&session, curl_multi_init(), count, max_conn, opts.timeout_sec);
1283
2164
 
1284
2165
  cleanup_ctx_t cleanup = {.session = &session, .invalid = NULL, .retry_cfg = &retry_cfg};
1285
2166
 
@@ -1304,12 +2185,13 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1304
2185
  .session = &session,
1305
2186
  .invalid = invalid,
1306
2187
  .retry_cfg = &retry_cfg,
1307
- .timeout_sec = timeout_sec,
2188
+ .opts = &opts,
1308
2189
  };
1309
2190
 
1310
2191
  #ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
1311
2192
  VALUE scheduler = current_fiber_scheduler();
1312
- if (scheduler != Qnil && !stream) {
2193
+ VALUE accept = target > 0 && !NIL_P(options) ? hash_aref_key(options, KEY_ACCEPT) : Qnil;
2194
+ if (scheduler != Qnil && !stream && NIL_P(accept)) {
1313
2195
  scheduler_execute_ctx_t scheduler_ctx = {
1314
2196
  .ea = &ea,
1315
2197
  .scheduler = scheduler,
@@ -1326,7 +2208,6 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
1326
2208
  (VALUE)&cleanup);
1327
2209
  }
1328
2210
  #endif
1329
-
1330
2211
  return rb_ensure(internal_execute_body, (VALUE)&ea, cleanup_session, (VALUE)&cleanup);
1331
2212
  }
1332
2213
 
@@ -1366,6 +2247,7 @@ static VALUE rb_fast_curl_stream_execute(int argc, VALUE *argv, VALUE self) {
1366
2247
 
1367
2248
  void Init_fast_curl(void) {
1368
2249
  curl_global_init(CURL_GLOBAL_ALL);
2250
+ fast_share_init();
1369
2251
 
1370
2252
  for (int i = 0; i < KEY_LAST; i++) {
1371
2253
  fast_ids[i] = rb_intern(KEY_NAMES[i]);
@@ -1373,8 +2255,18 @@ void Init_fast_curl(void) {
1373
2255
  rb_gc_register_address(&fast_syms[i]);
1374
2256
  }
1375
2257
 
2258
+ for (int i = 0; i < ERR_LAST; i++) {
2259
+ error_syms[i] = ID2SYM(rb_intern(ERROR_KIND_NAMES[i]));
2260
+ rb_gc_register_address(&error_syms[i]);
2261
+ }
2262
+
1376
2263
  VALUE mFastCurl = rb_define_module("FastCurl");
1377
2264
 
2265
+ if (rb_const_defined_at(mFastCurl, rb_intern("Headers"))) {
2266
+ fast_cHeaders = rb_const_get_at(mFastCurl, rb_intern("Headers"));
2267
+ rb_gc_register_address(&fast_cHeaders);
2268
+ }
2269
+
1378
2270
  rb_define_module_function(mFastCurl, "execute", rb_fast_curl_execute, -1);
1379
2271
  rb_define_module_function(mFastCurl, "first_execute", rb_fast_curl_first_execute, -1);
1380
2272
  rb_define_module_function(mFastCurl, "stream_execute", rb_fast_curl_stream_execute, -1);