ld-eventsource 1.0.2 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +28 -81
- data/.gitignore +2 -0
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +19 -0
- data/.ldrelease/circleci/template/gems-setup.sh +16 -0
- data/.ldrelease/circleci/template/prepare.sh +17 -0
- data/.ldrelease/circleci/template/publish.sh +19 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +13 -0
- data/CHANGELOG.md +24 -0
- data/README.md +2 -2
- data/ld-eventsource.gemspec +3 -3
- data/lib/ld-eventsource/client.rb +86 -21
- data/lib/ld-eventsource/impl/backoff.rb +0 -4
- data/lib/ld-eventsource/version.rb +1 -1
- data/spec/backoff_spec.rb +52 -0
- data/spec/client_spec.rb +68 -8
- data/spec/http_stub.rb +2 -0
- metadata +36 -24
- data/Gemfile.lock +0 -46
- data/lib/ld-eventsource/impl/streaming_http.rb +0 -222
- data/spec/streaming_http_spec.rb +0 -263
@@ -38,10 +38,6 @@ module SSE
|
|
38
38
|
good_duration = Time.now.to_f - @last_good_time
|
39
39
|
@attempts = 0 if good_duration >= @reconnect_reset_interval
|
40
40
|
end
|
41
|
-
if @attempts == 0
|
42
|
-
@attempts += 1
|
43
|
-
return 0
|
44
|
-
end
|
45
41
|
@last_good_time = nil
|
46
42
|
target = ([@base_interval * (2 ** @attempts), @max_interval].min).to_f
|
47
43
|
@attempts += 1
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "ld-eventsource"
|
2
|
+
|
3
|
+
require "http_stub"
|
4
|
+
|
5
|
+
module SSE
|
6
|
+
module Impl
|
7
|
+
describe Backoff do
|
8
|
+
it "increases exponentially with jitter" do
|
9
|
+
initial = 1.5
|
10
|
+
max = 60
|
11
|
+
b = Backoff.new(initial, max)
|
12
|
+
previous = 0
|
13
|
+
|
14
|
+
for i in 1..6 do
|
15
|
+
interval = b.next_interval
|
16
|
+
expect(interval).to be > previous
|
17
|
+
target = initial * (2 ** (i - 1))
|
18
|
+
expect(interval).to be <= target
|
19
|
+
expect(interval).to be >= target / 2
|
20
|
+
previous = i
|
21
|
+
end
|
22
|
+
|
23
|
+
interval = b.next_interval
|
24
|
+
expect(interval).to be >= previous
|
25
|
+
expect(interval).to be <= max
|
26
|
+
end
|
27
|
+
|
28
|
+
it "resets to initial delay if reset threshold has elapsed" do
|
29
|
+
initial = 1.5
|
30
|
+
max = 60
|
31
|
+
threshold = 2
|
32
|
+
b = Backoff.new(initial, max, reconnect_reset_interval: threshold)
|
33
|
+
|
34
|
+
for i in 1..6 do
|
35
|
+
# just cause the backoff to increase quickly, don't actually do these delays
|
36
|
+
b.next_interval
|
37
|
+
end
|
38
|
+
|
39
|
+
b.mark_success
|
40
|
+
sleep(threshold + 0.001)
|
41
|
+
|
42
|
+
interval = b.next_interval
|
43
|
+
expect(interval).to be <= initial
|
44
|
+
expect(interval).to be >= initial / 2
|
45
|
+
|
46
|
+
interval = b.next_interval # make sure it continues increasing after that
|
47
|
+
expect(interval).to be <= (initial * 2)
|
48
|
+
expect(interval).to be >= initial
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
require "ld-eventsource"
|
2
|
-
require "socketry"
|
3
|
-
require "http_stub"
|
4
2
|
|
5
3
|
#
|
6
4
|
# End-to-end tests of the SSE client against a real server
|
@@ -62,8 +60,11 @@ EOT
|
|
62
60
|
expect(received_req.header).to eq({
|
63
61
|
"accept" => ["text/event-stream"],
|
64
62
|
"cache-control" => ["no-cache"],
|
65
|
-
"host" => ["127.0.0.1"],
|
66
|
-
"authorization" => ["secret"]
|
63
|
+
"host" => ["127.0.0.1:" + server.port.to_s],
|
64
|
+
"authorization" => ["secret"],
|
65
|
+
"user-agent" => ["ruby-eventsource"],
|
66
|
+
"content-length" => ["0"],
|
67
|
+
"connection" => ["close"]
|
67
68
|
})
|
68
69
|
end
|
69
70
|
end
|
@@ -85,9 +86,12 @@ EOT
|
|
85
86
|
expect(received_req.header).to eq({
|
86
87
|
"accept" => ["text/event-stream"],
|
87
88
|
"cache-control" => ["no-cache"],
|
88
|
-
"host" => ["127.0.0.1"],
|
89
|
+
"host" => ["127.0.0.1:" + server.port.to_s],
|
89
90
|
"authorization" => ["secret"],
|
90
|
-
"last-event-id" => [id]
|
91
|
+
"last-event-id" => [id],
|
92
|
+
"user-agent" => ["ruby-eventsource"],
|
93
|
+
"content-length" => ["0"],
|
94
|
+
"connection" => ["close"]
|
91
95
|
})
|
92
96
|
end
|
93
97
|
end
|
@@ -330,7 +334,7 @@ EOT
|
|
330
334
|
expect(event_sink.pop).to eq(simple_event_1)
|
331
335
|
if i > 0
|
332
336
|
interval = request_times[i] - request_end_times[i - 1]
|
333
|
-
expect(interval).to be <= initial_interval
|
337
|
+
expect(interval).to be <= (initial_interval + 0.1)
|
334
338
|
end
|
335
339
|
end
|
336
340
|
end
|
@@ -362,7 +366,63 @@ EOT
|
|
362
366
|
with_client(client) do |client|
|
363
367
|
expect(event_sink.pop).to eq(simple_event_1)
|
364
368
|
interval = request_times[1] - request_times[0]
|
365
|
-
expect(interval).to be <
|
369
|
+
expect(interval).to be < 0.5
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
it "connects to HTTP server through proxy" do
|
375
|
+
events_body = simple_event_1_text
|
376
|
+
with_server do |server|
|
377
|
+
server.setup_response("/") do |req,res|
|
378
|
+
send_stream_content(res, events_body, keep_open: false)
|
379
|
+
end
|
380
|
+
with_server(StubProxyServer.new) do |proxy|
|
381
|
+
event_sink = Queue.new
|
382
|
+
client = subject.new(server.base_uri, proxy: proxy.base_uri) do |c|
|
383
|
+
c.on_event { |event| event_sink << event }
|
384
|
+
end
|
385
|
+
|
386
|
+
with_client(client) do |client|
|
387
|
+
expect(event_sink.pop).to eq(simple_event_1)
|
388
|
+
expect(proxy.request_count).to eq(1)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
it "resets read timeout between events" do
|
395
|
+
event_body = simple_event_1_text
|
396
|
+
with_server do |server|
|
397
|
+
attempt = 0
|
398
|
+
server.setup_response("/") do |req,res|
|
399
|
+
attempt += 1
|
400
|
+
if attempt == 1
|
401
|
+
stream = send_stream_content(res, event_body, keep_open: true)
|
402
|
+
Thread.new do
|
403
|
+
2.times {
|
404
|
+
# write within timeout interval
|
405
|
+
sleep(0.75)
|
406
|
+
stream.write(event_body)
|
407
|
+
}
|
408
|
+
# cause timeout
|
409
|
+
sleep(1.25)
|
410
|
+
end
|
411
|
+
elsif attempt == 2
|
412
|
+
send_stream_content(res, event_body, keep_open: false)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
event_sink = Queue.new
|
417
|
+
client = subject.new(server.base_uri, reconnect_time: reconnect_asap, read_timeout: 1) do |c|
|
418
|
+
c.on_event { |event| event_sink << event }
|
419
|
+
end
|
420
|
+
|
421
|
+
with_client(client) do |client|
|
422
|
+
4.times {
|
423
|
+
expect(event_sink.pop).to eq(simple_event_1)
|
424
|
+
}
|
425
|
+
expect(attempt).to eq 2
|
366
426
|
end
|
367
427
|
end
|
368
428
|
end
|
data/spec/http_stub.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ld-eventsource
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 2.2.10
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 2.2.10
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,47 +53,53 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.3.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: webrick
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '1.
|
62
|
-
type: :
|
61
|
+
version: '1.7'
|
62
|
+
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '1.
|
68
|
+
version: '1.7'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: concurrent-ruby
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0
|
75
|
+
version: '1.0'
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0
|
82
|
+
version: '1.0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: http
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 4.4.1
|
90
|
+
- - "<"
|
88
91
|
- !ruby/object:Gem::Version
|
89
|
-
version: 0.
|
92
|
+
version: 6.0.0
|
90
93
|
type: :runtime
|
91
94
|
prerelease: false
|
92
95
|
version_requirements: !ruby/object:Gem::Requirement
|
93
96
|
requirements:
|
94
|
-
- - "
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 4.4.1
|
100
|
+
- - "<"
|
95
101
|
- !ruby/object:Gem::Version
|
96
|
-
version: 0.
|
102
|
+
version: 6.0.0
|
97
103
|
description: LaunchDarkly SSE client for Ruby
|
98
104
|
email:
|
99
105
|
- team@launchdarkly.com
|
@@ -103,10 +109,18 @@ extra_rdoc_files: []
|
|
103
109
|
files:
|
104
110
|
- ".circleci/config.yml"
|
105
111
|
- ".gitignore"
|
112
|
+
- ".ldrelease/circleci/linux/execute.sh"
|
113
|
+
- ".ldrelease/circleci/mac/execute.sh"
|
114
|
+
- ".ldrelease/circleci/template/build.sh"
|
115
|
+
- ".ldrelease/circleci/template/gems-setup.sh"
|
116
|
+
- ".ldrelease/circleci/template/prepare.sh"
|
117
|
+
- ".ldrelease/circleci/template/publish.sh"
|
118
|
+
- ".ldrelease/circleci/template/test.sh"
|
119
|
+
- ".ldrelease/circleci/template/update-version.sh"
|
120
|
+
- ".ldrelease/circleci/windows/execute.ps1"
|
106
121
|
- ".ldrelease/config.yml"
|
107
122
|
- CHANGELOG.md
|
108
123
|
- Gemfile
|
109
|
-
- Gemfile.lock
|
110
124
|
- LICENSE
|
111
125
|
- README.md
|
112
126
|
- ld-eventsource.gemspec
|
@@ -116,14 +130,13 @@ files:
|
|
116
130
|
- lib/ld-eventsource/events.rb
|
117
131
|
- lib/ld-eventsource/impl/backoff.rb
|
118
132
|
- lib/ld-eventsource/impl/event_parser.rb
|
119
|
-
- lib/ld-eventsource/impl/streaming_http.rb
|
120
133
|
- lib/ld-eventsource/version.rb
|
121
134
|
- scripts/gendocs.sh
|
122
135
|
- scripts/release.sh
|
136
|
+
- spec/backoff_spec.rb
|
123
137
|
- spec/client_spec.rb
|
124
138
|
- spec/event_parser_spec.rb
|
125
139
|
- spec/http_stub.rb
|
126
|
-
- spec/streaming_http_spec.rb
|
127
140
|
homepage: https://github.com/launchdarkly/ruby-eventsource
|
128
141
|
licenses:
|
129
142
|
- Apache-2.0
|
@@ -143,13 +156,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
156
|
- !ruby/object:Gem::Version
|
144
157
|
version: '0'
|
145
158
|
requirements: []
|
146
|
-
|
147
|
-
rubygems_version: 2.5.2.3
|
159
|
+
rubygems_version: 3.2.15
|
148
160
|
signing_key:
|
149
161
|
specification_version: 4
|
150
162
|
summary: LaunchDarkly SSE client
|
151
163
|
test_files:
|
164
|
+
- spec/backoff_spec.rb
|
152
165
|
- spec/client_spec.rb
|
153
166
|
- spec/event_parser_spec.rb
|
154
167
|
- spec/http_stub.rb
|
155
|
-
- spec/streaming_http_spec.rb
|
data/Gemfile.lock
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
ld-eventsource (1.0.2)
|
5
|
-
concurrent-ruby (~> 1.0)
|
6
|
-
http_tools (~> 0.4.5)
|
7
|
-
socketry (~> 0.5.1)
|
8
|
-
|
9
|
-
GEM
|
10
|
-
remote: https://rubygems.org/
|
11
|
-
specs:
|
12
|
-
concurrent-ruby (1.1.6)
|
13
|
-
diff-lcs (1.3)
|
14
|
-
hitimes (1.3.1)
|
15
|
-
hitimes (1.3.1-java)
|
16
|
-
http_tools (0.4.5)
|
17
|
-
rspec (3.7.0)
|
18
|
-
rspec-core (~> 3.7.0)
|
19
|
-
rspec-expectations (~> 3.7.0)
|
20
|
-
rspec-mocks (~> 3.7.0)
|
21
|
-
rspec-core (3.7.1)
|
22
|
-
rspec-support (~> 3.7.0)
|
23
|
-
rspec-expectations (3.7.0)
|
24
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
25
|
-
rspec-support (~> 3.7.0)
|
26
|
-
rspec-mocks (3.7.0)
|
27
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
-
rspec-support (~> 3.7.0)
|
29
|
-
rspec-support (3.7.0)
|
30
|
-
rspec_junit_formatter (0.3.0)
|
31
|
-
rspec-core (>= 2, < 4, != 2.12.0)
|
32
|
-
socketry (0.5.1)
|
33
|
-
hitimes (~> 1.2)
|
34
|
-
|
35
|
-
PLATFORMS
|
36
|
-
java
|
37
|
-
ruby
|
38
|
-
|
39
|
-
DEPENDENCIES
|
40
|
-
bundler (~> 1.7)
|
41
|
-
ld-eventsource!
|
42
|
-
rspec (~> 3.2)
|
43
|
-
rspec_junit_formatter (~> 0.3.0)
|
44
|
-
|
45
|
-
BUNDLED WITH
|
46
|
-
1.17.3
|
@@ -1,222 +0,0 @@
|
|
1
|
-
require "ld-eventsource/errors"
|
2
|
-
|
3
|
-
require "concurrent/atomics"
|
4
|
-
require "http_tools"
|
5
|
-
require "socketry"
|
6
|
-
|
7
|
-
module SSE
|
8
|
-
module Impl
|
9
|
-
#
|
10
|
-
# Wrapper around a socket providing a simplified HTTP request-response cycle including streaming.
|
11
|
-
# The socket is created and managed by Socketry, which we use so that we can have a read timeout.
|
12
|
-
#
|
13
|
-
class StreamingHTTPConnection
|
14
|
-
attr_reader :status, :headers
|
15
|
-
|
16
|
-
#
|
17
|
-
# Opens a new connection.
|
18
|
-
#
|
19
|
-
# @param [String] uri the URI to connect o
|
20
|
-
# @param [String] proxy the proxy server URI, if any
|
21
|
-
# @param [Hash] headers request headers
|
22
|
-
# @param [Float] connect_timeout connection timeout
|
23
|
-
# @param [Float] read_timeout read timeout
|
24
|
-
#
|
25
|
-
def initialize(uri, proxy: nil, headers: {}, connect_timeout: nil, read_timeout: nil)
|
26
|
-
@socket = HTTPConnectionFactory.connect(uri, proxy, connect_timeout, read_timeout)
|
27
|
-
@socket.write(build_request(uri, headers))
|
28
|
-
@reader = HTTPResponseReader.new(@socket, read_timeout)
|
29
|
-
@status = @reader.status
|
30
|
-
@headers = @reader.headers
|
31
|
-
@closed = Concurrent::AtomicBoolean.new(false)
|
32
|
-
end
|
33
|
-
|
34
|
-
#
|
35
|
-
# Closes the connection.
|
36
|
-
#
|
37
|
-
def close
|
38
|
-
if @closed.make_true
|
39
|
-
@socket.close if @socket
|
40
|
-
@socket = nil
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
#
|
45
|
-
# Generator that returns one line of the response body at a time (delimited by \r, \n,
|
46
|
-
# or \r\n) until the response is fully consumed or the socket is closed.
|
47
|
-
#
|
48
|
-
def read_lines
|
49
|
-
@reader.read_lines
|
50
|
-
end
|
51
|
-
|
52
|
-
#
|
53
|
-
# Consumes the entire response body and returns it.
|
54
|
-
#
|
55
|
-
# @return [String] the response body
|
56
|
-
#
|
57
|
-
def read_all
|
58
|
-
@reader.read_all
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
# Build an HTTP request line and headers.
|
64
|
-
def build_request(uri, headers)
|
65
|
-
ret = "GET #{uri.request_uri} HTTP/1.1\r\n"
|
66
|
-
ret << "Host: #{uri.host}\r\n"
|
67
|
-
headers.each { |k, v|
|
68
|
-
ret << "#{k}: #{v}\r\n"
|
69
|
-
}
|
70
|
-
ret + "\r\n"
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
#
|
75
|
-
# Used internally to send the HTTP request, including the proxy dialogue if necessary.
|
76
|
-
# @private
|
77
|
-
#
|
78
|
-
class HTTPConnectionFactory
|
79
|
-
def self.connect(uri, proxy, connect_timeout, read_timeout)
|
80
|
-
if !proxy
|
81
|
-
return open_socket(uri, connect_timeout)
|
82
|
-
end
|
83
|
-
|
84
|
-
socket = open_socket(proxy, connect_timeout)
|
85
|
-
socket.write(build_proxy_request(uri, proxy))
|
86
|
-
|
87
|
-
# temporarily create a reader just for the proxy connect response
|
88
|
-
proxy_reader = HTTPResponseReader.new(socket, read_timeout)
|
89
|
-
if proxy_reader.status != 200
|
90
|
-
raise Errors::HTTPProxyError.new(proxy_reader.status)
|
91
|
-
end
|
92
|
-
|
93
|
-
# start using TLS at this point if appropriate
|
94
|
-
if uri.scheme.downcase == 'https'
|
95
|
-
wrap_socket_in_ssl_socket(socket)
|
96
|
-
else
|
97
|
-
socket
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def self.open_socket(uri, connect_timeout)
|
104
|
-
if uri.scheme.downcase == 'https'
|
105
|
-
Socketry::SSL::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
|
106
|
-
else
|
107
|
-
Socketry::TCP::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
# Build a proxy connection header.
|
112
|
-
def self.build_proxy_request(uri, proxy)
|
113
|
-
ret = "CONNECT #{uri.host}:#{uri.port} HTTP/1.1\r\n"
|
114
|
-
ret << "Host: #{uri.host}:#{uri.port}\r\n"
|
115
|
-
if proxy.user || proxy.password
|
116
|
-
encoded_credentials = Base64.strict_encode64([proxy.user || '', proxy.password || ''].join(":"))
|
117
|
-
ret << "Proxy-Authorization: Basic #{encoded_credentials}\r\n"
|
118
|
-
end
|
119
|
-
ret << "\r\n"
|
120
|
-
ret
|
121
|
-
end
|
122
|
-
|
123
|
-
def self.wrap_socket_in_ssl_socket(socket)
|
124
|
-
io = IO.try_convert(socket)
|
125
|
-
ssl_sock = OpenSSL::SSL::SSLSocket.new(io, OpenSSL::SSL::SSLContext.new)
|
126
|
-
ssl_sock.connect
|
127
|
-
Socketry::SSL::Socket.new.from_socket(ssl_sock)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
#
|
132
|
-
# Used internally to read the HTTP response, either all at once or as a stream of text lines.
|
133
|
-
# Incoming data is fed into an instance of HTTPTools::Parser, which gives us the header and
|
134
|
-
# chunks of the body via callbacks.
|
135
|
-
# @private
|
136
|
-
#
|
137
|
-
class HTTPResponseReader
|
138
|
-
DEFAULT_CHUNK_SIZE = 10000
|
139
|
-
|
140
|
-
attr_reader :status, :headers
|
141
|
-
|
142
|
-
def initialize(socket, read_timeout)
|
143
|
-
@socket = socket
|
144
|
-
@read_timeout = read_timeout
|
145
|
-
@parser = HTTPTools::Parser.new
|
146
|
-
@buffer = ""
|
147
|
-
@done = false
|
148
|
-
@lock = Mutex.new
|
149
|
-
|
150
|
-
# Provide callbacks for the Parser to give us the headers and body. This has to be done
|
151
|
-
# before we start piping any data into the parser.
|
152
|
-
have_headers = false
|
153
|
-
@parser.on(:header) do
|
154
|
-
have_headers = true
|
155
|
-
end
|
156
|
-
@parser.on(:stream) do |data|
|
157
|
-
@lock.synchronize { @buffer << data } # synchronize because we're called from another thread in Socketry
|
158
|
-
end
|
159
|
-
@parser.on(:finish) do
|
160
|
-
@lock.synchronize { @done = true }
|
161
|
-
end
|
162
|
-
|
163
|
-
# Block until the status code and headers have been successfully read.
|
164
|
-
while !have_headers
|
165
|
-
raise EOFError if !read_chunk_into_buffer
|
166
|
-
end
|
167
|
-
@headers = Hash[@parser.header.map { |k,v| [k.downcase, v] }]
|
168
|
-
@status = @parser.status_code
|
169
|
-
end
|
170
|
-
|
171
|
-
def read_lines
|
172
|
-
Enumerator.new do |gen|
|
173
|
-
loop do
|
174
|
-
line = read_line
|
175
|
-
break if line.nil?
|
176
|
-
gen.yield line
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def read_all
|
182
|
-
while read_chunk_into_buffer
|
183
|
-
end
|
184
|
-
@buffer
|
185
|
-
end
|
186
|
-
|
187
|
-
private
|
188
|
-
|
189
|
-
# Attempt to read some more data from the socket. Return true if successful, false if EOF.
|
190
|
-
# A read timeout will result in an exception from Socketry's readpartial method.
|
191
|
-
def read_chunk_into_buffer
|
192
|
-
# If @done is set, it means the Parser has signaled end of response body
|
193
|
-
@lock.synchronize { return false if @done }
|
194
|
-
begin
|
195
|
-
data = @socket.readpartial(DEFAULT_CHUNK_SIZE, timeout: @read_timeout)
|
196
|
-
rescue Socketry::TimeoutError
|
197
|
-
# We rethrow this as our own type so the caller doesn't have to know the Socketry API
|
198
|
-
raise Errors::ReadTimeoutError.new(@read_timeout)
|
199
|
-
end
|
200
|
-
return false if data == :eof
|
201
|
-
@parser << data
|
202
|
-
# We are piping the content through the parser so that it can handle things like chunked
|
203
|
-
# encoding for us. The content ends up being appended to @buffer via our callback.
|
204
|
-
true
|
205
|
-
end
|
206
|
-
|
207
|
-
# Extract the next line of text from the read buffer, refilling the buffer as needed.
|
208
|
-
def read_line
|
209
|
-
loop do
|
210
|
-
@lock.synchronize do
|
211
|
-
i = @buffer.index(/[\r\n]/)
|
212
|
-
if !i.nil?
|
213
|
-
i += 1 if (@buffer[i] == "\r" && i < @buffer.length - 1 && @buffer[i + 1] == "\n")
|
214
|
-
return @buffer.slice!(0, i + 1).force_encoding(Encoding::UTF_8)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
return nil if !read_chunk_into_buffer
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|