patron 1.0.0 → 1.1.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.
@@ -13,6 +13,13 @@
13
13
  * Thread#raise, Timeout and interpreter exit return promptly from any phase of a
14
14
  * transfer, including DNS and connect.
15
15
  *
16
+ * The same drive loop serves both shapes of request. A buffered request runs to completion
17
+ * and appends the body to a String. A streamed one (Session#stream) is driven by its consumer:
18
+ * transfer_start runs until the response head is complete, and every transfer_read runs it
19
+ * again until some body bytes have landed in the String the caller handed in. Between two
20
+ * reads the transfer is either paused or simply not driven, so no Ruby code ever runs
21
+ * inside libcurl on behalf of the consumer.
22
+ *
16
23
  * Requires Ruby >= 3.3 and libcurl >= 7.81.0.
17
24
  */
18
25
  #include <ruby.h>
@@ -24,6 +31,7 @@
24
31
 
25
32
  #include <stdio.h>
26
33
  #include <string.h>
34
+ #include <time.h>
27
35
  #include <signal.h>
28
36
  #include <unistd.h>
29
37
  #include <sys/stat.h>
@@ -38,6 +46,7 @@
38
46
 
39
47
  #define INTERRUPT_ABORT 1
40
48
  #define INTERRUPT_DOWNLOAD_OVERFLOW 2
49
+ #define INTERRUPT_REWIND_FAILED 3
41
50
 
42
51
  /* How long one curl_multi_poll() may sleep. libcurl wakes us earlier on socket activity,
43
52
  * on its own timers, and on curl_multi_wakeup(); this is only the upper bound. */
@@ -63,6 +72,7 @@ static VALUE eTooManyRedirects;
63
72
  static VALUE eAborted;
64
73
  static VALUE eDownloadLimitExceeded;
65
74
  static VALUE eClientCertificateError;
75
+ static VALUE eFiberError;
66
76
 
67
77
  /* Frozen, Ractor-shareable lookup tables exposed as Patron::Request::SSL_VERSIONS and
68
78
  * Patron::Request::HTTP_VERSIONS: version name => libcurl constant. */
@@ -76,15 +86,16 @@ static VALUE hHTTPVersions;
76
86
  X(insecure) X(ssl_version) X(http_version) X(ssl_cert) X(ssl_cert_type) \
77
87
  X(ssl_key) X(ssl_key_password) X(cacert) X(buffer_size) X(download_byte_limit) \
78
88
  X(progress_callback) X(automatic_content_encoding) X(force_ipv4) \
79
- X(upload_data) X(file_name) X(multipart)
89
+ X(upload_data) X(file_name) X(multipart) X(read_timeout)
80
90
 
81
91
  #define DECLARE_ID(name) static ID id_##name;
82
92
  REQUEST_ATTRIBUTES(DECLARE_ID)
83
93
  #undef DECLARE_ID
84
94
 
85
95
  static ID id_to_path, id_to_s, id_response_class, id_default_response_charset;
96
+ static ID id_read, id_readpartial, id_size, id_pos, id_seek, id_rewind;
86
97
  static ID id_get, id_post, id_put, id_patch, id_delete, id_head;
87
- static ID id_ivar_curl_code;
98
+ static ID id_ivar_curl_code, id_raise;
88
99
  static ID id_url_kw, id_status_kw, id_redirect_count_kw, id_header_data_kw, id_body_kw,
89
100
  id_default_charset_kw, id_timings_kw;
90
101
 
@@ -103,14 +114,36 @@ struct patron_session {
103
114
  CURL *easy;
104
115
  curl_mime *mime;
105
116
  struct curl_slist *headers;
106
- FILE *download_file;
107
117
  FILE *request_body_file;
118
+ curl_off_t request_content_length; /* from a Content-Length request header, or -1 */
119
+ int request_transfer_encoding; /* whether the request headers carry a Transfer-Encoding */
120
+ curl_off_t upload_io_origin; /* position of upload_io when the request started */
121
+ int upload_io_partial; /* whether upload_io gets #readpartial rather than #read */
108
122
  char error_buf[CURL_ERROR_SIZE];
109
123
  curl_off_t download_byte_limit;
110
124
  VALUE progress_proc;
111
125
  VALUE upload_data; /* pinned while libcurl reads it through CURLOPT_POSTFIELDS */
112
126
  VALUE header_buf; /* binary String receiving the raw headers */
113
127
  VALUE body_buf; /* binary String receiving the body, or nil when downloading to a file */
128
+ VALUE upload_io; /* object responding to #read the body is streamed from, or nil */
129
+ VALUE upload_buf; /* String upload_io reads into, copied into libcurl's buffer */
130
+
131
+ /* Per request, the state the drive loop and the streaming primitives share */
132
+ int streaming; /* the body goes to read_buf instead of body_buf */
133
+ int paused; /* body_write_cb answered CURL_WRITEFUNC_PAUSE */
134
+ int head_complete; /* the final response head is in header_buf */
135
+ int done; /* CURLMSG_DONE was seen, `result` holds its code */
136
+ int read_timed_out;
137
+ CURLcode result;
138
+ long head_length; /* header_buf bytes up to head-complete; trailers follow */
139
+ size_t redeliver_skip; /* bytes at the start of the next delivery already handed out */
140
+ long read_limit; /* length read_buf may grow to, -1 for unbounded */
141
+ double read_timeout; /* seconds allowed between two deliveries, 0 for none */
142
+ double last_activity; /* monotonic seconds of the last delivery */
143
+ VALUE read_buf; /* the caller's String, set only during transfer_read */
144
+ VALUE owner; /* the Fiber that started the transfer */
145
+ VALUE owner_thread; /* and its Thread */
146
+ int in_scheduler_wait; /* the owner is parked in the Fiber scheduler by wait_with_scheduler() */
114
147
 
115
148
  /* A callback may not let a Ruby exception unwind through libcurl's frames. The tag and
116
149
  * errinfo are parked here, the transfer is aborted, and perform() re-raises afterwards. */
@@ -128,7 +161,18 @@ static void session_free(void *ptr) {
128
161
  struct patron_session *s = ptr;
129
162
  int i;
130
163
 
131
- /* A transfer cannot be in flight here: perform() keeps self on the stack. */
164
+ /* A stream abandoned with its Fiber is still in flight here. No Ruby may run from this
165
+ * point on, and debug_cb would write to an IO that may have been collected already. */
166
+ s->debug_io = Qnil;
167
+ if (s->easy) {
168
+ curl_easy_setopt(s->easy, CURLOPT_VERBOSE, 0L);
169
+ curl_easy_setopt(s->easy, CURLOPT_NOPROGRESS, 1L);
170
+ if (s->multi) curl_multi_remove_handle(s->multi, s->easy);
171
+ curl_easy_cleanup(s->easy);
172
+ }
173
+ if (s->mime) curl_mime_free(s->mime);
174
+ if (s->headers) curl_slist_free_all(s->headers);
175
+ if (s->request_body_file) fclose(s->request_body_file);
132
176
  if (s->multi) curl_multi_cleanup(s->multi);
133
177
  if (s->base) curl_easy_cleanup(s->base);
134
178
  if (s->share) curl_share_cleanup(s->share);
@@ -144,7 +188,12 @@ static void session_mark(void *ptr) {
144
188
  rb_gc_mark_movable(s->progress_proc);
145
189
  rb_gc_mark_movable(s->header_buf);
146
190
  rb_gc_mark_movable(s->body_buf);
191
+ rb_gc_mark_movable(s->upload_io);
192
+ rb_gc_mark_movable(s->upload_buf);
147
193
  rb_gc_mark_movable(s->pending_errinfo);
194
+ rb_gc_mark_movable(s->read_buf);
195
+ rb_gc_mark_movable(s->owner);
196
+ rb_gc_mark_movable(s->owner_thread);
148
197
  /* libcurl holds RSTRING_PTR() of this one for the whole transfer, and an embedded
149
198
  * String keeps its bytes inside the object slot, so it must not be moved. */
150
199
  rb_gc_mark(s->upload_data);
@@ -158,7 +207,12 @@ static void session_compact(void *ptr) {
158
207
  s->progress_proc = rb_gc_location(s->progress_proc);
159
208
  s->header_buf = rb_gc_location(s->header_buf);
160
209
  s->body_buf = rb_gc_location(s->body_buf);
210
+ s->upload_io = rb_gc_location(s->upload_io);
211
+ s->upload_buf = rb_gc_location(s->upload_buf);
161
212
  s->pending_errinfo = rb_gc_location(s->pending_errinfo);
213
+ s->read_buf = rb_gc_location(s->read_buf);
214
+ s->owner = rb_gc_location(s->owner);
215
+ s->owner_thread = rb_gc_location(s->owner_thread);
162
216
  }
163
217
 
164
218
  static size_t session_memsize(const void *ptr) {
@@ -215,14 +269,18 @@ static VALUE error_class_for(CURLcode code) {
215
269
  }
216
270
  }
217
271
 
218
- NORETURN(static void raise_curl_error(VALUE klass, CURLcode code, const char *message));
219
- static void raise_curl_error(VALUE klass, CURLcode code, const char *message) {
272
+ static VALUE build_curl_error(VALUE klass, CURLcode code, const char *message) {
220
273
  VALUE exc;
221
274
 
222
275
  if (!message || !*message) message = curl_easy_strerror(code);
223
276
  exc = rb_exc_new_cstr(klass, message);
224
277
  rb_ivar_set(exc, id_ivar_curl_code, INT2NUM((int)code));
225
- rb_exc_raise(exc);
278
+ return exc;
279
+ }
280
+
281
+ NORETURN(static void raise_curl_error(VALUE klass, CURLcode code, const char *message));
282
+ static void raise_curl_error(VALUE klass, CURLcode code, const char *message) {
283
+ rb_exc_raise(build_curl_error(klass, code, message));
226
284
  }
227
285
 
228
286
  /* curl_easy_setopt() only fails for options the installed libcurl cannot honour; that must
@@ -283,25 +341,211 @@ static size_t append_to_string(struct patron_session *s, VALUE str, const char *
283
341
  return len;
284
342
  }
285
343
 
344
+ static double monotonic_now(void) {
345
+ struct timespec ts;
346
+ clock_gettime(CLOCK_MONOTONIC, &ts);
347
+ return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
348
+ }
349
+
350
+ static void mark_head_complete(struct patron_session *s) {
351
+ if (s->head_complete) return;
352
+ s->head_complete = 1;
353
+ s->head_length = NIL_P(s->header_buf) ? 0 : RSTRING_LEN(s->header_buf);
354
+ }
355
+
356
+ /* libcurl drops the bodies of the redirects it follows and 1xx responses have none, so the
357
+ * first body byte always belongs to the final response: the head is complete by then.
358
+ *
359
+ * A streamed transfer writes into the caller's String. When a delivery does not fit, what
360
+ * fits is copied and the transfer paused; libcurl then delivers the very same bytes again
361
+ * after curl_easy_pause(CURLPAUSE_CONT), and redeliver_skip steps over the part handed out
362
+ * already. Outside of transfer_read there is no String to write to, so the transfer pauses
363
+ * without taking anything. */
286
364
  static size_t body_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
287
365
  struct patron_session *s = userdata;
288
366
  size_t len = size * nmemb;
367
+ size_t taken = 0, room, n;
368
+ long cur;
369
+
370
+ mark_head_complete(s);
371
+ s->last_activity = monotonic_now();
372
+ if (!s->streaming) return append_to_string(s, s->body_buf, ptr, len);
373
+
374
+ if (s->redeliver_skip) {
375
+ if (s->redeliver_skip >= len) {
376
+ s->redeliver_skip -= len;
377
+ return len;
378
+ }
379
+ taken = s->redeliver_skip;
380
+ s->redeliver_skip = 0;
381
+ }
382
+
383
+ if (NIL_P(s->read_buf)) {
384
+ s->redeliver_skip = taken;
385
+ s->paused = 1;
386
+ return CURL_WRITEFUNC_PAUSE;
387
+ }
289
388
 
290
- if (s->download_file) {
291
- if (fwrite(ptr, 1, len, s->download_file) != len) return 0;
389
+ if (s->read_limit < 0) {
390
+ if (append_to_string(s, s->read_buf, ptr + taken, len - taken) != len - taken) return 0;
292
391
  return len;
293
392
  }
294
- return append_to_string(s, s->body_buf, ptr, len);
393
+
394
+ cur = RSTRING_LEN(s->read_buf);
395
+ room = s->read_limit > cur ? (size_t)(s->read_limit - cur) : 0;
396
+ n = len - taken < room ? len - taken : room;
397
+ if (n) {
398
+ memcpy(RSTRING_PTR(s->read_buf) + cur, ptr + taken, n);
399
+ rb_str_set_len(s->read_buf, cur + (long)n);
400
+ }
401
+ if (taken + n < len) {
402
+ s->redeliver_skip = taken + n;
403
+ s->paused = 1;
404
+ return CURL_WRITEFUNC_PAUSE;
405
+ }
406
+ return len;
295
407
  }
296
408
 
409
+ /* The early head-complete trigger: the blank line ending a 2xx head. A 2xx is never followed
410
+ * or retried, so nothing else can come before its body, and a long-poll or an event stream
411
+ * gets its head before the first event. 3xx, 401 and 407 wait for the first body byte. */
297
412
  static size_t header_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
298
413
  struct patron_session *s = userdata;
299
- return append_to_string(s, s->header_buf, ptr, size * nmemb);
414
+ size_t len = size * nmemb;
415
+ size_t written = append_to_string(s, s->header_buf, ptr, len);
416
+
417
+ s->last_activity = monotonic_now();
418
+ if (!s->head_complete && ((len == 2 && ptr[0] == '\r' && ptr[1] == '\n') || (len == 1 && ptr[0] == '\n'))) {
419
+ long code = 0;
420
+ curl_easy_getinfo(s->easy, CURLINFO_RESPONSE_CODE, &code);
421
+ if (code >= 200 && code < 300) mark_head_complete(s);
422
+ }
423
+ return written;
300
424
  }
301
425
 
302
426
  static size_t file_read_cb(char *buffer, size_t size, size_t nitems, void *userdata) {
303
- FILE *fp = userdata;
304
- return fread(buffer, 1, size * nitems, fp);
427
+ struct patron_session *s = userdata;
428
+ if (s->interrupt) return CURL_READFUNC_ABORT;
429
+ return fread(buffer, 1, size * nitems, s->request_body_file);
430
+ }
431
+
432
+ /* Same as io_seek_cb(): a failed rewind has to fail the request, not empty the body */
433
+ static int file_seek_cb(void *userdata, curl_off_t offset, int origin) {
434
+ struct patron_session *s = userdata;
435
+
436
+ if (fseeko(s->request_body_file, (off_t)offset, origin) == 0) return CURL_SEEKFUNC_OK;
437
+ s->interrupt = INTERRUPT_REWIND_FAILED;
438
+ return CURL_SEEKFUNC_FAIL;
439
+ }
440
+
441
+ struct io_read_args {
442
+ struct patron_session *s;
443
+ size_t len;
444
+ };
445
+
446
+ static VALUE io_readpartial(VALUE arg) {
447
+ struct io_read_args *a = (struct io_read_args *)arg;
448
+ VALUE argv[2] = { SIZET2NUM(a->len), a->s->upload_buf };
449
+ return rb_funcallv(a->s->upload_io, id_readpartial, 2, argv);
450
+ }
451
+
452
+ static VALUE io_readpartial_eof(VALUE arg, VALUE err) {
453
+ return Qnil;
454
+ }
455
+
456
+ static VALUE io_read_protected(VALUE arg) {
457
+ struct io_read_args *a = (struct io_read_args *)arg;
458
+ struct patron_session *s = a->s;
459
+ VALUE argv[2] = { SIZET2NUM(a->len), s->upload_buf };
460
+ VALUE chunk;
461
+
462
+ if (s->upload_io_partial) {
463
+ chunk = rb_rescue2(io_readpartial, arg, io_readpartial_eof, Qnil, rb_eEOFError, (VALUE)0);
464
+ } else {
465
+ chunk = rb_funcallv(s->upload_io, id_read, 2, argv);
466
+ }
467
+
468
+ if (NIL_P(chunk)) return Qnil;
469
+ StringValue(chunk);
470
+ /* A readable that ignores the length would overflow libcurl's buffer */
471
+ if ((size_t)RSTRING_LEN(chunk) > a->len) {
472
+ raise_curl_error(ePatronError, CURLE_READ_ERROR, "The request body #read returned more bytes than asked for");
473
+ }
474
+ return chunk;
475
+ }
476
+
477
+ /* Streams the body from upload_io. #readpartial(len, buf), or #read(len, buf) when there is
478
+ * no #readpartial, reads into upload_buf, which is then copied into libcurl's buffer: Ruby
479
+ * cannot be handed memory it does not own. #read on a pipe or a socket waits for all of `len`
480
+ * (libcurl asks for 64KB), #readpartial returns what the writer has written so far, and ends
481
+ * with an EOFError rather than nil. Under a Fiber scheduler a blocking read yields to it like
482
+ * any other IO in the fiber. */
483
+ static size_t io_read_cb(char *buffer, size_t size, size_t nitems, void *userdata) {
484
+ struct patron_session *s = userdata;
485
+ struct io_read_args args = { s, size * nitems };
486
+ int tag = 0;
487
+ VALUE chunk;
488
+
489
+ if (s->interrupt) return CURL_READFUNC_ABORT;
490
+ chunk = rb_protect(io_read_protected, (VALUE)&args, &tag);
491
+ if (tag) {
492
+ park_exception(s, tag);
493
+ return CURL_READFUNC_ABORT;
494
+ }
495
+ if (NIL_P(chunk)) return 0;
496
+ memcpy(buffer, RSTRING_PTR(chunk), (size_t)RSTRING_LEN(chunk));
497
+ return (size_t)RSTRING_LEN(chunk);
498
+ }
499
+
500
+ struct io_seek_args {
501
+ struct patron_session *s;
502
+ curl_off_t offset;
503
+ int origin;
504
+ };
505
+
506
+ /* IO::SEEK_SET and friends are the C SEEK_* values, so origin passes through as-is */
507
+ static VALUE io_seek_protected(VALUE arg) {
508
+ struct io_seek_args *a = (struct io_seek_args *)arg;
509
+ VALUE io = a->s->upload_io;
510
+
511
+ if (rb_respond_to(io, id_seek)) {
512
+ VALUE argv[2] = { OFFT2NUM(a->offset), INT2FIX(a->origin) };
513
+ rb_funcallv(io, id_seek, 2, argv);
514
+ return Qtrue;
515
+ }
516
+ /* Zlib::GzipReader and friends can only start over */
517
+ if (a->origin == SEEK_SET && a->offset == 0 && rb_respond_to(io, id_rewind)) {
518
+ rb_funcallv(io, id_rewind, 0, NULL);
519
+ return Qtrue;
520
+ }
521
+ return Qfalse;
522
+ }
523
+
524
+ /* libcurl rewinds the body when it has to send it again: a 307/308 redirect, an auth
525
+ * round-trip. A pipe has #seek but raises ESPIPE, which only means "cannot seek".
526
+ * libcurl 8.7 logs a failed rewind and then follows the redirect with an empty body
527
+ * anyway, so we abort the transfer ourselves rather than trust it to. */
528
+ static int io_seek_cb(void *userdata, curl_off_t offset, int origin) {
529
+ struct patron_session *s = userdata;
530
+ struct io_seek_args args = { s, offset, origin };
531
+ int tag = 0;
532
+ VALUE seeked;
533
+
534
+ if (origin == SEEK_SET) args.offset += s->upload_io_origin;
535
+ seeked = rb_protect(io_seek_protected, (VALUE)&args, &tag);
536
+ if (tag) {
537
+ VALUE err = rb_errinfo();
538
+ if (rb_obj_is_kind_of(err, rb_eSystemCallError) || rb_obj_is_kind_of(err, rb_eIOError)) {
539
+ rb_set_errinfo(Qnil);
540
+ seeked = Qfalse;
541
+ } else {
542
+ park_exception(s, tag);
543
+ return CURL_SEEKFUNC_FAIL;
544
+ }
545
+ }
546
+ if (RTEST(seeked)) return CURL_SEEKFUNC_OK;
547
+ s->interrupt = INTERRUPT_REWIND_FAILED;
548
+ return CURL_SEEKFUNC_CANTSEEK;
305
549
  }
306
550
 
307
551
  struct progress_args {
@@ -475,7 +719,14 @@ static VALUE session_alloc(VALUE klass) {
475
719
  s->upload_data = Qnil;
476
720
  s->header_buf = Qnil;
477
721
  s->body_buf = Qnil;
722
+ s->upload_io = Qnil;
723
+ s->upload_buf = Qnil;
724
+ s->request_content_length = -1;
478
725
  s->pending_errinfo = Qnil;
726
+ s->read_buf = Qnil;
727
+ s->owner = Qnil;
728
+ s->owner_thread = Qnil;
729
+ s->read_limit = -1;
479
730
  init_handles(s);
480
731
  return obj;
481
732
  }
@@ -614,6 +865,15 @@ static int each_http_header(VALUE key, VALUE value, VALUE self) {
614
865
  setopt_or_raise(s->easy, CURLOPT_ACCEPT_ENCODING, "gzip");
615
866
  }
616
867
 
868
+ /* A streamed body of unknown size goes by these, see set_request_body_from_io() */
869
+ if (header_name_is(name, "Content-Length")) {
870
+ VALUE length = rb_obj_as_string(value);
871
+ char *end = NULL;
872
+ long long parsed = strtoll(StringValueCStr(length), &end, 10);
873
+ if (end && *end == '\0' && parsed >= 0 && RSTRING_LEN(length) > 0) s->request_content_length = (curl_off_t)parsed;
874
+ }
875
+ if (header_name_is(name, "Transfer-Encoding")) s->request_transfer_encoding = 1;
876
+
617
877
  line = rb_sprintf("%"PRIsVALUE": %"PRIsVALUE, name, value);
618
878
  s->headers = curl_slist_append(s->headers, StringValueCStr(line));
619
879
  return ST_CONTINUE;
@@ -669,14 +929,57 @@ static void set_request_body_from_file(struct patron_session *s, VALUE path) {
669
929
 
670
930
  setopt_or_raise(s->easy, CURLOPT_UPLOAD, 1L);
671
931
  setopt_or_raise(s->easy, CURLOPT_READFUNCTION, file_read_cb);
672
- setopt_or_raise(s->easy, CURLOPT_READDATA, s->request_body_file);
932
+ setopt_or_raise(s->easy, CURLOPT_READDATA, s);
933
+ setopt_or_raise(s->easy, CURLOPT_SEEKFUNCTION, file_seek_cb);
934
+ setopt_or_raise(s->easy, CURLOPT_SEEKDATA, s);
673
935
  setopt_or_raise(s->easy, CURLOPT_INFILESIZE_LARGE, (curl_off_t)st.st_size);
674
936
  }
675
937
 
938
+ /* Size comes from #size when there is one (minus #pos, for a half-read StringIO), then from
939
+ * a Content-Length header, and failing both the body goes out chunked. libcurl would pick
940
+ * chunked by itself too, but it would then send it alongside an app-supplied Content-Length. */
941
+ static void set_request_body_from_io(struct patron_session *s, VALUE io) {
942
+ curl_off_t size = -1;
943
+
944
+ SESSION_WRITE(s, upload_io, io);
945
+ SESSION_WRITE(s, upload_buf, rb_str_buf_new(0));
946
+ s->upload_io_origin = 0;
947
+ s->upload_io_partial = rb_respond_to(io, id_readpartial);
948
+
949
+ if (rb_respond_to(io, id_size)) {
950
+ VALUE value = rb_funcallv(io, id_size, 0, NULL);
951
+ if (!NIL_P(value)) {
952
+ size = NUM2OFFT(value);
953
+ if (rb_respond_to(io, id_pos)) {
954
+ s->upload_io_origin = NUM2OFFT(rb_funcallv(io, id_pos, 0, NULL));
955
+ size = size > s->upload_io_origin ? size - s->upload_io_origin : 0;
956
+ }
957
+ }
958
+ }
959
+ if (size < 0) size = s->request_content_length;
960
+ if (size < 0 && !s->request_transfer_encoding) {
961
+ s->headers = curl_slist_append(s->headers, "Transfer-Encoding: chunked");
962
+ }
963
+
964
+ setopt_or_raise(s->easy, CURLOPT_UPLOAD, 1L);
965
+ setopt_or_raise(s->easy, CURLOPT_READFUNCTION, io_read_cb);
966
+ setopt_or_raise(s->easy, CURLOPT_READDATA, s);
967
+ setopt_or_raise(s->easy, CURLOPT_SEEKFUNCTION, io_seek_cb);
968
+ setopt_or_raise(s->easy, CURLOPT_SEEKDATA, s);
969
+ if (size >= 0) setopt_or_raise(s->easy, CURLOPT_INFILESIZE_LARGE, size);
970
+ }
971
+
972
+ /* #to_path wins over #read so that a File keeps going through fread(). Every IO has a
973
+ * #to_path since Ruby 3.2, which is nil for a pipe or a socket. */
676
974
  static void set_request_body(struct patron_session *s, VALUE stringable_or_file) {
677
- if (rb_respond_to(stringable_or_file, id_to_path)) {
678
- VALUE path = rb_funcallv(stringable_or_file, id_to_path, 0, NULL);
975
+ VALUE path = Qnil;
976
+
977
+ if (rb_respond_to(stringable_or_file, id_to_path)) path = rb_funcallv(stringable_or_file, id_to_path, 0, NULL);
978
+
979
+ if (!NIL_P(path)) {
679
980
  set_request_body_from_file(s, rb_obj_as_string(path));
981
+ } else if (rb_respond_to(stringable_or_file, id_read)) {
982
+ set_request_body_from_io(s, stringable_or_file);
680
983
  } else {
681
984
  VALUE data = rb_obj_as_string(stringable_or_file);
682
985
  SESSION_WRITE(s, upload_data, data);
@@ -744,10 +1047,8 @@ static void set_options_from_request(struct patron_session *s, VALUE request) {
744
1047
  set_request_body(s, upload_data);
745
1048
  setopt_or_raise(easy, CURLOPT_CUSTOMREQUEST, "GET");
746
1049
  }
747
- if (RTEST(file_name)) {
748
- s->download_file = open_file(file_name, "wb");
749
- SESSION_WRITE(s, body_buf, Qnil);
750
- }
1050
+ /* Downloads go through Session#get_file, which streams into the file from Ruby */
1051
+ if (RTEST(file_name)) rb_raise(rb_eArgError, "A GET with a file name has to go through Session#get_file");
751
1052
  } else if (action == id_post || action == id_put || action == id_patch) {
752
1053
  VALUE multipart = request_attr(request, id_multipart);
753
1054
  const char *verb = action == id_post ? "POST" : action == id_put ? "PUT" : "PATCH";
@@ -867,6 +1168,9 @@ static void set_options_from_request(struct patron_session *s, VALUE request) {
867
1168
 
868
1169
  value = request_attr(request, id_buffer_size);
869
1170
  if (RTEST(value)) setopt_or_raise(easy, CURLOPT_BUFFERSIZE, NUM2LONG(value));
1171
+
1172
+ value = request_attr(request, id_read_timeout);
1173
+ s->read_timeout = RTEST(value) ? NUM2DBL(value) : 0.0;
870
1174
  }
871
1175
 
872
1176
  /*----------------------------------------------------------------------------*/
@@ -923,12 +1227,13 @@ static VALUE create_response(struct patron_session *s) {
923
1227
 
924
1228
  struct poll_context {
925
1229
  CURLM *multi;
1230
+ int timeout_ms;
926
1231
  CURLMcode rc;
927
1232
  };
928
1233
 
929
1234
  static void *poll_without_gvl(void *ptr) {
930
1235
  struct poll_context *ctx = ptr;
931
- ctx->rc = curl_multi_poll(ctx->multi, NULL, 0, POLL_TIMEOUT_MS, NULL);
1236
+ ctx->rc = curl_multi_poll(ctx->multi, NULL, 0, ctx->timeout_ms, NULL);
932
1237
  return NULL;
933
1238
  }
934
1239
 
@@ -941,8 +1246,8 @@ static void wakeup_poll(void *ptr) {
941
1246
  }
942
1247
 
943
1248
  /* Blocks the whole thread until libcurl has something to do, with the GVL released */
944
- static void wait_without_scheduler(struct patron_session *s) {
945
- struct poll_context ctx = { s->multi, CURLM_OK };
1249
+ static void wait_without_scheduler(struct patron_session *s, long max_ms) {
1250
+ struct poll_context ctx = { s->multi, (int)max_ms, CURLM_OK };
946
1251
  /* Pending Ruby interrupts are raised right here as rb_nogvl returns; the rb_ensure
947
1252
  * around the request takes care of the half-finished transfer. */
948
1253
  rb_nogvl(poll_without_gvl, &ctx, wakeup_poll, s, RB_NOGVL_UBF_ASYNC_SAFE);
@@ -955,7 +1260,8 @@ static VALUE float_seconds(long milliseconds) {
955
1260
 
956
1261
  /* An IO wrapping one of libcurl's sockets, for handing to the scheduler. FMODE_EXTERNAL keeps
957
1262
  * Ruby from ever closing the descriptor; libcurl owns it. The wrappers are dropped whenever the
958
- * set of sockets changes, so a descriptor number the kernel reused gets a fresh one. */
1263
+ * set of sockets changes and after every request, so a descriptor number the kernel reused gets
1264
+ * a fresh one. */
959
1265
  static VALUE socket_io(struct patron_session *s, int fd) {
960
1266
  VALUE key = INT2FIX(fd);
961
1267
  VALUE io = rb_hash_lookup2(s->socket_ios, key, Qnil);
@@ -972,7 +1278,7 @@ static VALUE socket_io(struct patron_session *s, int fd) {
972
1278
  /* Yields to the Fiber scheduler until libcurl has something to do, so other fibers on this thread
973
1279
  * run meanwhile. Uses io_wait for the usual single socket, io_select when libcurl juggles several
974
1280
  * (happy eyeballs), and a plain sleep while it has none (threaded resolver at work). */
975
- static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
1281
+ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler, long max_ms) {
976
1282
  fd_set rd, wr, ex;
977
1283
  int maxfd = -1, fd, nfds = 0, i;
978
1284
  int fds[MAX_WAIT_FDS], events[MAX_WAIT_FDS];
@@ -982,7 +1288,7 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
982
1288
  FD_ZERO(&rd); FD_ZERO(&wr); FD_ZERO(&ex);
983
1289
  curl_multi_fdset(s->multi, &rd, &wr, &ex, &maxfd);
984
1290
  curl_multi_timeout(s->multi, &timeout_ms);
985
- if (timeout_ms < 0 || timeout_ms > POLL_TIMEOUT_MS) timeout_ms = POLL_TIMEOUT_MS;
1291
+ if (timeout_ms < 0 || timeout_ms > max_ms) timeout_ms = max_ms;
986
1292
 
987
1293
  for (fd = 0; fd <= maxfd && nfds < MAX_WAIT_FDS; fd++) {
988
1294
  int ev = 0;
@@ -998,7 +1304,9 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
998
1304
  if (nfds == 0) {
999
1305
  /* libcurl asks to be called back within 100 ms when it has no socket to wait on */
1000
1306
  if (timeout_ms > 100) timeout_ms = 100;
1307
+ s->in_scheduler_wait = 1;
1001
1308
  rb_fiber_scheduler_kernel_sleep(scheduler, float_seconds(timeout_ms));
1309
+ s->in_scheduler_wait = 0;
1002
1310
  return;
1003
1311
  }
1004
1312
 
@@ -1013,7 +1321,10 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
1013
1321
 
1014
1322
  timeout = float_seconds(timeout_ms);
1015
1323
  if (nfds == 1) {
1016
- rb_fiber_scheduler_io_wait(scheduler, socket_io(s, fds[0]), INT2NUM(events[0]), timeout);
1324
+ VALUE io = socket_io(s, fds[0]);
1325
+ s->in_scheduler_wait = 1;
1326
+ rb_fiber_scheduler_io_wait(scheduler, io, INT2NUM(events[0]), timeout);
1327
+ s->in_scheduler_wait = 0;
1017
1328
  return;
1018
1329
  }
1019
1330
 
@@ -1027,79 +1338,148 @@ static void wait_with_scheduler(struct patron_session *s, VALUE scheduler) {
1027
1338
  if (events[i] & RUBY_IO_WRITABLE) rb_ary_push(writables, io);
1028
1339
  if (events[i] & RUBY_IO_PRIORITY) rb_ary_push(exceptables, io);
1029
1340
  }
1341
+ s->in_scheduler_wait = 1;
1030
1342
  ready = rb_fiber_scheduler_io_select(scheduler, readables, writables, exceptables, timeout);
1031
1343
  if (ready == Qundef) {
1032
1344
  /* A scheduler without io_select; poll instead. Only ever happens while connecting. */
1033
1345
  rb_fiber_scheduler_kernel_sleep(scheduler, float_seconds(timeout_ms > 50 ? 50 : timeout_ms));
1034
1346
  }
1347
+ s->in_scheduler_wait = 0;
1348
+ }
1349
+ }
1350
+
1351
+ /* Where drive_transfer() may stop before the transfer is done */
1352
+ enum drive_until {
1353
+ UNTIL_DONE,
1354
+ UNTIL_HEAD, /* the final response head is complete */
1355
+ UNTIL_DATA, /* some bytes landed in read_buf, or it is full */
1356
+ };
1357
+
1358
+ static void collect_done(struct patron_session *s) {
1359
+ CURLMsg *msg;
1360
+ int remaining = 0;
1361
+
1362
+ while ((msg = curl_multi_info_read(s->multi, &remaining))) {
1363
+ if (msg->msg == CURLMSG_DONE && msg->easy_handle == s->easy) {
1364
+ s->done = 1;
1365
+ s->result = msg->data.result;
1366
+ }
1035
1367
  }
1036
1368
  }
1037
1369
 
1038
- static void drive_transfer(struct patron_session *s) {
1370
+ static int drive_should_stop(struct patron_session *s, enum drive_until until, long read_from) {
1371
+ switch (until) {
1372
+ case UNTIL_HEAD: return s->head_complete;
1373
+ case UNTIL_DATA: return s->paused || RSTRING_LEN(s->read_buf) > read_from;
1374
+ default: return 0;
1375
+ }
1376
+ }
1377
+
1378
+ /* Drives the transfer until it is done or `until` holds. read_timeout is a deadline counted
1379
+ * from the last delivery, so the waits are capped at what is left of it. `read_from` is the
1380
+ * read_buf length that does not count as new data. */
1381
+ static void drive_transfer(struct patron_session *s, enum drive_until until, long read_from) {
1039
1382
  int running = 1;
1040
1383
  CURLMcode mc;
1041
1384
  /* Set per thread; a Session inside Async { } yields to it instead of blocking the thread */
1042
1385
  VALUE scheduler = rb_fiber_scheduler_current();
1043
1386
 
1044
- mc = curl_multi_add_handle(s->multi, s->easy);
1045
- if (mc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(mc));
1387
+ for (;;) {
1388
+ long wait_ms = POLL_TIMEOUT_MS;
1046
1389
 
1047
- while (running) {
1048
1390
  mc = curl_multi_perform(s->multi, &running);
1049
1391
  if (mc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(mc));
1392
+ collect_done(s);
1050
1393
  if (s->pending_tag || s->interrupt) break;
1051
- if (!running) break;
1394
+ if (!running) s->done = 1;
1395
+ if (s->done || drive_should_stop(s, until, read_from)) break;
1396
+
1397
+ if (s->read_timeout > 0) {
1398
+ double left = s->last_activity + s->read_timeout - monotonic_now();
1399
+ if (left <= 0) {
1400
+ s->read_timed_out = 1;
1401
+ break;
1402
+ }
1403
+ if (left * 1000.0 < (double)wait_ms) wait_ms = (long)(left * 1000.0) + 1;
1404
+ }
1052
1405
 
1053
1406
  if (NIL_P(scheduler)) {
1054
- wait_without_scheduler(s);
1407
+ wait_without_scheduler(s, wait_ms);
1055
1408
  } else {
1056
- wait_with_scheduler(s, scheduler);
1409
+ wait_with_scheduler(s, scheduler, wait_ms);
1057
1410
  }
1058
1411
  if (s->interrupt) break;
1059
1412
  }
1413
+ if (s->done) mark_head_complete(s);
1060
1414
  }
1061
1415
 
1062
- static CURLcode transfer_result(struct patron_session *s) {
1063
- CURLcode result = CURLE_OK;
1064
- CURLMsg *msg;
1065
- int remaining = 0;
1416
+ /* Raises whatever ended the transfer early, and the transfer's own failure when
1417
+ * `include_result` is set and it is done. */
1418
+ static void raise_if_failed(struct patron_session *s, int include_result) {
1419
+ if (s->pending_tag) reraise_parked(s);
1066
1420
 
1067
- while ((msg = curl_multi_info_read(s->multi, &remaining))) {
1068
- if (msg->msg == CURLMSG_DONE && msg->easy_handle == s->easy) result = msg->data.result;
1421
+ if (s->interrupt == INTERRUPT_DOWNLOAD_OVERFLOW) {
1422
+ raise_curl_error(eDownloadLimitExceeded, CURLE_ABORTED_BY_CALLBACK, "Download byte limit exceeded");
1423
+ }
1424
+ if (s->interrupt == INTERRUPT_REWIND_FAILED) {
1425
+ raise_curl_error(ePatronError, CURLE_SEND_FAIL_REWIND, "The request body has to be sent again, but it cannot be rewound");
1426
+ }
1427
+ if (s->interrupt) {
1428
+ raise_curl_error(eAborted, CURLE_ABORTED_BY_CALLBACK, "Request interrupted");
1429
+ }
1430
+ if (s->read_timed_out) {
1431
+ char message[128];
1432
+ snprintf(message, sizeof(message), "Read timeout: no data received for %.3g seconds", s->read_timeout);
1433
+ raise_curl_error(eTimeoutError, CURLE_OPERATION_TIMEDOUT, message);
1434
+ }
1435
+ if (include_result && s->done && s->result != CURLE_OK) {
1436
+ raise_curl_error(error_class_for(s->result), s->result, s->error_buf);
1069
1437
  }
1070
- return result;
1071
1438
  }
1072
1439
 
1073
- struct perform_args {
1074
- struct patron_session *s;
1075
- VALUE request;
1076
- };
1077
-
1078
- static VALUE perform(VALUE ptr) {
1079
- struct perform_args *args = (struct perform_args *)ptr;
1080
- struct patron_session *s = args->s;
1081
- CURLcode result;
1440
+ /* Today's request setup plus curl_multi_add_handle(). Everything it allocates is released
1441
+ * by cleanup(), which the caller must make sure runs. */
1442
+ static void begin_transfer(struct patron_session *s, VALUE request, int streaming) {
1443
+ CURLMcode mc;
1082
1444
 
1083
1445
  s->interrupt = 0;
1446
+ s->in_scheduler_wait = 0;
1084
1447
  s->pending_tag = 0;
1085
1448
  s->error_buf[0] = '\0';
1449
+ s->streaming = streaming;
1450
+ s->paused = 0;
1451
+ s->head_complete = 0;
1452
+ s->head_length = 0;
1453
+ s->done = 0;
1454
+ s->result = CURLE_OK;
1455
+ s->read_timed_out = 0;
1456
+ s->redeliver_skip = 0;
1457
+ s->read_limit = -1;
1086
1458
  SESSION_WRITE(s, header_buf, rb_str_buf_new(HEADER_BUFFER_INITIAL_CAPACITY));
1087
- SESSION_WRITE(s, body_buf, rb_str_buf_new(BODY_BUFFER_INITIAL_CAPACITY));
1459
+ SESSION_WRITE(s, body_buf, streaming ? Qnil : rb_str_buf_new(BODY_BUFFER_INITIAL_CAPACITY));
1088
1460
 
1089
- set_options_from_request(s, args->request);
1090
- drive_transfer(s);
1461
+ set_options_from_request(s, request);
1462
+ /* A proxy's "200 Connection established" would otherwise trip the early head trigger;
1463
+ * HeaderParser keeps only the last response anyway */
1464
+ if (streaming) setopt_or_raise(s->easy, CURLOPT_SUPPRESS_CONNECT_HEADERS, 1L);
1091
1465
 
1092
- if (s->pending_tag) reraise_parked(s);
1466
+ s->last_activity = monotonic_now();
1467
+ mc = curl_multi_add_handle(s->multi, s->easy);
1468
+ if (mc != CURLM_OK) rb_raise(ePatronError, "%s", curl_multi_strerror(mc));
1469
+ }
1093
1470
 
1094
- if (s->interrupt == INTERRUPT_DOWNLOAD_OVERFLOW) {
1095
- raise_curl_error(eDownloadLimitExceeded, CURLE_ABORTED_BY_CALLBACK, "Download byte limit exceeded");
1096
- }
1097
- if (s->interrupt) {
1098
- raise_curl_error(eAborted, CURLE_ABORTED_BY_CALLBACK, "Request interrupted");
1099
- }
1471
+ struct perform_args {
1472
+ struct patron_session *s;
1473
+ VALUE request;
1474
+ };
1475
+
1476
+ static VALUE perform(VALUE ptr) {
1477
+ struct perform_args *args = (struct perform_args *)ptr;
1478
+ struct patron_session *s = args->s;
1100
1479
 
1101
- result = transfer_result(s);
1102
- if (result != CURLE_OK) raise_curl_error(error_class_for(result), result, s->error_buf);
1480
+ begin_transfer(s, args->request, 0);
1481
+ drive_transfer(s, UNTIL_DONE, 0);
1482
+ raise_if_failed(s, 1);
1103
1483
 
1104
1484
  /* Write the cookies accumulated in the share out to the jar, if one is configured */
1105
1485
  curl_easy_setopt(s->easy, CURLOPT_COOKIELIST, "FLUSH");
@@ -1124,10 +1504,6 @@ static VALUE cleanup(VALUE self) {
1124
1504
  curl_slist_free_all(s->headers);
1125
1505
  s->headers = NULL;
1126
1506
  }
1127
- if (s->download_file) {
1128
- fclose(s->download_file);
1129
- s->download_file = NULL;
1130
- }
1131
1507
  if (s->request_body_file) {
1132
1508
  fclose(s->request_body_file);
1133
1509
  s->request_body_file = NULL;
@@ -1137,11 +1513,39 @@ static VALUE cleanup(VALUE self) {
1137
1513
  SESSION_WRITE(s, upload_data, Qnil);
1138
1514
  SESSION_WRITE(s, header_buf, Qnil);
1139
1515
  SESSION_WRITE(s, body_buf, Qnil);
1516
+ SESSION_WRITE(s, upload_io, Qnil);
1517
+ SESSION_WRITE(s, upload_buf, Qnil);
1518
+ SESSION_WRITE(s, read_buf, Qnil);
1519
+ SESSION_WRITE(s, owner, Qnil);
1520
+ SESSION_WRITE(s, owner_thread, Qnil);
1521
+ s->in_scheduler_wait = 0;
1522
+ /* An aborted transfer closes its socket, and the next one tends to get the same number. Handed
1523
+ * the same IO again, io-event's epoll selector takes it for still registered and never wakes. */
1524
+ if (!NIL_P(s->socket_ios)) rb_hash_clear(s->socket_ios);
1525
+ s->upload_io_origin = 0;
1526
+ s->upload_io_partial = 0;
1527
+ s->request_content_length = -1;
1528
+ s->request_transfer_encoding = 0;
1140
1529
  s->download_byte_limit = 0;
1530
+ s->streaming = 0;
1531
+ s->paused = 0;
1532
+ s->read_timeout = 0.0;
1141
1533
  s->busy = 0;
1142
1534
  return Qnil;
1143
1535
  }
1144
1536
 
1537
+ static void raise_if_busy(struct patron_session *s) {
1538
+ if (s->busy) {
1539
+ rb_raise(ePatronError, "This Session is already performing a request in another thread or fiber; use one Session per thread or fiber");
1540
+ }
1541
+ }
1542
+
1543
+ static void claim(struct patron_session *s) {
1544
+ s->busy = 1;
1545
+ SESSION_WRITE(s, owner, rb_fiber_current());
1546
+ SESSION_WRITE(s, owner_thread, rb_thread_current());
1547
+ }
1548
+
1145
1549
  /*
1146
1550
  * Performs the HTTP request described by +request+ and returns a Response built with
1147
1551
  * #response_class. Raises a Patron::Error subclass when libcurl reports a failure.
@@ -1153,17 +1557,226 @@ static VALUE session_handle_request(VALUE self, VALUE request) {
1153
1557
  struct patron_session *s = get_session(self);
1154
1558
  struct perform_args args = { s, request };
1155
1559
 
1156
- if (s->busy) {
1157
- rb_raise(ePatronError, "This Session is already performing a request in another thread or fiber; use one Session per thread or fiber");
1158
- }
1560
+ raise_if_busy(s);
1159
1561
  ensure_handles_for_this_process(s);
1160
- s->busy = 1;
1562
+ claim(s);
1161
1563
  return rb_ensure(perform, (VALUE)&args, cleanup, self);
1162
1564
  }
1163
1565
 
1566
+ /*----------------------------------------------------------------------------*/
1567
+ /* Streaming primitives. Session#stream calls them in order inside one begin/ensure:
1568
+ * transfer_start, any number of transfer_read, transfer_finish. */
1569
+
1570
+ static VALUE start_stream(VALUE ptr) {
1571
+ struct perform_args *args = (struct perform_args *)ptr;
1572
+ struct patron_session *s = args->s;
1573
+ CURL *easy;
1574
+ char *effective_url = NULL;
1575
+ long code = 0, redirect_count = 0;
1576
+
1577
+ begin_transfer(s, args->request, 1);
1578
+ drive_transfer(s, UNTIL_HEAD, 0);
1579
+ raise_if_failed(s, 1);
1580
+
1581
+ easy = s->easy;
1582
+ curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &effective_url);
1583
+ curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &code);
1584
+ curl_easy_getinfo(easy, CURLINFO_REDIRECT_COUNT, &redirect_count);
1585
+ return rb_ary_new_from_args(4,
1586
+ LONG2NUM(code),
1587
+ effective_url ? rb_usascii_str_new_cstr(effective_url) : Qnil,
1588
+ LONG2NUM(redirect_count),
1589
+ rb_str_subseq(s->header_buf, 0, s->head_length));
1590
+ }
1591
+
1592
+ /*
1593
+ * Starts a streamed transfer and drives it until the head of the final response is complete.
1594
+ * Raises the usual Patron::Error when the transfer fails before that, having cleaned up.
1595
+ *
1596
+ * @api private
1597
+ * @param request [Patron::Request]
1598
+ * @return [Array] status, effective URL, redirect count and the raw header data
1599
+ */
1600
+ static VALUE session_transfer_start(VALUE self, VALUE request) {
1601
+ struct patron_session *s = get_session(self);
1602
+ struct perform_args args = { s, request };
1603
+ int state = 0;
1604
+ VALUE head;
1605
+
1606
+ raise_if_busy(s);
1607
+ ensure_handles_for_this_process(s);
1608
+ claim(s);
1609
+ head = rb_protect(start_stream, (VALUE)&args, &state);
1610
+ if (state) {
1611
+ cleanup(self);
1612
+ rb_jump_tag(state);
1613
+ }
1614
+ return head;
1615
+ }
1616
+
1617
+ static struct patron_session *open_stream(VALUE self) {
1618
+ struct patron_session *s = get_session(self);
1619
+
1620
+ if (!s->easy || !s->streaming) rb_raise(ePatronError, "No streamed transfer is open on this Session");
1621
+ if (s->owner != rb_fiber_current()) rb_raise(ePatronError, "The streamed transfer belongs to another thread or fiber");
1622
+ return s;
1623
+ }
1624
+
1625
+ struct read_args {
1626
+ struct patron_session *s;
1627
+ long maxlen; /* -1 for whatever the next deliveries bring */
1628
+ long keep; /* bytes at the start of buf to leave alone */
1629
+ VALUE buf;
1630
+ };
1631
+
1632
+ static VALUE read_into_buf(VALUE ptr) {
1633
+ struct read_args *a = (struct read_args *)ptr;
1634
+ struct patron_session *s = a->s;
1635
+ long placed;
1636
+
1637
+ if (s->paused) {
1638
+ CURLcode rc;
1639
+ s->paused = 0;
1640
+ s->last_activity = monotonic_now();
1641
+ /* This may deliver the paused bytes right here, before any curl_multi_perform() */
1642
+ rc = curl_easy_pause(s->easy, CURLPAUSE_CONT);
1643
+ if (rc != CURLE_OK) raise_curl_error(error_class_for(rc), rc, s->error_buf);
1644
+ }
1645
+
1646
+ if (!s->paused && RSTRING_LEN(a->buf) == a->keep && !s->done) {
1647
+ s->last_activity = monotonic_now();
1648
+ drive_transfer(s, UNTIL_DATA, a->keep);
1649
+ }
1650
+
1651
+ placed = RSTRING_LEN(a->buf) - a->keep;
1652
+ raise_if_failed(s, placed == 0);
1653
+ if (placed == 0 && s->done) return Qnil;
1654
+ return LONG2NUM(placed);
1655
+ }
1656
+
1657
+ static VALUE read_done(VALUE ptr) {
1658
+ struct read_args *a = (struct read_args *)ptr;
1659
+ SESSION_WRITE(a->s, read_buf, Qnil);
1660
+ /* An exception out of the scheduler wait leaves it set, and the stream's own
1661
+ * ensure blocks run before transfer_finish gets to cleanup() */
1662
+ a->s->in_scheduler_wait = 0;
1663
+ return Qnil;
1664
+ }
1665
+
1666
+ /*
1667
+ * Receives the next bytes of the streamed body into +buf+, which libcurl writes into directly.
1668
+ * The String is only ever written to during this call.
1669
+ *
1670
+ * @api private
1671
+ * @overload transfer_read(maxlen, buf, keep = 0)
1672
+ * @param maxlen [Integer, nil] most bytes to place, nil for whatever the next deliveries bring
1673
+ * @param buf [String] truncated to +keep+ bytes and appended to, made binary
1674
+ * @param keep [Integer] bytes at the start of +buf+ to leave in place
1675
+ * @return [Integer, nil] the bytes placed, nil at the end of the body
1676
+ */
1677
+ static VALUE session_transfer_read(int argc, VALUE *argv, VALUE self) {
1678
+ struct patron_session *s = open_stream(self);
1679
+ VALUE maxlen, buf, keep;
1680
+ struct read_args args;
1681
+
1682
+ rb_scan_args(argc, argv, "21", &maxlen, &buf, &keep);
1683
+ StringValue(buf);
1684
+ args.s = s;
1685
+ args.buf = buf;
1686
+ args.maxlen = NIL_P(maxlen) ? -1 : NUM2LONG(maxlen);
1687
+ args.keep = NIL_P(keep) ? 0 : NUM2LONG(keep);
1688
+ if (args.maxlen < -1) rb_raise(rb_eArgError, "negative length %ld given", args.maxlen);
1689
+ if (args.keep < 0 || args.keep > RSTRING_LEN(buf)) rb_raise(rb_eArgError, "keep out of range");
1690
+
1691
+ /* A frozen or shared String raises or gets its own copy here, rather than being scribbled on */
1692
+ rb_str_modify(buf);
1693
+ rb_str_set_len(buf, args.keep);
1694
+ if (ENCODING_GET(buf) != rb_ascii8bit_encindex()) rb_enc_associate_index(buf, rb_ascii8bit_encindex());
1695
+ if (args.maxlen == 0) return INT2FIX(0);
1696
+ if (args.maxlen > 0) rb_str_modify_expand(buf, args.maxlen);
1697
+
1698
+ s->read_limit = args.maxlen < 0 ? -1 : args.keep + args.maxlen;
1699
+ SESSION_WRITE(s, read_buf, buf);
1700
+ return rb_ensure(read_into_buf, (VALUE)&args, read_done, (VALUE)&args);
1701
+ }
1702
+
1703
+ static VALUE finish_stream(VALUE ptr) {
1704
+ struct patron_session *s = (struct patron_session *)ptr;
1705
+ curl_off_t downloaded = 0, uploaded = 0;
1706
+ int complete = s->done && s->result == CURLE_OK && !s->interrupt && !s->pending_tag;
1707
+ VALUE trailer_data;
1708
+
1709
+ curl_easy_getinfo(s->easy, CURLINFO_SIZE_DOWNLOAD_T, &downloaded);
1710
+ curl_easy_getinfo(s->easy, CURLINFO_SIZE_UPLOAD_T, &uploaded);
1711
+ if (complete && RSTRING_LEN(s->header_buf) > s->head_length) {
1712
+ trailer_data = rb_str_subseq(s->header_buf, s->head_length, RSTRING_LEN(s->header_buf) - s->head_length);
1713
+ } else {
1714
+ trailer_data = rb_str_new(NULL, 0);
1715
+ }
1716
+ curl_easy_setopt(s->easy, CURLOPT_COOKIELIST, "FLUSH");
1717
+
1718
+ return rb_ary_new_from_args(5, LL2NUM(downloaded), LL2NUM(uploaded), create_timings(s->easy),
1719
+ complete ? Qtrue : Qfalse, trailer_data);
1720
+ }
1721
+
1722
+ /*
1723
+ * Ends the streamed transfer, whether or not its body was received in full. A no-op when
1724
+ * this thread or fiber has no streamed transfer open.
1725
+ *
1726
+ * @api private
1727
+ * @return [Array, nil] downloaded bytes, uploaded bytes, timings, whether the transfer completed,
1728
+ * and the raw trailer lines
1729
+ */
1730
+ static VALUE session_transfer_finish(VALUE self) {
1731
+ struct patron_session *s = get_session(self);
1732
+
1733
+ if (!s->easy || !s->streaming || s->owner != rb_fiber_current()) return Qnil;
1734
+ return rb_ensure(finish_stream, (VALUE)s, cleanup, self);
1735
+ }
1736
+
1737
+ struct scheduler_raise_args {
1738
+ VALUE scheduler;
1739
+ VALUE fiber;
1740
+ VALUE exc;
1741
+ };
1742
+
1743
+ static VALUE scheduler_raise(VALUE ptr) {
1744
+ struct scheduler_raise_args *a = (struct scheduler_raise_args *)ptr;
1745
+ return rb_funcall(a->scheduler, id_raise, 2, a->fiber, a->exc);
1746
+ }
1747
+
1748
+ /* curl_multi_wakeup() does not reach an owner parked in the Fiber scheduler: libcurl's wakeup
1749
+ * socket is not among the ones curl_multi_fdset() hands out. So raise into the owner instead,
1750
+ * through the scheduler's #raise, which is what Async::Task#stop uses. That queues the calling
1751
+ * fiber to be resumed; a bare Fiber#raise would transfer away from it for good. Without such a
1752
+ * scheduler the owner notices the flag at its next wait cap. */
1753
+ static void raise_into_waiting_owner(struct patron_session *s) {
1754
+ struct scheduler_raise_args args;
1755
+ int state = 0;
1756
+
1757
+ if (!s->in_scheduler_wait) return;
1758
+ if (s->owner_thread != rb_thread_current() || s->owner == rb_fiber_current()) return;
1759
+ if (!RTEST(rb_fiber_alive_p(s->owner))) return;
1760
+ args.scheduler = rb_fiber_scheduler_current();
1761
+ if (NIL_P(args.scheduler) || !rb_respond_to(args.scheduler, id_raise)) return;
1762
+
1763
+ args.fiber = s->owner;
1764
+ args.exc = build_curl_error(eAborted, CURLE_ABORTED_BY_CALLBACK, "Request interrupted");
1765
+ s->in_scheduler_wait = 0;
1766
+ rb_protect(scheduler_raise, (VALUE)&args, &state);
1767
+ if (state) {
1768
+ /* Could not raise into the owner, so the flag stops it at its next wait cap. Anything
1769
+ * else, such as this fiber being stopped while it waited to be resumed, is not ours. */
1770
+ if (!rb_obj_is_kind_of(rb_errinfo(), eFiberError)) rb_jump_tag(state);
1771
+ rb_set_errinfo(Qnil);
1772
+ }
1773
+ }
1774
+
1164
1775
  /*
1165
1776
  * Aborts the request currently in flight on this Session, which then raises Patron::Aborted.
1166
- * Safe to call from another thread.
1777
+ * Safe to call from another thread. Called from another fiber on the thread of the request,
1778
+ * under a Fiber scheduler with a #raise of its own (Async), it raises the Aborted into the
1779
+ * waiting fiber at once, the way Async::Task#stop does.
1167
1780
  *
1168
1781
  * @return self
1169
1782
  */
@@ -1172,6 +1785,7 @@ static VALUE session_interrupt(VALUE self) {
1172
1785
 
1173
1786
  s->interrupt = INTERRUPT_ABORT;
1174
1787
  if (s->multi) curl_multi_wakeup(s->multi);
1788
+ raise_into_waiting_owner(s);
1175
1789
  return self;
1176
1790
  }
1177
1791
 
@@ -1224,6 +1838,12 @@ void Init_session_ext(void) {
1224
1838
  #undef DEFINE_ID
1225
1839
  id_to_path = rb_intern_const("to_path");
1226
1840
  id_to_s = rb_intern_const("to_s");
1841
+ id_read = rb_intern_const("read");
1842
+ id_readpartial = rb_intern_const("readpartial");
1843
+ id_size = rb_intern_const("size");
1844
+ id_pos = rb_intern_const("pos");
1845
+ id_seek = rb_intern_const("seek");
1846
+ id_rewind = rb_intern_const("rewind");
1227
1847
  id_response_class = rb_intern_const("response_class");
1228
1848
  id_default_response_charset = rb_intern_const("default_response_charset");
1229
1849
  id_get = rb_intern_const("get");
@@ -1233,6 +1853,7 @@ void Init_session_ext(void) {
1233
1853
  id_delete = rb_intern_const("delete");
1234
1854
  id_head = rb_intern_const("head");
1235
1855
  id_ivar_curl_code = rb_intern_const("@curl_code");
1856
+ id_raise = rb_intern_const("raise");
1236
1857
  id_url_kw = rb_intern_const("url");
1237
1858
  id_status_kw = rb_intern_const("status");
1238
1859
  id_redirect_count_kw = rb_intern_const("redirect_count");
@@ -1261,6 +1882,7 @@ void Init_session_ext(void) {
1261
1882
  rb_global_variable(&eAborted);
1262
1883
  rb_global_variable(&eDownloadLimitExceeded);
1263
1884
  rb_global_variable(&eClientCertificateError);
1885
+ rb_global_variable(&eFiberError);
1264
1886
 
1265
1887
  ePatronError = rb_const_get(mPatron, rb_intern("Error"));
1266
1888
  eUnsupportedProtocol = rb_const_get(mPatron, rb_intern("UnsupportedProtocol"));
@@ -1273,6 +1895,7 @@ void Init_session_ext(void) {
1273
1895
  eTimeoutError = rb_const_get(mPatron, rb_intern("TimeoutError"));
1274
1896
  eTooManyRedirects = rb_const_get(mPatron, rb_intern("TooManyRedirects"));
1275
1897
  eAborted = rb_const_get(mPatron, rb_intern("Aborted"));
1898
+ eFiberError = rb_const_get(rb_cObject, rb_intern("FiberError"));
1276
1899
  eDownloadLimitExceeded = rb_const_get(mPatron, rb_intern("DownloadLimitExceeded"));
1277
1900
  eClientCertificateError = rb_const_get(mPatron, rb_intern("ClientCertificateError"));
1278
1901
 
@@ -1294,6 +1917,9 @@ void Init_session_ext(void) {
1294
1917
  rb_define_alias(cSession, "urldecode", "unescape");
1295
1918
 
1296
1919
  rb_define_private_method(cSession, "handle_request", session_handle_request, 1);
1920
+ rb_define_private_method(cSession, "transfer_start", session_transfer_start, 1);
1921
+ rb_define_private_method(cSession, "transfer_read", session_transfer_read, -1);
1922
+ rb_define_private_method(cSession, "transfer_finish", session_transfer_finish, 0);
1297
1923
  rb_define_private_method(cSession, "set_cookie_file", session_set_cookie_file, 1);
1298
1924
  rb_define_private_method(cSession, "set_debug_io", session_set_debug_io, 1);
1299
1925
  rb_define_method(cSession, "interrupt", session_interrupt, 0);