curb 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +75 -2
- data/ext/curb.c +11 -4
- data/ext/curb.h +4 -4
- data/ext/curb_easy.c +236 -18
- data/ext/curb_easy.h +1 -0
- data/ext/curb_multi.c +595 -20
- data/ext/curb_multi.h +4 -1
- data/ext/curb_postfield.c +6 -0
- data/ext/curb_upload.c +3 -0
- data/ext/extconf.rb +215 -17
- data/lib/curl/easy.rb +56 -5
- data/tests/bug_issue_noproxy.rb +56 -0
- data/tests/bug_issue_post_redirect.rb +93 -0
- data/tests/bug_issue_spnego.rb +41 -0
- data/tests/helper.rb +33 -0
- data/tests/mem_check.rb +3 -0
- data/tests/tc_curl_download.rb +2 -1
- data/tests/tc_curl_easy.rb +65 -6
- data/tests/tc_curl_multi.rb +31 -0
- data/tests/tc_fiber_scheduler.rb +190 -0
- metadata +10 -6
- data/tests/bug_issue277.rb +0 -32
- data/tests/bug_resolve.rb +0 -15
data/tests/tc_curl_easy.rb
CHANGED
@@ -48,20 +48,62 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def test_curlopt_stderr_with_io
|
51
|
-
|
51
|
+
tmp = Tempfile.new('curb_test_curlopt_stderr')
|
52
|
+
path = tmp.path
|
52
53
|
fd = IO.sysopen(path, 'w')
|
53
|
-
io = IO.
|
54
|
+
io = IO.new(fd, 'w')
|
55
|
+
io.sync = true
|
54
56
|
|
55
57
|
easy = Curl::Easy.new(TestServlet.url)
|
56
58
|
easy.verbose = true
|
57
59
|
easy.setopt(Curl::CURLOPT_STDERR, io)
|
58
60
|
easy.perform
|
59
61
|
|
62
|
+
io.flush
|
63
|
+
io.close
|
64
|
+
output = File.read(path)
|
65
|
+
|
66
|
+
assert_match(/HTTP\/1\.1\ 200\ OK(?:\ )?/, output)
|
67
|
+
assert_match('Host: 127.0.0.1:9129', output)
|
68
|
+
ensure
|
69
|
+
tmp.close! if defined?(tmp) && tmp
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_curlopt_stderr_gc_safe
|
73
|
+
tmp = Tempfile.new('curb_test_curlopt_stderr_gc')
|
74
|
+
path = tmp.path
|
75
|
+
fd = IO.sysopen(path, 'w')
|
76
|
+
io = IO.new(fd, 'w')
|
77
|
+
|
78
|
+
easy = Curl::Easy.new(TestServlet.url)
|
79
|
+
easy.verbose = true
|
80
|
+
easy.setopt(Curl::CURLOPT_STDERR, io)
|
81
|
+
|
82
|
+
# Drop our Ruby reference and force GC; curb should retain it internally
|
83
|
+
io = nil
|
84
|
+
GC.start
|
85
|
+
|
86
|
+
easy.perform
|
60
87
|
|
61
88
|
output = File.read(path)
|
89
|
+
assert_match(/HTTP\/1\.1\ 200\ OK(?:\ )?/, output)
|
90
|
+
assert_match('Host: 127.0.0.1:9129', output)
|
91
|
+
ensure
|
92
|
+
tmp.close! if defined?(tmp) && tmp
|
93
|
+
end
|
62
94
|
|
63
|
-
|
64
|
-
|
95
|
+
def test_head_request_no_body_and_no_timeout
|
96
|
+
# Ensure a HEAD request completes and does not attempt to read a body
|
97
|
+
# even when the server advertises a Content-Length.
|
98
|
+
easy = nil
|
99
|
+
assert_nothing_raised do
|
100
|
+
easy = Curl.http(:HEAD, TestServlet.url)
|
101
|
+
end
|
102
|
+
assert_not_nil easy
|
103
|
+
# Header string should contain the HTTP status line.
|
104
|
+
assert_match(/HTTP\/1\.1\s\d+\s/, easy.header_str.to_s)
|
105
|
+
# Body should be empty for HEAD requests (libcurl won't call the write callback).
|
106
|
+
assert_equal "", easy.body_str.to_s
|
65
107
|
end
|
66
108
|
|
67
109
|
def test_curlopt_stderr_fails_with_tempdir
|
@@ -245,6 +287,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
245
287
|
|
246
288
|
|
247
289
|
def test_last_effective_url_01
|
290
|
+
omit('Windows file URL semantics differ') if WINDOWS
|
248
291
|
c = Curl::Easy.new($TEST_URL)
|
249
292
|
|
250
293
|
assert_equal $TEST_URL, c.url
|
@@ -1041,7 +1084,11 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
1041
1084
|
$auth_header = nil
|
1042
1085
|
# curl checks the auth type supported by the server, so we have to create a
|
1043
1086
|
# new easy handle if we're going to change the auth type...
|
1044
|
-
|
1087
|
+
if WINDOWS
|
1088
|
+
# On Windows, libcurl often uses SSPI for NTLM which yields a different
|
1089
|
+
# header value and encoding; skip the NTLM-specific assertion.
|
1090
|
+
return
|
1091
|
+
end
|
1045
1092
|
curl = Curl::Easy.new(TestServlet.url)
|
1046
1093
|
curl.username = "foo"
|
1047
1094
|
curl.password = "bar"
|
@@ -1073,7 +1120,7 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
1073
1120
|
easy.http_post(pf)
|
1074
1121
|
|
1075
1122
|
assert_not_equal(0,easy.body.size)
|
1076
|
-
assert_equal(Digest::MD5.hexdigest(easy.body), Digest::MD5.hexdigest(File.
|
1123
|
+
assert_equal(Digest::MD5.hexdigest(easy.body), Digest::MD5.hexdigest(File.binread(readme)))
|
1077
1124
|
end
|
1078
1125
|
|
1079
1126
|
def test_easy_close
|
@@ -1095,6 +1142,18 @@ class TestCurbCurlEasy < Test::Unit::TestCase
|
|
1095
1142
|
easy.http_get
|
1096
1143
|
end
|
1097
1144
|
|
1145
|
+
def test_last_result_initialization
|
1146
|
+
# Test for issue #463 - ensure last_result is properly initialized to 0
|
1147
|
+
easy = Curl::Easy.new
|
1148
|
+
assert_equal 0, easy.last_result, "last_result should be initialized to 0 for new instance"
|
1149
|
+
|
1150
|
+
# Test that reset also sets last_result to 0
|
1151
|
+
easy.url = TestServlet.url
|
1152
|
+
easy.http_get
|
1153
|
+
easy.reset
|
1154
|
+
assert_equal 0, easy.last_result, "last_result should be reset to 0 after calling reset"
|
1155
|
+
end
|
1156
|
+
|
1098
1157
|
def test_easy_use_http_versions
|
1099
1158
|
easy = Curl::Easy.new
|
1100
1159
|
easy.url = TestServlet.url + "?query=foo"
|
data/tests/tc_curl_multi.rb
CHANGED
@@ -10,6 +10,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
10
10
|
# for https://github.com/taf2/curb/issues/277
|
11
11
|
# must connect to an external
|
12
12
|
def test_connection_keepalive
|
13
|
+
omit('Not supported on Windows runners') if WINDOWS
|
13
14
|
# this test fails with libcurl 7.22.0. I didn't investigate, but it may be related
|
14
15
|
# to CURLOPT_MAXCONNECTS bug fixed in 7.30.0:
|
15
16
|
# https://github.com/curl/curl/commit/e87e76e2dc108efb1cae87df496416f49c55fca0
|
@@ -171,6 +172,7 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
171
172
|
end
|
172
173
|
|
173
174
|
def test_multi_easy_get_with_error
|
175
|
+
omit('Path/line parsing differs on Windows') if WINDOWS
|
174
176
|
begin
|
175
177
|
did_raise = false
|
176
178
|
n = 3
|
@@ -571,6 +573,35 @@ class TestCurbCurlMulti < Test::Unit::TestCase
|
|
571
573
|
end
|
572
574
|
end
|
573
575
|
|
576
|
+
# Regression test for issue #267 (2015): ensure that when reusing
|
577
|
+
# easy handles with constrained concurrency, the callback receives
|
578
|
+
# the correct URL for each completed request rather than repeating
|
579
|
+
# the first URL.
|
580
|
+
def test_multi_easy_http_urls_unique_across_max_connects
|
581
|
+
urls = [
|
582
|
+
{ :url => TestServlet.url + '?q=1', :method => :get },
|
583
|
+
{ :url => TestServlet.url + '?q=2', :method => :get },
|
584
|
+
{ :url => TestServlet.url + '?q=3', :method => :get }
|
585
|
+
]
|
586
|
+
|
587
|
+
[1, 2, 3].each do |max|
|
588
|
+
results = []
|
589
|
+
Curl::Multi.http(urls.dup, {:pipeline => true, :max_connects => max}) do |easy, code, method|
|
590
|
+
assert_equal 200, code
|
591
|
+
assert_equal :get, method
|
592
|
+
results << easy.last_effective_url
|
593
|
+
end
|
594
|
+
|
595
|
+
# Ensure we saw one completion per input URL
|
596
|
+
assert_equal urls.size, results.size, "expected #{urls.size} results with max_connects=#{max}"
|
597
|
+
|
598
|
+
# And that each URL completed exactly once (no accidental reuse/mis-reporting)
|
599
|
+
expected_urls = urls.map { |u| u[:url] }
|
600
|
+
assert_equal expected_urls.to_set, results.to_set, "unexpected URLs for max_connects=#{max}: #{results.inspect}"
|
601
|
+
assert_equal expected_urls.size, results.uniq.size, "duplicate URLs seen for max_connects=#{max}: #{results.inspect}"
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
574
605
|
def test_multi_recieves_500
|
575
606
|
m = Curl::Multi.new
|
576
607
|
e = Curl::Easy.new("http://127.0.0.1:9129/methods")
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'async'
|
5
|
+
rescue LoadError
|
6
|
+
HAS_ASYNC = false
|
7
|
+
else
|
8
|
+
HAS_ASYNC = true
|
9
|
+
end
|
10
|
+
|
11
|
+
# This test verifies that Curl::Easy#perform cooperates with Ruby's Fiber scheduler
|
12
|
+
# by running multiple requests concurrently in a single thread using the Async gem.
|
13
|
+
class TestCurbFiberScheduler < Test::Unit::TestCase
|
14
|
+
include BugTestServerSetupTeardown
|
15
|
+
|
16
|
+
ITERS = 4
|
17
|
+
MIN_S = 0.25
|
18
|
+
# Each request sleeps 0.25s; two concurrent requests should be ~0.25–0.5s.
|
19
|
+
# Allow some jitter in CI environments.
|
20
|
+
THRESHOLD = ((MIN_S*(ITERS/2.0)) + (MIN_S/2.0)) # add more jitter for slower CI environments
|
21
|
+
SERIAL_TIME_WOULD_BE_ABOUT = MIN_S * ITERS
|
22
|
+
|
23
|
+
def setup
|
24
|
+
@port = 9993
|
25
|
+
|
26
|
+
@response_proc = lambda do |res|
|
27
|
+
res['Content-Type'] = 'text/plain'
|
28
|
+
sleep MIN_S
|
29
|
+
res.body = '200'
|
30
|
+
end
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_multi_is_scheduler_friendly
|
35
|
+
if skip_no_async
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
url = "http://127.0.0.1:#{@port}/test"
|
40
|
+
|
41
|
+
started = Time.now
|
42
|
+
results = []
|
43
|
+
|
44
|
+
|
45
|
+
async_run do
|
46
|
+
m = Curl::Multi.new
|
47
|
+
ITERS.times.each do
|
48
|
+
c = Curl::Easy.new(url)
|
49
|
+
c.on_complete { results << c.code }
|
50
|
+
m.add(c)
|
51
|
+
end
|
52
|
+
m.perform
|
53
|
+
end
|
54
|
+
|
55
|
+
duration = Time.now - started
|
56
|
+
|
57
|
+
assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
|
58
|
+
assert_equal ITERS, results.size
|
59
|
+
assert_equal ITERS.times.map {200}, results
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_easy_perform_is_scheduler_friendly
|
63
|
+
if skip_no_async
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
url = "http://127.0.0.1:#{@port}/test"
|
68
|
+
|
69
|
+
started = Time.now
|
70
|
+
results = []
|
71
|
+
|
72
|
+
async_run do |top|
|
73
|
+
tasks = ITERS.times.map do
|
74
|
+
top.async do
|
75
|
+
#t = Time.now.to_i
|
76
|
+
#puts "starting fiber [#{results.size}] -> #{t}"
|
77
|
+
c = Curl.get(url)
|
78
|
+
#puts "received result: #{results.size} -> #{Time.now.to_f - t.to_f}"
|
79
|
+
results << c.code
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
tasks.each(&:wait)
|
84
|
+
end
|
85
|
+
|
86
|
+
duration = Time.now - started
|
87
|
+
|
88
|
+
assert duration < THRESHOLD, "Requests did not run concurrently under fiber scheduler (#{duration}s) which exceeds the expected threshold of: #{THRESHOLD} serial time would be about: #{SERIAL_TIME_WOULD_BE_ABOUT}"
|
89
|
+
assert_equal ITERS, results.size
|
90
|
+
assert_equal ITERS.times.map {200}, results
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_multi_perform_yields_block_under_scheduler
|
94
|
+
if skip_no_async
|
95
|
+
return
|
96
|
+
end
|
97
|
+
|
98
|
+
url = "http://127.0.0.1:#{@port}/test"
|
99
|
+
yielded = 0
|
100
|
+
results = []
|
101
|
+
|
102
|
+
async_run do
|
103
|
+
m = Curl::Multi.new
|
104
|
+
ITERS.times do
|
105
|
+
c = Curl::Easy.new(url)
|
106
|
+
c.on_complete { results << c.code }
|
107
|
+
m.add(c)
|
108
|
+
end
|
109
|
+
m.perform do
|
110
|
+
yielded += 1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
assert_operator yielded, :>=, 1, 'perform did not yield block while waiting under scheduler'
|
115
|
+
assert_equal ITERS, results.size
|
116
|
+
assert_equal ITERS.times.map {200}, results
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_multi_single_request_scheduler_path
|
120
|
+
if skip_no_async
|
121
|
+
return
|
122
|
+
end
|
123
|
+
|
124
|
+
url = "http://127.0.0.1:#{@port}/test"
|
125
|
+
result = nil
|
126
|
+
|
127
|
+
async_run do
|
128
|
+
m = Curl::Multi.new
|
129
|
+
c = Curl::Easy.new(url)
|
130
|
+
c.on_complete { result = c.code }
|
131
|
+
m.add(c)
|
132
|
+
m.perform
|
133
|
+
end
|
134
|
+
|
135
|
+
assert_equal 200, result
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_multi_reuse_after_scheduler_perform
|
139
|
+
unless HAS_ASYNC
|
140
|
+
warn 'Skipping fiber scheduler test (Async gem not available)'
|
141
|
+
return
|
142
|
+
end
|
143
|
+
|
144
|
+
url = "http://127.0.0.1:#{@port}/test"
|
145
|
+
results = []
|
146
|
+
|
147
|
+
async_run do
|
148
|
+
m = Curl::Multi.new
|
149
|
+
# First round
|
150
|
+
c1 = Curl::Easy.new(url)
|
151
|
+
c1.on_complete { results << c1.code }
|
152
|
+
m.add(c1)
|
153
|
+
m.perform
|
154
|
+
|
155
|
+
# Second round on same multi
|
156
|
+
c2 = Curl::Easy.new(url)
|
157
|
+
c2.on_complete { results << c2.code }
|
158
|
+
m.add(c2)
|
159
|
+
m.perform
|
160
|
+
end
|
161
|
+
|
162
|
+
assert_equal [200, 200], results
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
def skip_no_async
|
167
|
+
if WINDOWS
|
168
|
+
warn 'Skipping fiber scheduler tests on Windows'
|
169
|
+
return true
|
170
|
+
end
|
171
|
+
unless HAS_ASYNC
|
172
|
+
warn 'Skipping fiber scheduler test (Async gem not available)'
|
173
|
+
return true
|
174
|
+
end
|
175
|
+
false
|
176
|
+
end
|
177
|
+
|
178
|
+
def async_run(&block)
|
179
|
+
# Prefer newer Async.run to avoid deprecated scheduler.async path.
|
180
|
+
if defined?(Async) && Async.respond_to?(:run)
|
181
|
+
Async.run(&block)
|
182
|
+
else
|
183
|
+
Async(&block)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1')
|
188
|
+
warn 'Skipping fiber scheduler tests on Ruby < 3.1'
|
189
|
+
return
|
190
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: curb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ross Bamford
|
8
8
|
- Todd A. Fisher
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-09-17 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
|
14
14
|
for the libcurl(3), a fully-featured client-side URL transfer library. cURL and
|
@@ -52,13 +52,14 @@ files:
|
|
52
52
|
- tests/bug_follow_redirect_288.rb
|
53
53
|
- tests/bug_instance_post_differs_from_class_post.rb
|
54
54
|
- tests/bug_issue102.rb
|
55
|
-
- tests/
|
55
|
+
- tests/bug_issue_noproxy.rb
|
56
|
+
- tests/bug_issue_post_redirect.rb
|
57
|
+
- tests/bug_issue_spnego.rb
|
56
58
|
- tests/bug_multi_segfault.rb
|
57
59
|
- tests/bug_postfields_crash.rb
|
58
60
|
- tests/bug_postfields_crash2.rb
|
59
61
|
- tests/bug_raise_on_callback.rb
|
60
62
|
- tests/bug_require_last_or_segfault.rb
|
61
|
-
- tests/bug_resolve.rb
|
62
63
|
- tests/bugtests.rb
|
63
64
|
- tests/helper.rb
|
64
65
|
- tests/mem_check.rb
|
@@ -74,6 +75,7 @@ files:
|
|
74
75
|
- tests/tc_curl_multi.rb
|
75
76
|
- tests/tc_curl_postfield.rb
|
76
77
|
- tests/tc_curl_protocols.rb
|
78
|
+
- tests/tc_fiber_scheduler.rb
|
77
79
|
- tests/timeout.rb
|
78
80
|
- tests/timeout_server.rb
|
79
81
|
- tests/unittests.rb
|
@@ -111,13 +113,14 @@ test_files:
|
|
111
113
|
- tests/bug_follow_redirect_288.rb
|
112
114
|
- tests/bug_instance_post_differs_from_class_post.rb
|
113
115
|
- tests/bug_issue102.rb
|
114
|
-
- tests/
|
116
|
+
- tests/bug_issue_noproxy.rb
|
117
|
+
- tests/bug_issue_post_redirect.rb
|
118
|
+
- tests/bug_issue_spnego.rb
|
115
119
|
- tests/bug_multi_segfault.rb
|
116
120
|
- tests/bug_postfields_crash.rb
|
117
121
|
- tests/bug_postfields_crash2.rb
|
118
122
|
- tests/bug_raise_on_callback.rb
|
119
123
|
- tests/bug_require_last_or_segfault.rb
|
120
|
-
- tests/bug_resolve.rb
|
121
124
|
- tests/bugtests.rb
|
122
125
|
- tests/helper.rb
|
123
126
|
- tests/mem_check.rb
|
@@ -133,6 +136,7 @@ test_files:
|
|
133
136
|
- tests/tc_curl_multi.rb
|
134
137
|
- tests/tc_curl_postfield.rb
|
135
138
|
- tests/tc_curl_protocols.rb
|
139
|
+
- tests/tc_fiber_scheduler.rb
|
136
140
|
- tests/timeout.rb
|
137
141
|
- tests/timeout_server.rb
|
138
142
|
- tests/unittests.rb
|
data/tests/bug_issue277.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
-
|
3
|
-
|
4
|
-
require 'curb'
|
5
|
-
|
6
|
-
class BugIssue102 < Test::Unit::TestCase
|
7
|
-
|
8
|
-
def test_gc_closewait
|
9
|
-
100.times do
|
10
|
-
responses = {}
|
11
|
-
requests = ["http://www.google.co.uk/", "http://www.ruby-lang.org/"]
|
12
|
-
m = Curl::Multi.new
|
13
|
-
# add a few easy handles
|
14
|
-
requests.each do |url|
|
15
|
-
responses[url] = ""
|
16
|
-
c = Curl::Easy.new(url) do|curl|
|
17
|
-
curl.follow_location = true
|
18
|
-
curl.on_body{|data| responses[url] << data; data.size }
|
19
|
-
curl.on_success {|easy| #puts "success, add more easy handles"
|
20
|
-
}
|
21
|
-
end
|
22
|
-
m.add(c)
|
23
|
-
end
|
24
|
-
|
25
|
-
m.perform do
|
26
|
-
#puts "idling... can do some work here"
|
27
|
-
end
|
28
|
-
GC.start
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
end
|
data/tests/bug_resolve.rb
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
-
|
3
|
-
class BugResolve < Test::Unit::TestCase
|
4
|
-
include BugTestServerSetupTeardown
|
5
|
-
|
6
|
-
def test_abuse_resolve
|
7
|
-
c = Curl::Easy.new("http://localhost:#{@port}/test")
|
8
|
-
while true
|
9
|
-
c.get
|
10
|
-
assert_equal 200, c.response_code
|
11
|
-
assert_match(/hi/, c.body)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
end
|