fast_curl 0.3.1 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +91 -11
- data/ext/fast_curl/fast_curl.c +491 -110
- data/lib/fast_curl/version.rb +1 -1
- data/lib/fast_curl.rb +114 -6
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32aca3a9aec734c69bc40aa9b84b519aeddc15defc3ad3a9e8f278f63d9f0ffb
|
|
4
|
+
data.tar.gz: 8037377b3d3e5ab02f4171c3f4ae56e6f42552a6cb5c6d7e2f5fd54921e2cc71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 670eefe8b9fa597e947ae0858bda833d35d15914fbd62e3093483cdf8c2c68560c121586c4b01eec75f81d3a4f6d49d04829d5711180178247f051cb510c8cd6
|
|
7
|
+
data.tar.gz: 709792e880465cc09de983246936ce66a843f5b7899e69ac6f5a390cef35b4cdc8baaf1dbda36abe39627cc2fe457e994076329ab746887d000d1a089e649af7
|
data/README.md
CHANGED
|
@@ -12,9 +12,9 @@ Ultra-fast parallel HTTP client for Ruby. C extension built on libcurl `curl_mul
|
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
**Requirements**: Ruby >=
|
|
15
|
+
**Requirements**: Ruby >= 2.7, libcurl
|
|
16
16
|
|
|
17
|
-
> **
|
|
17
|
+
> **Fiber Scheduler support requires Ruby >= 3.1.** The C extension uses `rb_fiber_scheduler_current`, `rb_fiber_scheduler_block` and `rb_fiber_scheduler_unblock` to yield control to the Fiber Scheduler during I/O; these APIs are stable from Ruby 3.1. On 2.7 and 3.0 the extension builds and runs correctly, but that code is compiled out — so a request made inside a scheduler blocks the whole thread and **no sibling fiber runs until it finishes**. Other OS threads are unaffected, since the GVL is still released. If you use `async`, use Ruby >= 3.1.
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
20
|
gem 'fast_curl'
|
|
@@ -50,16 +50,37 @@ end
|
|
|
50
50
|
|
|
51
51
|
### POST with body and headers
|
|
52
52
|
|
|
53
|
+
Be explicit about the encoding — `json:` and `form:` set the matching
|
|
54
|
+
`Content-Type` for you:
|
|
55
|
+
|
|
53
56
|
```ruby
|
|
54
57
|
FastCurl.post([
|
|
55
58
|
{
|
|
56
59
|
url: "https://api.example.com/users",
|
|
57
60
|
headers: { "Authorization" => "Bearer token" },
|
|
58
|
-
|
|
61
|
+
json: { name: "John" } # application/json
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
url: "https://api.example.com/login",
|
|
65
|
+
form: { user: "john", pass: "x" } # application/x-www-form-urlencoded
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
url: "https://api.example.com/blob",
|
|
69
|
+
headers: { "Content-Type" => "application/xml" },
|
|
70
|
+
body: "<user/>" # sent as-is
|
|
59
71
|
}
|
|
60
72
|
])
|
|
61
73
|
```
|
|
62
74
|
|
|
75
|
+
A raw String `body:` without an explicit `Content-Type` is sent as
|
|
76
|
+
`application/octet-stream`. A Hash `body:` is still encoded as JSON.
|
|
77
|
+
|
|
78
|
+
Query parameters can be passed separately:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
FastCurl.get([{ url: "https://api.example.com/search", params: { q: "ruby", page: 2 } }])
|
|
82
|
+
```
|
|
83
|
+
|
|
63
84
|
### First N responses (cancel the rest)
|
|
64
85
|
|
|
65
86
|
```ruby
|
|
@@ -78,13 +99,29 @@ FastCurl.stream_get(urls, connections: 50) do |index, response|
|
|
|
78
99
|
end
|
|
79
100
|
```
|
|
80
101
|
|
|
81
|
-
###
|
|
102
|
+
### Retries
|
|
103
|
+
|
|
104
|
+
**Only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS) are retried.**
|
|
105
|
+
Several retryable curl errors — `GOT_NOTHING`, `SEND_ERROR`, `RECV_ERROR`,
|
|
106
|
+
`PARTIAL_FILE` — can occur *after* the server has already accepted and processed
|
|
107
|
+
the request, so replaying a `POST` or `PATCH` may duplicate its side effects.
|
|
108
|
+
If you know the endpoint is safe to replay (e.g. it takes an idempotency key),
|
|
109
|
+
opt in with `retry_non_idempotent: true`.
|
|
110
|
+
|
|
111
|
+
`timeout` applies to a single attempt. Use `total_timeout` to bound the whole
|
|
112
|
+
call, including retries and backoff:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
FastCurl.get(urls, timeout: 5, retries: 3, total_timeout: 10_000)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Delays use exponential backoff with full jitter, starting from `retry_delay`.
|
|
82
119
|
|
|
83
120
|
```ruby
|
|
84
121
|
# Automatic retry on network errors (timeout, connection issues)
|
|
85
122
|
results = FastCurl.get([
|
|
86
123
|
{ url: "https://unreliable-api.com/data" }
|
|
87
|
-
], retries: 3, retry_delay: 1000) #
|
|
124
|
+
], retries: 3, retry_delay: 1000) # base delay 1s, doubling with jitter
|
|
88
125
|
|
|
89
126
|
# Retry on specific HTTP status codes
|
|
90
127
|
results = FastCurl.get([
|
|
@@ -109,14 +146,35 @@ end
|
|
|
109
146
|
|
|
110
147
|
## Response format
|
|
111
148
|
|
|
149
|
+
Every response — successful or not — has the same keys:
|
|
150
|
+
|
|
112
151
|
```ruby
|
|
113
152
|
[index, {
|
|
114
|
-
status: 200,
|
|
115
|
-
headers: { "
|
|
116
|
-
body: "response body"
|
|
153
|
+
status: 200, # HTTP status code, 0 on error
|
|
154
|
+
headers: { "content-type" => "application/json" },
|
|
155
|
+
body: "response body",
|
|
156
|
+
error: nil, # nil, or :curl_error / :invalid_request /
|
|
157
|
+
# :not_completed / :deadline_exceeded
|
|
158
|
+
error_code: nil, # CURLcode when error == :curl_error
|
|
159
|
+
effective_url: "https://...", # final URL after redirects
|
|
160
|
+
attempts: 1 # attempts made, including the first
|
|
117
161
|
}]
|
|
118
162
|
```
|
|
119
163
|
|
|
164
|
+
Check `response[:error]` rather than `response[:status] == 200`; a status of `0`
|
|
165
|
+
always means the request never produced an HTTP response.
|
|
166
|
+
|
|
167
|
+
Header names are normalised to lower case (HTTP/2 sends them that way and
|
|
168
|
+
HTTP/1.1 may not), and lookups are case-insensitive:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
response[:headers]["Content-Type"] # => "application/json"
|
|
172
|
+
response[:headers]["content-type"] # => "application/json"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Repeated fields fold into one comma-separated String. `set-cookie` cannot be
|
|
176
|
+
folded and is **always** an Array, even for a single cookie.
|
|
177
|
+
|
|
120
178
|
## Available methods
|
|
121
179
|
|
|
122
180
|
| Method | Description |
|
|
@@ -137,10 +195,32 @@ end
|
|
|
137
195
|
| Option | Default | Description |
|
|
138
196
|
|---|---|---|
|
|
139
197
|
| `connections` | 20 | Max parallel connections |
|
|
140
|
-
| `timeout` | 30 |
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
198
|
+
| `timeout` | 30 | Timeout for a single attempt, in seconds (1-300) |
|
|
199
|
+
| `connect_timeout` | 10000 | Connection phase timeout, in milliseconds |
|
|
200
|
+
| `total_timeout` | none | Wall-clock budget for the whole call, in milliseconds |
|
|
201
|
+
| `retries` | 1 | Retry attempts for idempotent methods (0-10) |
|
|
202
|
+
| `retry_delay` | 100 | Base backoff in milliseconds; doubles with jitter |
|
|
143
203
|
| `retry_codes` | [] | HTTP status codes to retry on |
|
|
204
|
+
| `retry_non_idempotent` | false | Also retry POST and PATCH |
|
|
205
|
+
| `follow_redirects` | true | Follow `Location` headers |
|
|
206
|
+
| `max_redirects` | 5 | Redirect limit (0-100) |
|
|
207
|
+
|
|
208
|
+
DNS results and TLS sessions are cached process-wide, so repeated calls to the
|
|
209
|
+
same host skip resolution and can resume TLS. TCP connections are pooled only
|
|
210
|
+
within a single call — see Known limitations.
|
|
211
|
+
|
|
212
|
+
## Known limitations
|
|
213
|
+
|
|
214
|
+
- TCP connections are not reused across separate calls; each call builds its own
|
|
215
|
+
`curl_multi` handle. Sharing libcurl's connection cache across concurrent
|
|
216
|
+
multi handles deadlocks or crashes, so only the DNS and TLS session caches are
|
|
217
|
+
shared.
|
|
218
|
+
- The whole response body is buffered in memory (100 MB cap per response);
|
|
219
|
+
`stream_execute` streams *responses*, not bodies.
|
|
220
|
+
- HTTP/2 multiplexing is enabled, but `connections` caps in-flight requests and
|
|
221
|
+
TCP connections with the same number, so multiplexing cannot be exploited
|
|
222
|
+
beyond that limit.
|
|
223
|
+
- No multipart, cookie jar, proxy or auth helpers yet.
|
|
144
224
|
|
|
145
225
|
## Performance
|
|
146
226
|
|
data/ext/fast_curl/fast_curl.c
CHANGED
|
@@ -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,24 @@
|
|
|
23
26
|
#define FAST_CURL_NORETURN
|
|
24
27
|
#endif
|
|
25
28
|
|
|
26
|
-
#define MAX_RESPONSE_SIZE
|
|
27
|
-
#define MAX_REDIRECTS
|
|
28
|
-
#define MAX_TIMEOUT
|
|
29
|
-
#define MAX_RETRIES
|
|
30
|
-
#define MAX_REQUESTS
|
|
31
|
-
#define MAX_CONNECTIONS
|
|
32
|
-
#define MAX_RETRY_DELAY_MS
|
|
33
|
-
#define DEFAULT_RETRIES
|
|
34
|
-
#define DEFAULT_RETRY_DELAY
|
|
35
|
-
#define
|
|
36
|
-
#define
|
|
37
|
-
#define
|
|
38
|
-
#define
|
|
39
|
-
#define
|
|
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_RETRY_DELAY_MS 30000
|
|
36
|
+
#define DEFAULT_RETRIES 1
|
|
37
|
+
#define DEFAULT_RETRY_DELAY 100
|
|
38
|
+
#define DEFAULT_CONNECT_TIMEOUT_MS 10000L
|
|
39
|
+
#define MAX_CONNECT_TIMEOUT_MS 300000L
|
|
40
|
+
#define MAX_TOTAL_TIMEOUT_MS 3600000L
|
|
41
|
+
#define INITIAL_BUF_CAP 8192
|
|
42
|
+
#define INITIAL_HEADER_CAP 16
|
|
43
|
+
#define POLL_TIMEOUT_MS 50
|
|
44
|
+
#define POLL_SLICE_MS 2000
|
|
45
|
+
#define FIBER_POLL_TIMEOUT_MS 10
|
|
46
|
+
#define HEADER_LINE_BUF_SIZE 512
|
|
40
47
|
|
|
41
48
|
static const CURLcode DEFAULT_RETRYABLE_CURLE[] = {
|
|
42
49
|
CURLE_COULDNT_CONNECT, CURLE_OPERATION_TIMEDOUT, CURLE_SEND_ERROR, CURLE_RECV_ERROR,
|
|
@@ -57,6 +64,14 @@ typedef enum {
|
|
|
57
64
|
KEY_RETRIES,
|
|
58
65
|
KEY_RETRY_DELAY,
|
|
59
66
|
KEY_RETRY_CODES,
|
|
67
|
+
KEY_CONNECT_TIMEOUT,
|
|
68
|
+
KEY_TOTAL_TIMEOUT,
|
|
69
|
+
KEY_RETRY_NON_IDEMPOTENT,
|
|
70
|
+
KEY_FOLLOW_REDIRECTS,
|
|
71
|
+
KEY_MAX_REDIRECTS,
|
|
72
|
+
KEY_EFFECTIVE_URL,
|
|
73
|
+
KEY_ERROR,
|
|
74
|
+
KEY_ATTEMPTS,
|
|
60
75
|
KEY_LAST
|
|
61
76
|
} key_id_t;
|
|
62
77
|
|
|
@@ -66,10 +81,111 @@ static VALUE fast_syms[KEY_LAST];
|
|
|
66
81
|
#define SYM(key) fast_syms[key]
|
|
67
82
|
|
|
68
83
|
static const char *const KEY_NAMES[KEY_LAST] = {
|
|
69
|
-
"status",
|
|
70
|
-
"
|
|
84
|
+
"status",
|
|
85
|
+
"headers",
|
|
86
|
+
"body",
|
|
87
|
+
"error_code",
|
|
88
|
+
"url",
|
|
89
|
+
"method",
|
|
90
|
+
"timeout",
|
|
91
|
+
"connections",
|
|
92
|
+
"count",
|
|
93
|
+
"retries",
|
|
94
|
+
"retry_delay",
|
|
95
|
+
"retry_codes",
|
|
96
|
+
"connect_timeout",
|
|
97
|
+
"total_timeout",
|
|
98
|
+
"retry_non_idempotent",
|
|
99
|
+
"follow_redirects",
|
|
100
|
+
"max_redirects",
|
|
101
|
+
"effective_url",
|
|
102
|
+
"error",
|
|
103
|
+
"attempts",
|
|
71
104
|
};
|
|
72
105
|
|
|
106
|
+
typedef enum {
|
|
107
|
+
ERR_CURL,
|
|
108
|
+
ERR_INVALID_REQUEST,
|
|
109
|
+
ERR_NOT_COMPLETED,
|
|
110
|
+
ERR_DEADLINE,
|
|
111
|
+
ERR_LAST
|
|
112
|
+
} error_kind_t;
|
|
113
|
+
|
|
114
|
+
static VALUE error_syms[ERR_LAST];
|
|
115
|
+
|
|
116
|
+
static const char *const ERROR_KIND_NAMES[ERR_LAST] = {
|
|
117
|
+
"curl_error",
|
|
118
|
+
"invalid_request",
|
|
119
|
+
"not_completed",
|
|
120
|
+
"deadline_exceeded",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
static long long fast_now_ms(void) {
|
|
124
|
+
struct timespec ts;
|
|
125
|
+
#ifdef CLOCK_MONOTONIC
|
|
126
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
127
|
+
#else
|
|
128
|
+
clock_gettime(CLOCK_REALTIME, &ts);
|
|
129
|
+
#endif
|
|
130
|
+
return (long long)ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static uint64_t fast_rand_state;
|
|
134
|
+
|
|
135
|
+
/* Small xorshift PRNG; only used to jitter retry delays. */
|
|
136
|
+
static uint32_t fast_rand(void) {
|
|
137
|
+
uint64_t x = fast_rand_state;
|
|
138
|
+
if (x == 0)
|
|
139
|
+
x = (uint64_t)fast_now_ms() | 1ULL;
|
|
140
|
+
x ^= x << 13;
|
|
141
|
+
x ^= x >> 7;
|
|
142
|
+
x ^= x << 17;
|
|
143
|
+
fast_rand_state = x;
|
|
144
|
+
return (uint32_t)(x >> 32);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Process-wide DNS and TLS session cache, so repeated calls to the same host
|
|
148
|
+
skip resolution and can resume TLS instead of doing a full handshake. */
|
|
149
|
+
static CURLSH *fast_share = NULL;
|
|
150
|
+
static pthread_mutex_t fast_share_locks[CURL_LOCK_DATA_LAST];
|
|
151
|
+
|
|
152
|
+
static void fast_share_lock(CURL *handle, curl_lock_data data, curl_lock_access access,
|
|
153
|
+
void *userptr) {
|
|
154
|
+
(void)handle;
|
|
155
|
+
(void)access;
|
|
156
|
+
(void)userptr;
|
|
157
|
+
if ((int)data > 0 && (int)data < CURL_LOCK_DATA_LAST)
|
|
158
|
+
pthread_mutex_lock(&fast_share_locks[data]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static void fast_share_unlock(CURL *handle, curl_lock_data data, void *userptr) {
|
|
162
|
+
(void)handle;
|
|
163
|
+
(void)userptr;
|
|
164
|
+
if ((int)data > 0 && (int)data < CURL_LOCK_DATA_LAST)
|
|
165
|
+
pthread_mutex_unlock(&fast_share_locks[data]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static void fast_share_init(void) {
|
|
169
|
+
for (int i = 0; i < CURL_LOCK_DATA_LAST; i++)
|
|
170
|
+
pthread_mutex_init(&fast_share_locks[i], NULL);
|
|
171
|
+
|
|
172
|
+
fast_share = curl_share_init();
|
|
173
|
+
if (!fast_share)
|
|
174
|
+
return;
|
|
175
|
+
|
|
176
|
+
curl_share_setopt(fast_share, CURLSHOPT_LOCKFUNC, fast_share_lock);
|
|
177
|
+
curl_share_setopt(fast_share, CURLSHOPT_UNLOCKFUNC, fast_share_unlock);
|
|
178
|
+
curl_share_setopt(fast_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
|
|
179
|
+
curl_share_setopt(fast_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
|
|
180
|
+
|
|
181
|
+
/* CURL_LOCK_DATA_CONNECT is deliberately NOT shared. Sharing the connection
|
|
182
|
+
cache between multi handles running concurrently in different threads
|
|
183
|
+
deadlocks against CURLMOPT_MAX_TOTAL_CONNECTIONS, and removing that limit
|
|
184
|
+
turns the deadlock into a segfault inside curl_multi_perform (reproduced
|
|
185
|
+
on libcurl 8.5.0 with two threads x 25 requests). Cross-call TCP reuse
|
|
186
|
+
needs a persistent multi handle per thread, not a shared cache. */
|
|
187
|
+
}
|
|
188
|
+
|
|
73
189
|
typedef struct {
|
|
74
190
|
char *data;
|
|
75
191
|
size_t len;
|
|
@@ -212,8 +328,19 @@ typedef struct {
|
|
|
212
328
|
int active;
|
|
213
329
|
CURLcode curl_result;
|
|
214
330
|
long http_status;
|
|
331
|
+
int idempotent;
|
|
332
|
+
int attempts;
|
|
333
|
+
const char *setup_error;
|
|
334
|
+
int setup_error_fatal;
|
|
215
335
|
} request_ctx_t;
|
|
216
336
|
|
|
337
|
+
static VALUE fast_validation_error = Qnil;
|
|
338
|
+
#define SET_SETUP_ERROR(ctx, msg, fatal) \
|
|
339
|
+
do { \
|
|
340
|
+
(ctx)->setup_error = (msg); \
|
|
341
|
+
(ctx)->setup_error_fatal = (fatal); \
|
|
342
|
+
} while (0)
|
|
343
|
+
|
|
217
344
|
static inline void request_ctx_init(request_ctx_t *ctx, int index) {
|
|
218
345
|
ctx->easy = NULL;
|
|
219
346
|
ctx->index = index;
|
|
@@ -224,6 +351,10 @@ static inline void request_ctx_init(request_ctx_t *ctx, int index) {
|
|
|
224
351
|
ctx->active = 0;
|
|
225
352
|
ctx->curl_result = CURLE_OK;
|
|
226
353
|
ctx->http_status = 0;
|
|
354
|
+
ctx->idempotent = 1;
|
|
355
|
+
ctx->attempts = 0;
|
|
356
|
+
ctx->setup_error = NULL;
|
|
357
|
+
ctx->setup_error_fatal = 0;
|
|
227
358
|
}
|
|
228
359
|
|
|
229
360
|
static void request_ctx_free(request_ctx_t *ctx) {
|
|
@@ -267,6 +398,8 @@ static int request_ctx_reset_for_retry(request_ctx_t *ctx) {
|
|
|
267
398
|
ctx->active = 0;
|
|
268
399
|
ctx->curl_result = CURLE_OK;
|
|
269
400
|
ctx->http_status = 0;
|
|
401
|
+
ctx->setup_error = NULL;
|
|
402
|
+
ctx->setup_error_fatal = 0;
|
|
270
403
|
return 1;
|
|
271
404
|
}
|
|
272
405
|
|
|
@@ -291,8 +424,18 @@ typedef struct {
|
|
|
291
424
|
long retry_delay_ms;
|
|
292
425
|
int *retry_http_codes;
|
|
293
426
|
int retry_http_count;
|
|
427
|
+
int retry_non_idempotent;
|
|
294
428
|
} retry_config_t;
|
|
295
429
|
|
|
430
|
+
typedef struct {
|
|
431
|
+
long timeout_sec;
|
|
432
|
+
long connect_timeout_ms;
|
|
433
|
+
long total_timeout_ms;
|
|
434
|
+
long follow_redirects;
|
|
435
|
+
long max_redirects;
|
|
436
|
+
long long deadline_ms;
|
|
437
|
+
} request_options_t;
|
|
438
|
+
|
|
296
439
|
static int contains_header_injection(const char *str, long len) {
|
|
297
440
|
for (long i = 0; i < len; i++) {
|
|
298
441
|
if (str[i] == '\r' || str[i] == '\n' || str[i] == '\0')
|
|
@@ -392,24 +535,37 @@ static void run_via_fiber_worker(VALUE scheduler, void *(*func)(void *), void *a
|
|
|
392
535
|
}
|
|
393
536
|
#endif
|
|
394
537
|
|
|
395
|
-
static
|
|
538
|
+
static VALUE fast_cHeaders = Qnil;
|
|
539
|
+
static VALUE new_headers_hash(void) {
|
|
540
|
+
if (!NIL_P(fast_cHeaders))
|
|
541
|
+
return rb_obj_alloc(fast_cHeaders);
|
|
542
|
+
return rb_hash_new();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
static void headers_hash_store(VALUE headers_hash, VALUE key, VALUE val, int always_array) {
|
|
396
546
|
VALUE existing = rb_hash_aref(headers_hash, key);
|
|
397
547
|
|
|
398
548
|
if (NIL_P(existing)) {
|
|
399
|
-
rb_hash_aset(headers_hash, key, val);
|
|
400
|
-
|
|
549
|
+
rb_hash_aset(headers_hash, key, always_array ? rb_ary_new_from_args(1, val) : val);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (RB_TYPE_P(existing, T_ARRAY)) {
|
|
401
554
|
rb_ary_push(existing, val);
|
|
402
|
-
|
|
403
|
-
VALUE values = rb_ary_new_from_args(2, existing, val);
|
|
404
|
-
rb_hash_aset(headers_hash, key, values);
|
|
555
|
+
return;
|
|
405
556
|
}
|
|
557
|
+
|
|
558
|
+
VALUE joined = rb_str_dup(existing);
|
|
559
|
+
rb_str_cat_cstr(joined, ", ");
|
|
560
|
+
rb_str_append(joined, val);
|
|
561
|
+
rb_hash_aset(headers_hash, key, joined);
|
|
406
562
|
}
|
|
407
563
|
|
|
408
564
|
static VALUE build_response(request_ctx_t *ctx) {
|
|
409
565
|
long status = 0;
|
|
410
566
|
curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &status);
|
|
411
567
|
|
|
412
|
-
VALUE headers_hash =
|
|
568
|
+
VALUE headers_hash = new_headers_hash();
|
|
413
569
|
for (int i = 0; i < ctx->headers.count; i++) {
|
|
414
570
|
const char *hdr = ctx->headers.entries[i].str;
|
|
415
571
|
size_t hdr_len = ctx->headers.entries[i].len;
|
|
@@ -418,6 +574,12 @@ static VALUE build_response(request_ctx_t *ctx) {
|
|
|
418
574
|
continue;
|
|
419
575
|
|
|
420
576
|
VALUE key = rb_str_new(hdr, colon - hdr);
|
|
577
|
+
char *kp = RSTRING_PTR(key);
|
|
578
|
+
long klen = RSTRING_LEN(key);
|
|
579
|
+
for (long k = 0; k < klen; k++)
|
|
580
|
+
kp[k] = (char)tolower((unsigned char)kp[k]);
|
|
581
|
+
int always_array = (klen == 10 && memcmp(kp, "set-cookie", 10) == 0);
|
|
582
|
+
|
|
421
583
|
const char *vs = colon + 1;
|
|
422
584
|
const char *ve = hdr + hdr_len;
|
|
423
585
|
|
|
@@ -427,7 +589,7 @@ static VALUE build_response(request_ctx_t *ctx) {
|
|
|
427
589
|
ve--;
|
|
428
590
|
|
|
429
591
|
VALUE val = rb_str_new(vs, ve - vs);
|
|
430
|
-
headers_hash_store(headers_hash, key, val);
|
|
592
|
+
headers_hash_store(headers_hash, key, val, always_array);
|
|
431
593
|
}
|
|
432
594
|
|
|
433
595
|
VALUE body_str =
|
|
@@ -437,19 +599,33 @@ static VALUE build_response(request_ctx_t *ctx) {
|
|
|
437
599
|
rb_hash_aset(result, SYM(KEY_STATUS), LONG2NUM(status));
|
|
438
600
|
rb_hash_aset(result, SYM(KEY_HEADERS), headers_hash);
|
|
439
601
|
rb_hash_aset(result, SYM(KEY_BODY), body_str);
|
|
602
|
+
rb_hash_aset(result, SYM(KEY_ERROR), Qnil);
|
|
603
|
+
rb_hash_aset(result, SYM(KEY_ERROR_CODE), Qnil);
|
|
604
|
+
rb_hash_aset(result, SYM(KEY_ATTEMPTS), INT2NUM(ctx->attempts));
|
|
605
|
+
|
|
606
|
+
char *effective = NULL;
|
|
607
|
+
if (curl_easy_getinfo(ctx->easy, CURLINFO_EFFECTIVE_URL, &effective) == CURLE_OK && effective)
|
|
608
|
+
rb_hash_aset(result, SYM(KEY_EFFECTIVE_URL), rb_str_new_cstr(effective));
|
|
609
|
+
else
|
|
610
|
+
rb_hash_aset(result, SYM(KEY_EFFECTIVE_URL), Qnil);
|
|
611
|
+
|
|
440
612
|
return result;
|
|
441
613
|
}
|
|
442
614
|
|
|
443
|
-
static VALUE build_error_response(const char *message) {
|
|
615
|
+
static VALUE build_error_response(const char *message, error_kind_t kind, int attempts) {
|
|
444
616
|
VALUE r = rb_hash_new();
|
|
445
617
|
rb_hash_aset(r, SYM(KEY_STATUS), INT2NUM(0));
|
|
446
618
|
rb_hash_aset(r, SYM(KEY_HEADERS), Qnil);
|
|
447
619
|
rb_hash_aset(r, SYM(KEY_BODY), rb_str_new_cstr(message));
|
|
620
|
+
rb_hash_aset(r, SYM(KEY_ERROR), error_syms[kind]);
|
|
621
|
+
rb_hash_aset(r, SYM(KEY_ERROR_CODE), Qnil);
|
|
622
|
+
rb_hash_aset(r, SYM(KEY_EFFECTIVE_URL), Qnil);
|
|
623
|
+
rb_hash_aset(r, SYM(KEY_ATTEMPTS), INT2NUM(attempts));
|
|
448
624
|
return r;
|
|
449
625
|
}
|
|
450
626
|
|
|
451
|
-
static VALUE build_error_response_with_code(const char *message, int error_code) {
|
|
452
|
-
VALUE r = build_error_response(message);
|
|
627
|
+
static VALUE build_error_response_with_code(const char *message, int error_code, int attempts) {
|
|
628
|
+
VALUE r = build_error_response(message, ERR_CURL, attempts);
|
|
453
629
|
rb_hash_aset(r, SYM(KEY_ERROR_CODE), INT2NUM(error_code));
|
|
454
630
|
return r;
|
|
455
631
|
}
|
|
@@ -474,20 +650,33 @@ static int is_valid_url(const char *url) {
|
|
|
474
650
|
return _r; \
|
|
475
651
|
} while (0)
|
|
476
652
|
|
|
477
|
-
static CURLcode setup_basic_options(CURL *easy, const char *url_str,
|
|
653
|
+
static CURLcode setup_basic_options(CURL *easy, const char *url_str, const request_options_t *opts,
|
|
478
654
|
request_ctx_t *ctx) {
|
|
655
|
+
long timeout_ms = opts->timeout_sec * 1000L;
|
|
656
|
+
|
|
657
|
+
if (opts->deadline_ms > 0) {
|
|
658
|
+
long long remaining = opts->deadline_ms - fast_now_ms();
|
|
659
|
+
if (remaining < 1)
|
|
660
|
+
remaining = 1;
|
|
661
|
+
if (remaining < (long long)timeout_ms)
|
|
662
|
+
timeout_ms = (long)remaining;
|
|
663
|
+
}
|
|
664
|
+
|
|
479
665
|
CURL_SETOPT_CHECK(easy, CURLOPT_URL, url_str);
|
|
480
666
|
CURL_SETOPT_CHECK(easy, CURLOPT_WRITEFUNCTION, write_callback);
|
|
481
667
|
CURL_SETOPT_CHECK(easy, CURLOPT_WRITEDATA, &ctx->body);
|
|
482
668
|
CURL_SETOPT_CHECK(easy, CURLOPT_HEADERFUNCTION, header_callback);
|
|
483
669
|
CURL_SETOPT_CHECK(easy, CURLOPT_HEADERDATA, &ctx->headers);
|
|
484
|
-
CURL_SETOPT_CHECK(easy,
|
|
670
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_TIMEOUT_MS, timeout_ms);
|
|
671
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_CONNECTTIMEOUT_MS, opts->connect_timeout_ms);
|
|
485
672
|
CURL_SETOPT_CHECK(easy, CURLOPT_NOSIGNAL, 1L);
|
|
486
|
-
CURL_SETOPT_CHECK(easy, CURLOPT_FOLLOWLOCATION,
|
|
487
|
-
CURL_SETOPT_CHECK(easy, CURLOPT_MAXREDIRS,
|
|
673
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_FOLLOWLOCATION, opts->follow_redirects);
|
|
674
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_MAXREDIRS, opts->max_redirects);
|
|
488
675
|
CURL_SETOPT_CHECK(easy, CURLOPT_ACCEPT_ENCODING, "");
|
|
489
676
|
CURL_SETOPT_CHECK(easy, CURLOPT_PRIVATE, (char *)ctx);
|
|
490
677
|
CURL_SETOPT_CHECK(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
|
|
678
|
+
if (fast_share)
|
|
679
|
+
CURL_SETOPT_CHECK(easy, CURLOPT_SHARE, fast_share);
|
|
491
680
|
return CURLE_OK;
|
|
492
681
|
}
|
|
493
682
|
|
|
@@ -518,12 +707,13 @@ typedef struct {
|
|
|
518
707
|
int post;
|
|
519
708
|
int nobody;
|
|
520
709
|
int allows_body;
|
|
710
|
+
int idempotent;
|
|
521
711
|
} http_method_t;
|
|
522
712
|
|
|
523
713
|
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},
|
|
714
|
+
{"GET", NULL, 0, 0, 0, 1}, {"POST", NULL, 1, 0, 1, 0}, {"PUT", "PUT", 0, 0, 1, 1},
|
|
715
|
+
{"DELETE", "DELETE", 0, 0, 1, 1}, {"PATCH", "PATCH", 0, 0, 1, 0}, {"HEAD", NULL, 0, 1, 0, 1},
|
|
716
|
+
{"OPTIONS", "OPTIONS", 0, 0, 0, 1},
|
|
527
717
|
};
|
|
528
718
|
|
|
529
719
|
static const http_method_t *find_http_method(const char *name) {
|
|
@@ -544,29 +734,52 @@ static CURLcode apply_http_method(CURL *easy, const http_method_t *method) {
|
|
|
544
734
|
return CURLE_OK;
|
|
545
735
|
}
|
|
546
736
|
|
|
547
|
-
static
|
|
548
|
-
const
|
|
549
|
-
const http_method_t *method = find_http_method(name);
|
|
737
|
+
static int setup_method_and_body(request_ctx_t *ctx, VALUE method_value, VALUE body) {
|
|
738
|
+
const http_method_t *method;
|
|
550
739
|
int has_body = !NIL_P(body);
|
|
740
|
+
char name[32];
|
|
741
|
+
|
|
742
|
+
if (NIL_P(method_value)) {
|
|
743
|
+
memcpy(name, "GET", 4);
|
|
744
|
+
} else {
|
|
745
|
+
if (!RB_TYPE_P(method_value, T_STRING)) {
|
|
746
|
+
SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
|
|
747
|
+
return 0;
|
|
748
|
+
}
|
|
749
|
+
long len = RSTRING_LEN(method_value);
|
|
750
|
+
if (len <= 0 || len >= (long)sizeof(name)) {
|
|
751
|
+
SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
memcpy(name, RSTRING_PTR(method_value), (size_t)len);
|
|
755
|
+
name[len] = '\0';
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
method = find_http_method(name);
|
|
759
|
+
if (!method) {
|
|
760
|
+
SET_SETUP_ERROR(ctx, "Unsupported HTTP method", 1);
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
if (has_body && !method->allows_body) {
|
|
764
|
+
SET_SETUP_ERROR(ctx, "This HTTP method must not include a body", 1);
|
|
765
|
+
return 0;
|
|
766
|
+
}
|
|
551
767
|
|
|
552
|
-
|
|
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);
|
|
768
|
+
ctx->idempotent = method->idempotent;
|
|
556
769
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return
|
|
770
|
+
if (apply_http_method(ctx->easy, method) != CURLE_OK) {
|
|
771
|
+
SET_SETUP_ERROR(ctx, "Failed to configure HTTP method", 0);
|
|
772
|
+
return 0;
|
|
773
|
+
}
|
|
560
774
|
|
|
561
|
-
if (has_body) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
return res;
|
|
775
|
+
if (has_body && set_body(ctx->easy, body) != CURLE_OK) {
|
|
776
|
+
SET_SETUP_ERROR(ctx, "Failed to configure request body", 0);
|
|
777
|
+
return 0;
|
|
565
778
|
}
|
|
566
779
|
|
|
567
780
|
RB_GC_GUARD(method_value);
|
|
568
781
|
RB_GC_GUARD(body);
|
|
569
|
-
return
|
|
782
|
+
return 1;
|
|
570
783
|
}
|
|
571
784
|
|
|
572
785
|
static void append_request_header(request_ctx_t *ctx, const char *buf) {
|
|
@@ -608,23 +821,42 @@ static void append_formatted_header(request_ctx_t *ctx, const char *key, long ke
|
|
|
608
821
|
free(line);
|
|
609
822
|
}
|
|
610
823
|
|
|
824
|
+
static VALUE header_part_to_str(VALUE v) {
|
|
825
|
+
if (RB_TYPE_P(v, T_STRING))
|
|
826
|
+
return v;
|
|
827
|
+
if (SYMBOL_P(v))
|
|
828
|
+
return rb_sym2str(v);
|
|
829
|
+
return rb_String(v);
|
|
830
|
+
}
|
|
831
|
+
|
|
611
832
|
static int header_iter_cb(VALUE key, VALUE val, VALUE arg) {
|
|
612
833
|
request_ctx_t *ctx = (request_ctx_t *)arg;
|
|
613
|
-
VALUE key_str
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
long klen = RSTRING_LEN(key_str);
|
|
834
|
+
VALUE key_str, val_str;
|
|
835
|
+
const char *k;
|
|
836
|
+
long klen;
|
|
617
837
|
const char *v = NULL;
|
|
618
838
|
long vlen = 0;
|
|
619
839
|
|
|
620
|
-
if (
|
|
621
|
-
|
|
840
|
+
if (ctx->setup_error)
|
|
841
|
+
return ST_STOP;
|
|
842
|
+
|
|
843
|
+
key_str = header_part_to_str(key);
|
|
844
|
+
val_str = NIL_P(val) ? Qnil : header_part_to_str(val);
|
|
845
|
+
k = RSTRING_PTR(key_str);
|
|
846
|
+
klen = RSTRING_LEN(key_str);
|
|
847
|
+
|
|
848
|
+
if (!is_valid_header_name(k, klen) || contains_header_injection(k, klen)) {
|
|
849
|
+
SET_SETUP_ERROR(ctx, "Invalid HTTP header name", 1);
|
|
850
|
+
return ST_STOP;
|
|
851
|
+
}
|
|
622
852
|
|
|
623
853
|
if (!NIL_P(val_str) && RSTRING_LEN(val_str) > 0) {
|
|
624
854
|
v = RSTRING_PTR(val_str);
|
|
625
855
|
vlen = RSTRING_LEN(val_str);
|
|
626
|
-
if (contains_header_injection(v, vlen))
|
|
627
|
-
|
|
856
|
+
if (contains_header_injection(v, vlen)) {
|
|
857
|
+
SET_SETUP_ERROR(ctx, "Invalid HTTP header value", 1);
|
|
858
|
+
return ST_STOP;
|
|
859
|
+
}
|
|
628
860
|
}
|
|
629
861
|
|
|
630
862
|
append_formatted_header(ctx, k, klen, v, vlen);
|
|
@@ -634,40 +866,58 @@ static int header_iter_cb(VALUE key, VALUE val, VALUE arg) {
|
|
|
634
866
|
return ST_CONTINUE;
|
|
635
867
|
}
|
|
636
868
|
|
|
637
|
-
static int setup_easy_handle(request_ctx_t *ctx, VALUE request,
|
|
638
|
-
|
|
869
|
+
static int setup_easy_handle(request_ctx_t *ctx, VALUE request, const request_options_t *opts) {
|
|
870
|
+
if (!RB_TYPE_P(request, T_HASH)) {
|
|
871
|
+
SET_SETUP_ERROR(ctx, "Request must be a Hash", 1);
|
|
872
|
+
return 0;
|
|
873
|
+
}
|
|
639
874
|
|
|
640
875
|
VALUE url = hash_aref_key(request, KEY_URL);
|
|
641
876
|
VALUE method = hash_aref_key(request, KEY_METHOD);
|
|
642
877
|
VALUE headers = hash_aref_key(request, KEY_HEADERS);
|
|
643
878
|
VALUE body = hash_aref_key(request, KEY_BODY);
|
|
644
879
|
|
|
645
|
-
if (NIL_P(url))
|
|
880
|
+
if (NIL_P(url)) {
|
|
881
|
+
SET_SETUP_ERROR(ctx, "Missing :url", 0);
|
|
646
882
|
return 0;
|
|
883
|
+
}
|
|
884
|
+
if (!RB_TYPE_P(url, T_STRING) || memchr(RSTRING_PTR(url), '\0', RSTRING_LEN(url))) {
|
|
885
|
+
SET_SETUP_ERROR(ctx, "Invalid URL", 1);
|
|
886
|
+
return 0;
|
|
887
|
+
}
|
|
647
888
|
|
|
648
889
|
const char *url_str = StringValueCStr(url);
|
|
649
|
-
if (!is_valid_url(url_str))
|
|
650
|
-
|
|
890
|
+
if (!is_valid_url(url_str)) {
|
|
891
|
+
SET_SETUP_ERROR(ctx, "Invalid URL (expected http:// or https://, max 2048 bytes)", 1);
|
|
892
|
+
return 0;
|
|
893
|
+
}
|
|
651
894
|
|
|
652
|
-
|
|
653
|
-
|
|
895
|
+
if (setup_basic_options(ctx->easy, url_str, opts, ctx) != CURLE_OK) {
|
|
896
|
+
SET_SETUP_ERROR(ctx, "Failed to configure request", 0);
|
|
654
897
|
return 0;
|
|
898
|
+
}
|
|
655
899
|
|
|
656
|
-
|
|
657
|
-
|
|
900
|
+
if (setup_security_options(ctx->easy) != CURLE_OK) {
|
|
901
|
+
SET_SETUP_ERROR(ctx, "Failed to configure TLS options", 0);
|
|
658
902
|
return 0;
|
|
903
|
+
}
|
|
659
904
|
|
|
660
|
-
|
|
661
|
-
if (res != CURLE_OK)
|
|
905
|
+
if (!setup_method_and_body(ctx, method, body))
|
|
662
906
|
return 0;
|
|
663
907
|
|
|
664
908
|
if (!NIL_P(headers)) {
|
|
665
|
-
|
|
909
|
+
if (!RB_TYPE_P(headers, T_HASH)) {
|
|
910
|
+
SET_SETUP_ERROR(ctx, ":headers must be a Hash", 1);
|
|
911
|
+
return 0;
|
|
912
|
+
}
|
|
666
913
|
rb_hash_foreach(headers, header_iter_cb, (VALUE)ctx);
|
|
914
|
+
if (ctx->setup_error)
|
|
915
|
+
return 0;
|
|
667
916
|
if (ctx->req_headers) {
|
|
668
|
-
|
|
669
|
-
|
|
917
|
+
if (curl_easy_setopt(ctx->easy, CURLOPT_HTTPHEADER, ctx->req_headers) != CURLE_OK) {
|
|
918
|
+
SET_SETUP_ERROR(ctx, "Failed to set request headers", 0);
|
|
670
919
|
return 0;
|
|
920
|
+
}
|
|
671
921
|
}
|
|
672
922
|
}
|
|
673
923
|
|
|
@@ -679,14 +929,25 @@ static int setup_easy_handle(request_ctx_t *ctx, VALUE request, long timeout_sec
|
|
|
679
929
|
return 1;
|
|
680
930
|
}
|
|
681
931
|
|
|
932
|
+
/* Poll until something actually completes, or the slice budget expires.
|
|
933
|
+
Previously this returned after every single 50ms poll, which under a fiber
|
|
934
|
+
scheduler meant a fresh OS thread twenty times a second. Cancellation still
|
|
935
|
+
breaks out immediately via curl_multi_wakeup. */
|
|
682
936
|
static void *poll_without_gvl(void *arg) {
|
|
683
937
|
multi_session_t *s = (multi_session_t *)arg;
|
|
684
|
-
|
|
685
|
-
|
|
938
|
+
long long started = fast_now_ms();
|
|
939
|
+
int before = s->still_running;
|
|
686
940
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
941
|
+
while (!s->cancelled) {
|
|
942
|
+
int numfds = 0;
|
|
943
|
+
curl_multi_poll(s->multi, NULL, 0, POLL_TIMEOUT_MS, &numfds);
|
|
944
|
+
curl_multi_perform(s->multi, &s->still_running);
|
|
945
|
+
|
|
946
|
+
if (s->still_running == 0 || s->still_running < before)
|
|
947
|
+
break;
|
|
948
|
+
if (fast_now_ms() - started >= POLL_SLICE_MS)
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
690
951
|
return NULL;
|
|
691
952
|
}
|
|
692
953
|
|
|
@@ -709,9 +970,11 @@ static VALUE build_result_pair(int index, VALUE response) {
|
|
|
709
970
|
return rb_ary_new_from_args(2, INT2NUM(index), response);
|
|
710
971
|
}
|
|
711
972
|
|
|
712
|
-
static int record_immediate_error(completion_ctx_t *cctx, int index, const char *message
|
|
973
|
+
static int record_immediate_error(completion_ctx_t *cctx, int index, const char *message,
|
|
974
|
+
int attempts) {
|
|
713
975
|
if (cctx->stream || cctx->target > 0) {
|
|
714
|
-
VALUE pair =
|
|
976
|
+
VALUE pair =
|
|
977
|
+
build_result_pair(index, build_error_response(message, ERR_INVALID_REQUEST, attempts));
|
|
715
978
|
|
|
716
979
|
if (cctx->stream)
|
|
717
980
|
rb_yield(pair);
|
|
@@ -751,10 +1014,11 @@ static int process_completed(multi_session_t *session, completion_ctx_t *cctx) {
|
|
|
751
1014
|
curl_easy_getinfo(ctx->easy, CURLINFO_RESPONSE_CODE, &ctx->http_status);
|
|
752
1015
|
|
|
753
1016
|
if (cctx->stream || cctx->target > 0) {
|
|
754
|
-
VALUE response =
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1017
|
+
VALUE response =
|
|
1018
|
+
(msg->data.result == CURLE_OK)
|
|
1019
|
+
? build_response(ctx)
|
|
1020
|
+
: build_error_response_with_code(curl_easy_strerror(msg->data.result),
|
|
1021
|
+
(int)msg->data.result, ctx->attempts);
|
|
758
1022
|
VALUE pair = build_result_pair(ctx->index, response);
|
|
759
1023
|
|
|
760
1024
|
if (cctx->stream)
|
|
@@ -782,30 +1046,34 @@ static int next_pending_index(multi_session_t *session) {
|
|
|
782
1046
|
}
|
|
783
1047
|
|
|
784
1048
|
static int activate_request(multi_session_t *session, VALUE requests, int idx, int *invalid,
|
|
785
|
-
|
|
1049
|
+
const request_options_t *opts) {
|
|
786
1050
|
request_ctx_t *ctx = &session->requests[idx];
|
|
787
1051
|
|
|
788
1052
|
if (invalid[idx] || ctx->done)
|
|
789
1053
|
return 0;
|
|
790
1054
|
|
|
791
|
-
if (!request_ctx_prepare_easy(ctx))
|
|
1055
|
+
if (!request_ctx_prepare_easy(ctx)) {
|
|
1056
|
+
SET_SETUP_ERROR(ctx, "Failed to allocate a curl handle", 0);
|
|
792
1057
|
return 0;
|
|
1058
|
+
}
|
|
793
1059
|
|
|
794
|
-
if (!setup_easy_handle(ctx, rb_ary_entry(requests, idx),
|
|
1060
|
+
if (!setup_easy_handle(ctx, rb_ary_entry(requests, idx), opts))
|
|
795
1061
|
return 0;
|
|
796
1062
|
|
|
797
|
-
|
|
798
|
-
|
|
1063
|
+
if (curl_multi_add_handle(session->multi, ctx->easy) != CURLM_OK) {
|
|
1064
|
+
SET_SETUP_ERROR(ctx, "Failed to schedule the request", 0);
|
|
799
1065
|
return 0;
|
|
1066
|
+
}
|
|
800
1067
|
|
|
1068
|
+
ctx->attempts++;
|
|
801
1069
|
ctx->active = 1;
|
|
802
1070
|
ctx->done = 0;
|
|
803
1071
|
session->active_count++;
|
|
804
1072
|
return 1;
|
|
805
1073
|
}
|
|
806
1074
|
|
|
807
|
-
static int fill_slots(multi_session_t *session, VALUE requests, int *invalid,
|
|
808
|
-
completion_ctx_t *cctx) {
|
|
1075
|
+
static int fill_slots(multi_session_t *session, VALUE requests, int *invalid,
|
|
1076
|
+
const request_options_t *opts, completion_ctx_t *cctx) {
|
|
809
1077
|
while (session->active_count < session->max_connections) {
|
|
810
1078
|
int idx = next_pending_index(session);
|
|
811
1079
|
if (idx < 0)
|
|
@@ -813,11 +1081,14 @@ static int fill_slots(multi_session_t *session, VALUE requests, int *invalid, lo
|
|
|
813
1081
|
|
|
814
1082
|
request_ctx_t *ctx = &session->requests[idx];
|
|
815
1083
|
|
|
816
|
-
if (!activate_request(session, requests, idx, invalid,
|
|
1084
|
+
if (!activate_request(session, requests, idx, invalid, opts)) {
|
|
817
1085
|
invalid[idx] = 1;
|
|
818
1086
|
ctx->done = 1;
|
|
819
1087
|
ctx->active = 0;
|
|
820
|
-
if (record_immediate_error(cctx, idx,
|
|
1088
|
+
if (record_immediate_error(cctx, idx,
|
|
1089
|
+
ctx->setup_error ? ctx->setup_error
|
|
1090
|
+
: "Invalid request configuration",
|
|
1091
|
+
ctx->attempts))
|
|
821
1092
|
return 1;
|
|
822
1093
|
}
|
|
823
1094
|
}
|
|
@@ -838,13 +1109,14 @@ static void prepare_pending(multi_session_t *session, int *indices, int count) {
|
|
|
838
1109
|
}
|
|
839
1110
|
|
|
840
1111
|
static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VALUE requests,
|
|
841
|
-
int *invalid,
|
|
1112
|
+
int *invalid, const request_options_t *opts, int *indices,
|
|
1113
|
+
int indices_count) {
|
|
842
1114
|
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
843
1115
|
VALUE scheduler = current_fiber_scheduler();
|
|
844
1116
|
#endif
|
|
845
1117
|
prepare_pending(session, indices, indices_count);
|
|
846
1118
|
|
|
847
|
-
if (fill_slots(session, requests, invalid,
|
|
1119
|
+
if (fill_slots(session, requests, invalid, opts, cctx))
|
|
848
1120
|
return;
|
|
849
1121
|
|
|
850
1122
|
curl_multi_perform(session->multi, &session->still_running);
|
|
@@ -852,7 +1124,10 @@ static void run_multi_loop(multi_session_t *session, completion_ctx_t *cctx, VAL
|
|
|
852
1124
|
return;
|
|
853
1125
|
|
|
854
1126
|
while (!session->cancelled && (session->active_count > 0 || pending_remaining(session))) {
|
|
855
|
-
if (
|
|
1127
|
+
if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
|
|
1128
|
+
break;
|
|
1129
|
+
|
|
1130
|
+
if (fill_slots(session, requests, invalid, opts, cctx))
|
|
856
1131
|
return;
|
|
857
1132
|
|
|
858
1133
|
if (session->active_count == 0)
|
|
@@ -880,6 +1155,9 @@ static int is_default_retryable_curle(CURLcode code) {
|
|
|
880
1155
|
}
|
|
881
1156
|
|
|
882
1157
|
static int should_retry(request_ctx_t *ctx, retry_config_t *rc) {
|
|
1158
|
+
if (!ctx->idempotent && !rc->retry_non_idempotent)
|
|
1159
|
+
return 0;
|
|
1160
|
+
|
|
883
1161
|
if (ctx->curl_result != CURLE_OK)
|
|
884
1162
|
return is_default_retryable_curle(ctx->curl_result);
|
|
885
1163
|
|
|
@@ -930,6 +1208,26 @@ static void retry_config_init(retry_config_t *retry_cfg) {
|
|
|
930
1208
|
retry_cfg->retry_delay_ms = DEFAULT_RETRY_DELAY;
|
|
931
1209
|
retry_cfg->retry_http_codes = NULL;
|
|
932
1210
|
retry_cfg->retry_http_count = 0;
|
|
1211
|
+
retry_cfg->retry_non_idempotent = 0;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
static long retry_backoff_ms(const retry_config_t *rc, int attempt) {
|
|
1215
|
+
long delay = rc->retry_delay_ms;
|
|
1216
|
+
long half;
|
|
1217
|
+
|
|
1218
|
+
if (delay <= 0)
|
|
1219
|
+
return 0;
|
|
1220
|
+
|
|
1221
|
+
for (int i = 0; i < attempt && delay < MAX_RETRY_DELAY_MS; i++)
|
|
1222
|
+
delay *= 2;
|
|
1223
|
+
if (delay > MAX_RETRY_DELAY_MS)
|
|
1224
|
+
delay = MAX_RETRY_DELAY_MS;
|
|
1225
|
+
|
|
1226
|
+
half = delay / 2;
|
|
1227
|
+
if (half > 0)
|
|
1228
|
+
delay = half + (long)(fast_rand() % (uint32_t)(half + 1));
|
|
1229
|
+
|
|
1230
|
+
return delay;
|
|
933
1231
|
}
|
|
934
1232
|
|
|
935
1233
|
static long parse_long_option(VALUE options, key_id_t key, const char *name, long min, long max,
|
|
@@ -986,8 +1284,16 @@ static void parse_retry_codes(VALUE options, retry_config_t *retry_cfg) {
|
|
|
986
1284
|
retry_cfg->retry_http_codes[i] = NUM2INT(rb_ary_entry(codes, i));
|
|
987
1285
|
}
|
|
988
1286
|
|
|
989
|
-
static void parse_options(VALUE options,
|
|
990
|
-
|
|
1287
|
+
static void parse_options(VALUE options, request_options_t *opts, int *max_conn,
|
|
1288
|
+
retry_config_t *retry_cfg) {
|
|
1289
|
+
VALUE flag;
|
|
1290
|
+
|
|
1291
|
+
opts->timeout_sec = 30;
|
|
1292
|
+
opts->connect_timeout_ms = DEFAULT_CONNECT_TIMEOUT_MS;
|
|
1293
|
+
opts->total_timeout_ms = 0;
|
|
1294
|
+
opts->follow_redirects = 1;
|
|
1295
|
+
opts->max_redirects = MAX_REDIRECTS;
|
|
1296
|
+
opts->deadline_ms = 0;
|
|
991
1297
|
*max_conn = 20;
|
|
992
1298
|
retry_config_init(retry_cfg);
|
|
993
1299
|
|
|
@@ -995,7 +1301,24 @@ static void parse_options(VALUE options, long *timeout, int *max_conn, retry_con
|
|
|
995
1301
|
return;
|
|
996
1302
|
|
|
997
1303
|
Check_Type(options, T_HASH);
|
|
998
|
-
|
|
1304
|
+
opts->timeout_sec =
|
|
1305
|
+
parse_long_option(options, KEY_TIMEOUT, "timeout", 1, MAX_TIMEOUT, opts->timeout_sec, NULL);
|
|
1306
|
+
opts->connect_timeout_ms =
|
|
1307
|
+
parse_long_option(options, KEY_CONNECT_TIMEOUT, "connect_timeout", 1,
|
|
1308
|
+
MAX_CONNECT_TIMEOUT_MS, opts->connect_timeout_ms, NULL);
|
|
1309
|
+
opts->total_timeout_ms = parse_long_option(options, KEY_TOTAL_TIMEOUT, "total_timeout", 1,
|
|
1310
|
+
MAX_TOTAL_TIMEOUT_MS, opts->total_timeout_ms, NULL);
|
|
1311
|
+
opts->max_redirects = parse_long_option(options, KEY_MAX_REDIRECTS, "max_redirects", 0, 100,
|
|
1312
|
+
opts->max_redirects, NULL);
|
|
1313
|
+
|
|
1314
|
+
flag = hash_aref_key(options, KEY_FOLLOW_REDIRECTS);
|
|
1315
|
+
if (!NIL_P(flag))
|
|
1316
|
+
opts->follow_redirects = RTEST(flag) ? 1L : 0L;
|
|
1317
|
+
|
|
1318
|
+
flag = hash_aref_key(options, KEY_RETRY_NON_IDEMPOTENT);
|
|
1319
|
+
if (!NIL_P(flag))
|
|
1320
|
+
retry_cfg->retry_non_idempotent = RTEST(flag) ? 1 : 0;
|
|
1321
|
+
|
|
999
1322
|
*max_conn = parse_int_option(options, KEY_CONNECTIONS, "connections", 1, MAX_CONNECTIONS,
|
|
1000
1323
|
*max_conn, NULL);
|
|
1001
1324
|
retry_cfg->max_retries = parse_int_option(options, KEY_RETRIES, "retries", 0, MAX_RETRIES,
|
|
@@ -1074,7 +1397,7 @@ typedef struct {
|
|
|
1074
1397
|
multi_session_t *session;
|
|
1075
1398
|
int *invalid;
|
|
1076
1399
|
retry_config_t *retry_cfg;
|
|
1077
|
-
|
|
1400
|
+
request_options_t *opts;
|
|
1078
1401
|
} execute_args_t;
|
|
1079
1402
|
|
|
1080
1403
|
static VALUE internal_execute_body(VALUE arg) {
|
|
@@ -1083,7 +1406,8 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1083
1406
|
multi_session_t *session = ea->session;
|
|
1084
1407
|
int *invalid = ea->invalid;
|
|
1085
1408
|
retry_config_t *retry_cfg = ea->retry_cfg;
|
|
1086
|
-
|
|
1409
|
+
request_options_t *opts = ea->opts;
|
|
1410
|
+
int deadline_hit = 0;
|
|
1087
1411
|
int count = session->count;
|
|
1088
1412
|
int target = ea->target;
|
|
1089
1413
|
int stream = ea->stream;
|
|
@@ -1102,10 +1426,18 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1102
1426
|
rb_ary_store(cctx.results, i, Qnil);
|
|
1103
1427
|
}
|
|
1104
1428
|
|
|
1105
|
-
|
|
1429
|
+
if (opts->total_timeout_ms > 0)
|
|
1430
|
+
opts->deadline_ms = fast_now_ms() + opts->total_timeout_ms;
|
|
1431
|
+
|
|
1432
|
+
run_multi_loop(session, &cctx, requests, invalid, opts, NULL, count);
|
|
1106
1433
|
|
|
1107
1434
|
if (!stream && target <= 0 && retry_cfg->max_retries > 0) {
|
|
1108
1435
|
for (int attempt = 0; attempt < retry_cfg->max_retries; attempt++) {
|
|
1436
|
+
if (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms) {
|
|
1437
|
+
deadline_hit = 1;
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1109
1441
|
int *retry_indices = malloc(sizeof(int) * (size_t)count);
|
|
1110
1442
|
if (!retry_indices)
|
|
1111
1443
|
rb_raise(rb_eNoMemError, "failed to allocate retry index array");
|
|
@@ -1123,7 +1455,18 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1123
1455
|
break;
|
|
1124
1456
|
}
|
|
1125
1457
|
|
|
1126
|
-
|
|
1458
|
+
long backoff = retry_backoff_ms(retry_cfg, attempt);
|
|
1459
|
+
if (opts->deadline_ms > 0) {
|
|
1460
|
+
long long left = opts->deadline_ms - fast_now_ms();
|
|
1461
|
+
if (left <= 0) {
|
|
1462
|
+
deadline_hit = 1;
|
|
1463
|
+
free(retry_indices);
|
|
1464
|
+
break;
|
|
1465
|
+
}
|
|
1466
|
+
if (backoff > (long)left)
|
|
1467
|
+
backoff = (long)left;
|
|
1468
|
+
}
|
|
1469
|
+
retry_delay_sleep(backoff);
|
|
1127
1470
|
|
|
1128
1471
|
int runnable_count = 0;
|
|
1129
1472
|
for (int r = 0; r < retry_count; r++) {
|
|
@@ -1133,6 +1476,7 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1133
1476
|
if (!request_ctx_reset_for_retry(rc)) {
|
|
1134
1477
|
invalid[idx] = 1;
|
|
1135
1478
|
rc->done = 1;
|
|
1479
|
+
rc->setup_error = "Failed to allocate a curl handle for the retry";
|
|
1136
1480
|
continue;
|
|
1137
1481
|
}
|
|
1138
1482
|
|
|
@@ -1141,7 +1485,7 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1141
1485
|
|
|
1142
1486
|
if (runnable_count > 0) {
|
|
1143
1487
|
cctx.completed = 0;
|
|
1144
|
-
run_multi_loop(session, &cctx, requests, invalid,
|
|
1488
|
+
run_multi_loop(session, &cctx, requests, invalid, opts, retry_indices,
|
|
1145
1489
|
runnable_count);
|
|
1146
1490
|
}
|
|
1147
1491
|
|
|
@@ -1155,14 +1499,20 @@ static VALUE internal_execute_body(VALUE arg) {
|
|
|
1155
1499
|
VALUE response;
|
|
1156
1500
|
|
|
1157
1501
|
if (invalid[i]) {
|
|
1158
|
-
response = build_error_response(
|
|
1502
|
+
response = build_error_response(rc->setup_error ? rc->setup_error
|
|
1503
|
+
: "Invalid request configuration",
|
|
1504
|
+
ERR_INVALID_REQUEST, rc->attempts);
|
|
1159
1505
|
} else if (!rc->done) {
|
|
1160
|
-
response =
|
|
1506
|
+
response =
|
|
1507
|
+
deadline_hit || (opts->deadline_ms > 0 && fast_now_ms() >= opts->deadline_ms)
|
|
1508
|
+
? build_error_response("Total timeout exceeded", ERR_DEADLINE, rc->attempts)
|
|
1509
|
+
: build_error_response("Request was not completed", ERR_NOT_COMPLETED,
|
|
1510
|
+
rc->attempts);
|
|
1161
1511
|
} else if (rc->curl_result == CURLE_OK) {
|
|
1162
1512
|
response = build_response(rc);
|
|
1163
1513
|
} else {
|
|
1164
1514
|
response = build_error_response_with_code(curl_easy_strerror(rc->curl_result),
|
|
1165
|
-
(int)rc->curl_result);
|
|
1515
|
+
(int)rc->curl_result, rc->attempts);
|
|
1166
1516
|
}
|
|
1167
1517
|
|
|
1168
1518
|
rb_ary_store(cctx.results, i, build_result_pair(i, response));
|
|
@@ -1262,10 +1612,31 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1262
1612
|
if (target > 0 && target > count)
|
|
1263
1613
|
target = count;
|
|
1264
1614
|
|
|
1265
|
-
|
|
1615
|
+
request_options_t opts;
|
|
1266
1616
|
int max_conn;
|
|
1267
1617
|
retry_config_t retry_cfg;
|
|
1268
|
-
parse_options(options, &
|
|
1618
|
+
parse_options(options, &opts, &max_conn, &retry_cfg);
|
|
1619
|
+
|
|
1620
|
+
for (int i = 0; i < count; i++) {
|
|
1621
|
+
request_ctx_t probe;
|
|
1622
|
+
request_ctx_init(&probe, i);
|
|
1623
|
+
if (!request_ctx_prepare_easy(&probe)) {
|
|
1624
|
+
request_ctx_free(&probe);
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
setup_easy_handle(&probe, rb_ary_entry(requests, i), &opts);
|
|
1628
|
+
int fatal = probe.setup_error_fatal;
|
|
1629
|
+
const char *message = probe.setup_error;
|
|
1630
|
+
char buffer[128];
|
|
1631
|
+
if (fatal && message) {
|
|
1632
|
+
snprintf(buffer, sizeof(buffer), "%s (request %d)", message, i);
|
|
1633
|
+
}
|
|
1634
|
+
request_ctx_free(&probe);
|
|
1635
|
+
if (fatal) {
|
|
1636
|
+
VALUE klass = NIL_P(fast_validation_error) ? rb_eArgError : fast_validation_error;
|
|
1637
|
+
rb_raise(klass, "%s", buffer);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1269
1640
|
|
|
1270
1641
|
if (stream || target > 0) {
|
|
1271
1642
|
if (retry_cfg.retries_explicit && retry_cfg.max_retries > 0 && stream)
|
|
@@ -1279,7 +1650,7 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1279
1650
|
|
|
1280
1651
|
multi_session_t session;
|
|
1281
1652
|
int *invalid = NULL;
|
|
1282
|
-
multi_session_init(&session, curl_multi_init(), count, max_conn, timeout_sec);
|
|
1653
|
+
multi_session_init(&session, curl_multi_init(), count, max_conn, opts.timeout_sec);
|
|
1283
1654
|
|
|
1284
1655
|
cleanup_ctx_t cleanup = {.session = &session, .invalid = NULL, .retry_cfg = &retry_cfg};
|
|
1285
1656
|
|
|
@@ -1304,7 +1675,7 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1304
1675
|
.session = &session,
|
|
1305
1676
|
.invalid = invalid,
|
|
1306
1677
|
.retry_cfg = &retry_cfg,
|
|
1307
|
-
.
|
|
1678
|
+
.opts = &opts,
|
|
1308
1679
|
};
|
|
1309
1680
|
|
|
1310
1681
|
#ifdef FAST_CURL_HAVE_FIBER_SCHEDULER
|
|
@@ -1326,7 +1697,6 @@ static VALUE internal_execute(VALUE requests, VALUE options, int target, int str
|
|
|
1326
1697
|
(VALUE)&cleanup);
|
|
1327
1698
|
}
|
|
1328
1699
|
#endif
|
|
1329
|
-
|
|
1330
1700
|
return rb_ensure(internal_execute_body, (VALUE)&ea, cleanup_session, (VALUE)&cleanup);
|
|
1331
1701
|
}
|
|
1332
1702
|
|
|
@@ -1366,6 +1736,7 @@ static VALUE rb_fast_curl_stream_execute(int argc, VALUE *argv, VALUE self) {
|
|
|
1366
1736
|
|
|
1367
1737
|
void Init_fast_curl(void) {
|
|
1368
1738
|
curl_global_init(CURL_GLOBAL_ALL);
|
|
1739
|
+
fast_share_init();
|
|
1369
1740
|
|
|
1370
1741
|
for (int i = 0; i < KEY_LAST; i++) {
|
|
1371
1742
|
fast_ids[i] = rb_intern(KEY_NAMES[i]);
|
|
@@ -1373,8 +1744,18 @@ void Init_fast_curl(void) {
|
|
|
1373
1744
|
rb_gc_register_address(&fast_syms[i]);
|
|
1374
1745
|
}
|
|
1375
1746
|
|
|
1747
|
+
for (int i = 0; i < ERR_LAST; i++) {
|
|
1748
|
+
error_syms[i] = ID2SYM(rb_intern(ERROR_KIND_NAMES[i]));
|
|
1749
|
+
rb_gc_register_address(&error_syms[i]);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1376
1752
|
VALUE mFastCurl = rb_define_module("FastCurl");
|
|
1377
1753
|
|
|
1754
|
+
if (rb_const_defined_at(mFastCurl, rb_intern("Headers"))) {
|
|
1755
|
+
fast_cHeaders = rb_const_get_at(mFastCurl, rb_intern("Headers"));
|
|
1756
|
+
rb_gc_register_address(&fast_cHeaders);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1378
1759
|
rb_define_module_function(mFastCurl, "execute", rb_fast_curl_execute, -1);
|
|
1379
1760
|
rb_define_module_function(mFastCurl, "first_execute", rb_fast_curl_first_execute, -1);
|
|
1380
1761
|
rb_define_module_function(mFastCurl, "stream_execute", rb_fast_curl_stream_execute, -1);
|
data/lib/fast_curl/version.rb
CHANGED
data/lib/fast_curl.rb
CHANGED
|
@@ -1,13 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
+
require 'uri'
|
|
4
5
|
require_relative "fast_curl/version"
|
|
5
|
-
require_relative "fast_curl/fast_curl"
|
|
6
6
|
|
|
7
7
|
module FastCurl
|
|
8
8
|
class Error < StandardError; end
|
|
9
9
|
class TimeoutError < Error; end
|
|
10
10
|
|
|
11
|
+
class Headers < Hash
|
|
12
|
+
def [](key)
|
|
13
|
+
super(normalize(key))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def []=(key, value)
|
|
17
|
+
super(normalize(key), value)
|
|
18
|
+
end
|
|
19
|
+
alias store []=
|
|
20
|
+
|
|
21
|
+
def fetch(key, *args, &block)
|
|
22
|
+
super(normalize(key), *args, &block)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def key?(key)
|
|
26
|
+
super(normalize(key))
|
|
27
|
+
end
|
|
28
|
+
alias has_key? key?
|
|
29
|
+
alias include? key?
|
|
30
|
+
alias member? key?
|
|
31
|
+
|
|
32
|
+
def delete(key, &block)
|
|
33
|
+
super(normalize(key), &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dig(key, *rest)
|
|
37
|
+
super(normalize(key), *rest)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def values_at(*keys)
|
|
41
|
+
super(*keys.map { |key| normalize(key) })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def assoc(key)
|
|
45
|
+
super(normalize(key))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merge(*others, &block)
|
|
49
|
+
others.inject(dup) { |acc, other| acc.merge!(other, &block) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def merge!(*others)
|
|
53
|
+
others.each do |other|
|
|
54
|
+
other.each do |key, value|
|
|
55
|
+
self[key] = block_given? && key?(key) ? yield(normalize(key), self[key], value) : value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
alias update merge!
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def normalize(key)
|
|
65
|
+
key.is_a?(String) || key.is_a?(Symbol) ? key.to_s.downcase : key
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
require_relative "fast_curl/fast_curl"
|
|
71
|
+
|
|
72
|
+
module FastCurl
|
|
11
73
|
DEFAULT_OPTIONS = {
|
|
12
74
|
connections: 20,
|
|
13
75
|
timeout: 30
|
|
@@ -16,6 +78,10 @@ module FastCurl
|
|
|
16
78
|
METHODS = %i[get post put delete patch].freeze
|
|
17
79
|
BODY_METHODS = %i[post put patch].freeze
|
|
18
80
|
|
|
81
|
+
JSON_TYPE = "application/json"
|
|
82
|
+
FORM_TYPE = "application/x-www-form-urlencoded"
|
|
83
|
+
BINARY_TYPE = "application/octet-stream"
|
|
84
|
+
|
|
19
85
|
class << self
|
|
20
86
|
METHODS.each do |method|
|
|
21
87
|
define_method(method) do |requests, **options|
|
|
@@ -35,14 +101,56 @@ module FastCurl
|
|
|
35
101
|
|
|
36
102
|
def build_requests(requests, method)
|
|
37
103
|
requests.map do |req|
|
|
38
|
-
r = { url: req
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
104
|
+
r = { url: build_url(req), method: method.to_s.upcase }
|
|
105
|
+
headers = req[:headers] ? req[:headers].dup : nil
|
|
106
|
+
|
|
107
|
+
if BODY_METHODS.include?(method)
|
|
108
|
+
body, content_type = build_body(req)
|
|
109
|
+
if body
|
|
110
|
+
r[:body] = body
|
|
111
|
+
if content_type && !content_type?(headers)
|
|
112
|
+
headers ||= {}
|
|
113
|
+
headers["Content-Type"] = content_type
|
|
114
|
+
end
|
|
115
|
+
end
|
|
43
116
|
end
|
|
117
|
+
|
|
118
|
+
r[:headers] = headers if headers && !headers.empty?
|
|
44
119
|
r
|
|
45
120
|
end
|
|
46
121
|
end
|
|
122
|
+
|
|
123
|
+
def build_url(req)
|
|
124
|
+
url = req[:url]
|
|
125
|
+
params = req[:params]
|
|
126
|
+
return url if params.nil? || params.empty? || !url.is_a?(String)
|
|
127
|
+
|
|
128
|
+
separator = url.include?("?") ? "&" : "?"
|
|
129
|
+
"#{url}#{separator}#{URI.encode_www_form(params)}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns [body_string, default_content_type].
|
|
133
|
+
# json: {...} or "..." -> application/json
|
|
134
|
+
# form: {...} -> application/x-www-form-urlencoded
|
|
135
|
+
# body: "..." -> application/octet-stream (set your own type)
|
|
136
|
+
# body: {...} -> application/json (kept for backwards compatibility)
|
|
137
|
+
def build_body(req)
|
|
138
|
+
if req.key?(:json)
|
|
139
|
+
value = req[:json]
|
|
140
|
+
[value.is_a?(String) ? value : value.to_json, JSON_TYPE]
|
|
141
|
+
elsif req.key?(:form)
|
|
142
|
+
value = req[:form]
|
|
143
|
+
[value.is_a?(String) ? value : URI.encode_www_form(value), FORM_TYPE]
|
|
144
|
+
elsif req.key?(:body) && !req[:body].nil?
|
|
145
|
+
value = req[:body]
|
|
146
|
+
value.is_a?(Hash) ? [value.to_json, JSON_TYPE] : [value.to_s, BINARY_TYPE]
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def content_type?(headers)
|
|
151
|
+
return false unless headers
|
|
152
|
+
|
|
153
|
+
headers.any? { |k, _| k.to_s.casecmp("content-type").zero? }
|
|
154
|
+
end
|
|
47
155
|
end
|
|
48
156
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast_curl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- roman-haidarov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|
|
@@ -140,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
140
140
|
- !ruby/object:Gem::Version
|
|
141
141
|
version: '0'
|
|
142
142
|
requirements: []
|
|
143
|
-
rubygems_version: 3.
|
|
143
|
+
rubygems_version: 3.4.22
|
|
144
144
|
signing_key:
|
|
145
145
|
specification_version: 4
|
|
146
146
|
summary: Ultra-fast parallel HTTP client as Ruby C extension on libcurl multi
|