ld-eventsource 1.0.3 → 2.0.0.pre.beta.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/.circleci/config.yml +12 -53
- data/.gitignore +1 -0
- data/.ldrelease/config.yml +2 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +28 -9
- data/README.md +2 -2
- data/ld-eventsource.gemspec +1 -2
- data/lib/ld-eventsource/client.rb +86 -21
- data/lib/ld-eventsource/version.rb +1 -1
- data/spec/backoff_spec.rb +2 -0
- data/spec/client_spec.rb +66 -6
- data/spec/http_stub.rb +2 -0
- metadata +7 -24
- data/lib/ld-eventsource/impl/streaming_http.rb +0 -222
- data/spec/streaming_http_spec.rb +0 -263
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abed7a7f61d7f731895a05d722273996e683fab0
|
|
4
|
+
data.tar.gz: aa41fa22d28b66bd6bb6585f4030b06cb3b0dc05
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6597dfb8e3c7b1d4103ea18c44bb7d83a598b33565f79403fbec790d361c97ad1988b11027229e20cab4ec9e3895ad47faee37949eb1829a479d15b941f5d3e0
|
|
7
|
+
data.tar.gz: 910c3a810cc7534d316910dc06b95d311a3d22a659956a68d78c31a9f78b3210e9f019e94851e71b15e50271c74e1257735e1f57097d9c52f209acb82d837ac9
|
data/.circleci/config.yml
CHANGED
|
@@ -4,11 +4,10 @@ workflows:
|
|
|
4
4
|
version: 2
|
|
5
5
|
test:
|
|
6
6
|
jobs:
|
|
7
|
-
- test-misc-rubies
|
|
8
|
-
- test-2.2
|
|
9
|
-
- test-2.3
|
|
10
7
|
- test-2.4
|
|
11
8
|
- test-2.5
|
|
9
|
+
- test-2.6
|
|
10
|
+
- test-2.7
|
|
12
11
|
- test-jruby-9.2
|
|
13
12
|
|
|
14
13
|
ruby-docker-template: &ruby-docker-template
|
|
@@ -18,6 +17,7 @@ ruby-docker-template: &ruby-docker-template
|
|
|
18
17
|
if [[ $CIRCLE_JOB == test-jruby* ]]; then
|
|
19
18
|
gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
|
|
20
19
|
fi
|
|
20
|
+
- run: sudo apt-get update -y && sudo apt-get install -y build-essential
|
|
21
21
|
- run: ruby -v
|
|
22
22
|
- run: gem install bundler -v "~> 1.17"
|
|
23
23
|
- run: bundle install
|
|
@@ -29,64 +29,23 @@ ruby-docker-template: &ruby-docker-template
|
|
|
29
29
|
path: ./rspec
|
|
30
30
|
|
|
31
31
|
jobs:
|
|
32
|
-
test-2.
|
|
32
|
+
test-2.4:
|
|
33
33
|
<<: *ruby-docker-template
|
|
34
34
|
docker:
|
|
35
|
-
- image: circleci/ruby:2.
|
|
36
|
-
test-2.
|
|
35
|
+
- image: circleci/ruby:2.4
|
|
36
|
+
test-2.5:
|
|
37
37
|
<<: *ruby-docker-template
|
|
38
38
|
docker:
|
|
39
|
-
- image: circleci/ruby:2.
|
|
40
|
-
test-2.
|
|
39
|
+
- image: circleci/ruby:2.5
|
|
40
|
+
test-2.6:
|
|
41
41
|
<<: *ruby-docker-template
|
|
42
42
|
docker:
|
|
43
|
-
- image: circleci/ruby:2.
|
|
44
|
-
test-2.
|
|
43
|
+
- image: circleci/ruby:2.6
|
|
44
|
+
test-2.7:
|
|
45
45
|
<<: *ruby-docker-template
|
|
46
46
|
docker:
|
|
47
|
-
- image: circleci/ruby:2.
|
|
47
|
+
- image: circleci/ruby:2.7
|
|
48
48
|
test-jruby-9.2:
|
|
49
49
|
<<: *ruby-docker-template
|
|
50
50
|
docker:
|
|
51
|
-
- image: circleci/jruby:9-jdk
|
|
52
|
-
|
|
53
|
-
# The following very slow job uses an Ubuntu container to run the Ruby versions that
|
|
54
|
-
# CircleCI doesn't provide Docker images for.
|
|
55
|
-
test-misc-rubies:
|
|
56
|
-
machine:
|
|
57
|
-
image: circleci/classic:latest
|
|
58
|
-
environment:
|
|
59
|
-
- RUBIES: "jruby-9.1.17.0"
|
|
60
|
-
steps:
|
|
61
|
-
- checkout
|
|
62
|
-
- run:
|
|
63
|
-
name: install all Ruby versions
|
|
64
|
-
command: "parallel rvm install ::: $RUBIES"
|
|
65
|
-
- run:
|
|
66
|
-
name: bundle install for all versions
|
|
67
|
-
shell: /bin/bash -leo pipefail # need -l in order for "rvm use" to work
|
|
68
|
-
command: |
|
|
69
|
-
set -e;
|
|
70
|
-
for i in $RUBIES;
|
|
71
|
-
do
|
|
72
|
-
rvm use $i;
|
|
73
|
-
if [[ $i == jruby* ]]; then
|
|
74
|
-
gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
|
|
75
|
-
fi;
|
|
76
|
-
# bundler 2.0 may be preinstalled, we need to remove it if so
|
|
77
|
-
yes | gem uninstall bundler --version '>=2.0' || true;
|
|
78
|
-
gem install bundler -v "~> 1.17";
|
|
79
|
-
bundle install;
|
|
80
|
-
mv Gemfile.lock "Gemfile.lock.$i"
|
|
81
|
-
done
|
|
82
|
-
- run:
|
|
83
|
-
name: run tests for all versions
|
|
84
|
-
shell: /bin/bash -leo pipefail
|
|
85
|
-
command: |
|
|
86
|
-
set -e;
|
|
87
|
-
for i in $RUBIES;
|
|
88
|
-
do
|
|
89
|
-
rvm use $i;
|
|
90
|
-
cp "Gemfile.lock.$i" Gemfile.lock;
|
|
91
|
-
bundle exec rspec spec;
|
|
92
|
-
done
|
|
51
|
+
- image: circleci/jruby:9.2-jdk
|
data/.gitignore
CHANGED
data/.ldrelease/config.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the LaunchDarkly SSE Client for Ruby will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
|
4
4
|
|
|
5
|
+
## [1.0.3] - 2020-03-17
|
|
6
|
+
### Fixed:
|
|
7
|
+
- The backoff delay logic for reconnecting after a stream failure was broken so that if a failure occurred after a stream had been active for at least `reconnect_reset_interval` (default 60 seconds), retries would use _no_ delay, potentially causing a flood of requests and a spike in CPU usage.
|
|
8
|
+
|
|
5
9
|
## [1.0.2] - 2020-03-10
|
|
6
10
|
### Removed:
|
|
7
11
|
- Removed an unused dependency on `rake`. There are no other changes in this release.
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
ld-eventsource (
|
|
4
|
+
ld-eventsource (2.0.0.pre.beta.1)
|
|
5
5
|
concurrent-ruby (~> 1.0)
|
|
6
|
-
|
|
7
|
-
socketry (~> 0.5.1)
|
|
6
|
+
http (~> 4.4.1)
|
|
8
7
|
|
|
9
8
|
GEM
|
|
10
9
|
remote: https://rubygems.org/
|
|
11
10
|
specs:
|
|
12
|
-
|
|
11
|
+
addressable (2.7.0)
|
|
12
|
+
public_suffix (>= 2.0.2, < 5.0)
|
|
13
|
+
concurrent-ruby (1.1.8)
|
|
13
14
|
diff-lcs (1.3)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
domain_name (0.5.20190701)
|
|
16
|
+
unf (>= 0.0.5, < 1.0.0)
|
|
17
|
+
ffi (1.14.2)
|
|
18
|
+
ffi (1.14.2-java)
|
|
19
|
+
ffi-compiler (1.0.1)
|
|
20
|
+
ffi (>= 1.0.0)
|
|
21
|
+
rake
|
|
22
|
+
http (4.4.1)
|
|
23
|
+
addressable (~> 2.3)
|
|
24
|
+
http-cookie (~> 1.0)
|
|
25
|
+
http-form_data (~> 2.2)
|
|
26
|
+
http-parser (~> 1.2.0)
|
|
27
|
+
http-cookie (1.0.3)
|
|
28
|
+
domain_name (~> 0.5)
|
|
29
|
+
http-form_data (2.3.0)
|
|
30
|
+
http-parser (1.2.3)
|
|
31
|
+
ffi-compiler (>= 1.0, < 2.0)
|
|
32
|
+
public_suffix (4.0.6)
|
|
33
|
+
rake (13.0.3)
|
|
17
34
|
rspec (3.7.0)
|
|
18
35
|
rspec-core (~> 3.7.0)
|
|
19
36
|
rspec-expectations (~> 3.7.0)
|
|
@@ -29,8 +46,10 @@ GEM
|
|
|
29
46
|
rspec-support (3.7.0)
|
|
30
47
|
rspec_junit_formatter (0.3.0)
|
|
31
48
|
rspec-core (>= 2, < 4, != 2.12.0)
|
|
32
|
-
|
|
33
|
-
|
|
49
|
+
unf (0.1.4)
|
|
50
|
+
unf_ext
|
|
51
|
+
unf (0.1.4-java)
|
|
52
|
+
unf_ext (0.0.7.7)
|
|
34
53
|
|
|
35
54
|
PLATFORMS
|
|
36
55
|
java
|
data/README.md
CHANGED
|
@@ -3,14 +3,14 @@ LaunchDarkly SSE Client for Ruby
|
|
|
3
3
|
|
|
4
4
|
[](http://badge.fury.io/rb/ld-eventsource) [](https://circleci.com/gh/launchdarkly/ruby-eventsource/tree/master)
|
|
5
5
|
|
|
6
|
-
A client for the [Server-Sent Events](https://www.w3.org/TR/eventsource/) protocol. This implementation runs on a worker thread, and uses the [`
|
|
6
|
+
A client for the [Server-Sent Events](https://www.w3.org/TR/eventsource/) protocol. This implementation runs on a worker thread, and uses the [`http`](https://rubygems.org/gems/http) gem to manage a persistent connection. Its primary purpose is to support the [LaunchDarkly SDK for Ruby](https://github.com/launchdarkly/ruby-client), but it can be used independently.
|
|
7
7
|
|
|
8
8
|
Parts of this code are based on https://github.com/Tonkpils/celluloid-eventsource, but it does not use Celluloid.
|
|
9
9
|
|
|
10
10
|
Supported Ruby versions
|
|
11
11
|
-----------------------
|
|
12
12
|
|
|
13
|
-
This gem has a minimum Ruby version of 2.
|
|
13
|
+
This gem has a minimum Ruby version of 2.4, or 9.2 for JRuby.
|
|
14
14
|
|
|
15
15
|
Quick setup
|
|
16
16
|
-----------
|
data/ld-eventsource.gemspec
CHANGED
|
@@ -25,6 +25,5 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
|
|
26
26
|
|
|
27
27
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
|
28
|
-
spec.add_runtime_dependency "
|
|
29
|
-
spec.add_runtime_dependency "socketry", "~> 0.5.1"
|
|
28
|
+
spec.add_runtime_dependency "http", "~> 4.4.1"
|
|
30
29
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
require "ld-eventsource/impl/backoff"
|
|
2
2
|
require "ld-eventsource/impl/event_parser"
|
|
3
|
-
require "ld-eventsource/impl/streaming_http"
|
|
4
3
|
require "ld-eventsource/events"
|
|
5
4
|
require "ld-eventsource/errors"
|
|
6
5
|
|
|
@@ -8,6 +7,7 @@ require "concurrent/atomics"
|
|
|
8
7
|
require "logger"
|
|
9
8
|
require "thread"
|
|
10
9
|
require "uri"
|
|
10
|
+
require "http"
|
|
11
11
|
|
|
12
12
|
module SSE
|
|
13
13
|
#
|
|
@@ -80,6 +80,9 @@ module SSE
|
|
|
80
80
|
# proxy with the `HTTP_PROXY` or `HTTPS_PROXY` environment variable)
|
|
81
81
|
# @param logger [Logger] a Logger instance for the client to use for diagnostic output;
|
|
82
82
|
# defaults to a logger with WARN level that goes to standard output
|
|
83
|
+
# @param socket_factory [#open] (nil) an optional factory object for creating sockets,
|
|
84
|
+
# if you want to use something other than the default `TCPSocket`; it must implement
|
|
85
|
+
# `open(uri, timeout)` to return a connected `Socket`
|
|
83
86
|
# @yieldparam [Client] client the new client instance, before opening the connection
|
|
84
87
|
#
|
|
85
88
|
def initialize(uri,
|
|
@@ -90,7 +93,8 @@ module SSE
|
|
|
90
93
|
reconnect_reset_interval: DEFAULT_RECONNECT_RESET_INTERVAL,
|
|
91
94
|
last_event_id: nil,
|
|
92
95
|
proxy: nil,
|
|
93
|
-
logger: nil
|
|
96
|
+
logger: nil,
|
|
97
|
+
socket_factory: nil)
|
|
94
98
|
@uri = URI(uri)
|
|
95
99
|
@stopped = Concurrent::AtomicBoolean.new(false)
|
|
96
100
|
|
|
@@ -98,7 +102,11 @@ module SSE
|
|
|
98
102
|
@connect_timeout = connect_timeout
|
|
99
103
|
@read_timeout = read_timeout
|
|
100
104
|
@logger = logger || default_logger
|
|
101
|
-
|
|
105
|
+
http_client_options = {}
|
|
106
|
+
if socket_factory
|
|
107
|
+
http_client_options["socket_class"] = socket_factory
|
|
108
|
+
end
|
|
109
|
+
|
|
102
110
|
if proxy
|
|
103
111
|
@proxy = proxy
|
|
104
112
|
else
|
|
@@ -108,6 +116,21 @@ module SSE
|
|
|
108
116
|
end
|
|
109
117
|
end
|
|
110
118
|
|
|
119
|
+
if @proxy
|
|
120
|
+
http_client_options["proxy"] = {
|
|
121
|
+
:proxy_address => @proxy.host,
|
|
122
|
+
:proxy_port => @proxy.port
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@http_client = HTTP::Client.new(http_client_options)
|
|
127
|
+
.timeout({
|
|
128
|
+
read: read_timeout,
|
|
129
|
+
connect: connect_timeout
|
|
130
|
+
})
|
|
131
|
+
@buffer = ""
|
|
132
|
+
@lock = Mutex.new
|
|
133
|
+
|
|
111
134
|
@backoff = Impl::Backoff.new(reconnect_time || DEFAULT_RECONNECT_TIME, MAX_RECONNECT_TIME,
|
|
112
135
|
reconnect_reset_interval: reconnect_reset_interval)
|
|
113
136
|
|
|
@@ -163,12 +186,56 @@ module SSE
|
|
|
163
186
|
#
|
|
164
187
|
def close
|
|
165
188
|
if @stopped.make_true
|
|
166
|
-
|
|
167
|
-
@cxn = nil
|
|
189
|
+
reset_http
|
|
168
190
|
end
|
|
169
191
|
end
|
|
170
192
|
|
|
171
193
|
private
|
|
194
|
+
|
|
195
|
+
def reset_http
|
|
196
|
+
@http_client.close if !@http_client.nil?
|
|
197
|
+
@cxn = nil
|
|
198
|
+
@buffer = ""
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def read_lines
|
|
202
|
+
Enumerator.new do |gen|
|
|
203
|
+
loop do
|
|
204
|
+
line = read_line
|
|
205
|
+
break if line.nil?
|
|
206
|
+
gen.yield line
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def read_line
|
|
212
|
+
loop do
|
|
213
|
+
@lock.synchronize do
|
|
214
|
+
i = @buffer.index(/[\r\n]/)
|
|
215
|
+
if !i.nil? && !(i == @buffer.length - 1 && @buffer[i] == "\r")
|
|
216
|
+
i += 1 if (@buffer[i] == "\r" && @buffer[i + 1] == "\n")
|
|
217
|
+
return @buffer.slice!(0, i + 1).force_encoding(Encoding::UTF_8)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
return nil if !read_chunk_into_buffer
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def read_chunk_into_buffer
|
|
225
|
+
# If @done is set, it means the Parser has signaled end of response body
|
|
226
|
+
@lock.synchronize { return false if @done }
|
|
227
|
+
begin
|
|
228
|
+
data = @cxn.readpartial
|
|
229
|
+
rescue HTTP::TimeoutError
|
|
230
|
+
# We rethrow this as our own type so the caller doesn't have to know the httprb API
|
|
231
|
+
raise Errors::ReadTimeoutError.new(@read_timeout)
|
|
232
|
+
end
|
|
233
|
+
return false if data == nil
|
|
234
|
+
@buffer << data
|
|
235
|
+
# We are piping the content through the parser so that it can handle things like chunked
|
|
236
|
+
# encoding for us. The content ends up being appended to @buffer via our callback.
|
|
237
|
+
true
|
|
238
|
+
end
|
|
172
239
|
|
|
173
240
|
def default_logger
|
|
174
241
|
log = ::Logger.new($stdout)
|
|
@@ -196,7 +263,7 @@ module SSE
|
|
|
196
263
|
end
|
|
197
264
|
end
|
|
198
265
|
begin
|
|
199
|
-
|
|
266
|
+
reset_http
|
|
200
267
|
rescue StandardError => e
|
|
201
268
|
log_and_dispatch_error(e, "Unexpected error while closing stream")
|
|
202
269
|
end
|
|
@@ -215,31 +282,28 @@ module SSE
|
|
|
215
282
|
cxn = nil
|
|
216
283
|
begin
|
|
217
284
|
@logger.info { "Connecting to event stream at #{@uri}" }
|
|
218
|
-
cxn =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
read_timeout: @read_timeout
|
|
223
|
-
)
|
|
224
|
-
if cxn.status == 200
|
|
285
|
+
cxn = @http_client.request("GET", @uri, {
|
|
286
|
+
headers: build_headers
|
|
287
|
+
})
|
|
288
|
+
if cxn.status.code == 200
|
|
225
289
|
content_type = cxn.headers["content-type"]
|
|
226
290
|
if content_type && content_type.start_with?("text/event-stream")
|
|
227
291
|
return cxn # we're good to proceed
|
|
228
292
|
else
|
|
229
|
-
|
|
293
|
+
reset_http
|
|
230
294
|
err = Errors::HTTPContentTypeError.new(cxn.headers["content-type"])
|
|
231
295
|
@on[:error].call(err)
|
|
232
296
|
@logger.warn { "Event source returned unexpected content type '#{cxn.headers["content-type"]}'" }
|
|
233
297
|
end
|
|
234
298
|
else
|
|
235
|
-
body = cxn.
|
|
236
|
-
|
|
237
|
-
@logger.info { "Server returned error status #{cxn.status}" }
|
|
238
|
-
err = Errors::HTTPStatusError.new(cxn.status, body)
|
|
299
|
+
body = cxn.to_s # grab the whole response body in case it has error details
|
|
300
|
+
reset_http
|
|
301
|
+
@logger.info { "Server returned error status #{cxn.status.code}" }
|
|
302
|
+
err = Errors::HTTPStatusError.new(cxn.status.code, body)
|
|
239
303
|
@on[:error].call(err)
|
|
240
304
|
end
|
|
241
305
|
rescue
|
|
242
|
-
|
|
306
|
+
reset_http
|
|
243
307
|
raise # will be handled in run_stream
|
|
244
308
|
end
|
|
245
309
|
# if unsuccessful, continue the loop to connect again
|
|
@@ -253,7 +317,7 @@ module SSE
|
|
|
253
317
|
# it can automatically reset itself if enough time passes between failures.
|
|
254
318
|
@backoff.mark_success
|
|
255
319
|
|
|
256
|
-
event_parser = Impl::EventParser.new(
|
|
320
|
+
event_parser = Impl::EventParser.new(read_lines)
|
|
257
321
|
event_parser.items.each do |item|
|
|
258
322
|
return if @stopped.value
|
|
259
323
|
case item
|
|
@@ -288,7 +352,8 @@ module SSE
|
|
|
288
352
|
def build_headers
|
|
289
353
|
h = {
|
|
290
354
|
'Accept' => 'text/event-stream',
|
|
291
|
-
'Cache-Control' => 'no-cache'
|
|
355
|
+
'Cache-Control' => 'no-cache',
|
|
356
|
+
'User-Agent' => 'ruby-eventsource'
|
|
292
357
|
}
|
|
293
358
|
h['Last-Event-Id'] = @last_id if !@last_id.nil?
|
|
294
359
|
h.merge(@headers)
|
data/spec/backoff_spec.rb
CHANGED
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
|
|
@@ -366,4 +370,60 @@ EOT
|
|
|
366
370
|
end
|
|
367
371
|
end
|
|
368
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
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
369
429
|
end
|
data/spec/http_stub.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ld-eventsource
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0.pre.beta.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-01-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -67,33 +67,19 @@ dependencies:
|
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '1.0'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
70
|
+
name: http
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version:
|
|
75
|
+
version: 4.4.1
|
|
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:
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: socketry
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - "~>"
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: 0.5.1
|
|
90
|
-
type: :runtime
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - "~>"
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: 0.5.1
|
|
82
|
+
version: 4.4.1
|
|
97
83
|
description: LaunchDarkly SSE client for Ruby
|
|
98
84
|
email:
|
|
99
85
|
- team@launchdarkly.com
|
|
@@ -116,7 +102,6 @@ files:
|
|
|
116
102
|
- lib/ld-eventsource/events.rb
|
|
117
103
|
- lib/ld-eventsource/impl/backoff.rb
|
|
118
104
|
- lib/ld-eventsource/impl/event_parser.rb
|
|
119
|
-
- lib/ld-eventsource/impl/streaming_http.rb
|
|
120
105
|
- lib/ld-eventsource/version.rb
|
|
121
106
|
- scripts/gendocs.sh
|
|
122
107
|
- scripts/release.sh
|
|
@@ -124,7 +109,6 @@ files:
|
|
|
124
109
|
- spec/client_spec.rb
|
|
125
110
|
- spec/event_parser_spec.rb
|
|
126
111
|
- spec/http_stub.rb
|
|
127
|
-
- spec/streaming_http_spec.rb
|
|
128
112
|
homepage: https://github.com/launchdarkly/ruby-eventsource
|
|
129
113
|
licenses:
|
|
130
114
|
- Apache-2.0
|
|
@@ -140,9 +124,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
140
124
|
version: '0'
|
|
141
125
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
126
|
requirements:
|
|
143
|
-
- - "
|
|
127
|
+
- - ">"
|
|
144
128
|
- !ruby/object:Gem::Version
|
|
145
|
-
version:
|
|
129
|
+
version: 1.3.1
|
|
146
130
|
requirements: []
|
|
147
131
|
rubyforge_project:
|
|
148
132
|
rubygems_version: 2.5.2.3
|
|
@@ -154,4 +138,3 @@ test_files:
|
|
|
154
138
|
- spec/client_spec.rb
|
|
155
139
|
- spec/event_parser_spec.rb
|
|
156
140
|
- spec/http_stub.rb
|
|
157
|
-
- spec/streaming_http_spec.rb
|
|
@@ -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
|
data/spec/streaming_http_spec.rb
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
require "ld-eventsource/impl/streaming_http"
|
|
2
|
-
require "socketry"
|
|
3
|
-
require "http_stub"
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
# End-to-end tests of HTTP requests against a real server
|
|
7
|
-
#
|
|
8
|
-
describe SSE::Impl::StreamingHTTPConnection do
|
|
9
|
-
subject { SSE::Impl::StreamingHTTPConnection }
|
|
10
|
-
|
|
11
|
-
def with_connection(cxn)
|
|
12
|
-
begin
|
|
13
|
-
yield cxn
|
|
14
|
-
ensure
|
|
15
|
-
cxn.close
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
it "makes HTTP connection and sends request" do
|
|
20
|
-
with_server do |server|
|
|
21
|
-
requests = Queue.new
|
|
22
|
-
server.setup_response("/foo") do |req,res|
|
|
23
|
-
requests << req
|
|
24
|
-
res.status = 200
|
|
25
|
-
end
|
|
26
|
-
headers = {
|
|
27
|
-
"Accept" => "text/plain"
|
|
28
|
-
}
|
|
29
|
-
with_connection(subject.new(server.base_uri.merge("/foo?bar"), headers: headers)) do
|
|
30
|
-
received_req = requests.pop
|
|
31
|
-
expect(received_req.unparsed_uri).to eq("/foo?bar")
|
|
32
|
-
expect(received_req.header).to eq({
|
|
33
|
-
"accept" => ["text/plain"],
|
|
34
|
-
"host" => [server.base_uri.host]
|
|
35
|
-
})
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
it "receives response status" do
|
|
41
|
-
with_server do |server|
|
|
42
|
-
server.setup_response("/foo") do |req,res|
|
|
43
|
-
res.status = 204
|
|
44
|
-
end
|
|
45
|
-
with_connection(subject.new(server.base_uri.merge("/foo"))) do |cxn|
|
|
46
|
-
expect(cxn.status).to eq(204)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
it "receives response headers" do
|
|
52
|
-
with_server do |server|
|
|
53
|
-
server.setup_response("/foo") do |req,res|
|
|
54
|
-
res["Content-Type"] = "application/json"
|
|
55
|
-
end
|
|
56
|
-
with_connection(subject.new(server.base_uri.merge("/foo"))) do |cxn|
|
|
57
|
-
expect(cxn.headers["content-type"]).to eq("application/json")
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it "can read response as lines" do
|
|
63
|
-
body = <<-EOT
|
|
64
|
-
This is
|
|
65
|
-
a response
|
|
66
|
-
EOT
|
|
67
|
-
with_server do |server|
|
|
68
|
-
server.setup_response("/foo") do |req,res|
|
|
69
|
-
res.body = body
|
|
70
|
-
end
|
|
71
|
-
with_connection(subject.new(server.base_uri.merge("/foo"))) do |cxn|
|
|
72
|
-
lines = cxn.read_lines
|
|
73
|
-
expect(lines.next).to eq("This is\n")
|
|
74
|
-
expect(lines.next).to eq("a response\n")
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
it "can read entire response body" do
|
|
80
|
-
body = <<-EOT
|
|
81
|
-
This is
|
|
82
|
-
a response
|
|
83
|
-
EOT
|
|
84
|
-
with_server do |server|
|
|
85
|
-
server.setup_response("/foo") do |req,res|
|
|
86
|
-
res.body = body
|
|
87
|
-
end
|
|
88
|
-
with_connection(subject.new(server.base_uri.merge("/foo"))) do |cxn|
|
|
89
|
-
read_body = cxn.read_all
|
|
90
|
-
expect(read_body).to eq("This is\na response\n")
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
it "enforces read timeout" do
|
|
96
|
-
with_server do |server|
|
|
97
|
-
server.setup_response("/") do |req,res|
|
|
98
|
-
sleep(2)
|
|
99
|
-
res.status = 200
|
|
100
|
-
end
|
|
101
|
-
expect { subject.new(server.base_uri, read_timeout: 0.25) }.to raise_error(SSE::Errors::ReadTimeoutError)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
it "connects to HTTP server through proxy" do
|
|
106
|
-
body = "hi"
|
|
107
|
-
with_server do |server|
|
|
108
|
-
server.setup_response("/") do |req,res|
|
|
109
|
-
res.body = body
|
|
110
|
-
end
|
|
111
|
-
with_server(StubProxyServer.new) do |proxy|
|
|
112
|
-
with_connection(subject.new(server.base_uri, proxy: proxy.base_uri)) do |cxn|
|
|
113
|
-
read_body = cxn.read_all
|
|
114
|
-
expect(read_body).to eq("hi")
|
|
115
|
-
expect(proxy.request_count).to eq(1)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
it "throws error if proxy responds with error status" do
|
|
122
|
-
with_server do |server|
|
|
123
|
-
server.setup_response("/") do |req,res|
|
|
124
|
-
res.body = body
|
|
125
|
-
end
|
|
126
|
-
with_server(StubProxyServer.new) do |proxy|
|
|
127
|
-
proxy.connect_status = 403
|
|
128
|
-
expect { subject.new(server.base_uri, proxy: proxy.base_uri) }.to raise_error(SSE::Errors::HTTPProxyError)
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# The following 2 tests were originally written to connect to an embedded HTTPS server made with
|
|
134
|
-
# WEBrick. Unfortunately, some unknown problem prevents WEBrick's self-signed certificate feature
|
|
135
|
-
# from working in JRuby 9.1 (but not in any other Ruby version). Therefore these tests currently
|
|
136
|
-
# hit an external URL.
|
|
137
|
-
|
|
138
|
-
it "connects to HTTPS server" do
|
|
139
|
-
with_connection(subject.new(URI("https://app.launchdarkly.com"))) do |cxn|
|
|
140
|
-
expect(cxn.status).to eq 200
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
it "connects to HTTPS server through proxy" do
|
|
145
|
-
with_server(StubProxyServer.new) do |proxy|
|
|
146
|
-
with_connection(subject.new(URI("https://app.launchdarkly.com"), proxy: proxy.base_uri)) do |cxn|
|
|
147
|
-
expect(cxn.status).to eq 200
|
|
148
|
-
expect(proxy.request_count).to eq(1)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
#
|
|
155
|
-
# Tests of response parsing functionality without a real HTTP request
|
|
156
|
-
#
|
|
157
|
-
describe SSE::Impl::HTTPResponseReader do
|
|
158
|
-
subject { SSE::Impl::HTTPResponseReader }
|
|
159
|
-
|
|
160
|
-
let(:simple_response) { <<-EOT
|
|
161
|
-
HTTP/1.1 200 OK
|
|
162
|
-
Cache-Control: no-cache
|
|
163
|
-
Content-Type: text/event-stream
|
|
164
|
-
|
|
165
|
-
line1\r
|
|
166
|
-
line2
|
|
167
|
-
\r
|
|
168
|
-
EOT
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
def make_chunks(str)
|
|
172
|
-
# arbitrarily split content into 5-character blocks
|
|
173
|
-
str.scan(/.{1,5}/m).to_enum
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def mock_socket_without_timeout(chunks)
|
|
177
|
-
mock_socket(chunks) { :eof }
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def mock_socket_with_timeout(chunks)
|
|
181
|
-
mock_socket(chunks) { raise Socketry::TimeoutError }
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def mock_socket(chunks)
|
|
185
|
-
sock = double
|
|
186
|
-
allow(sock).to receive(:readpartial) do
|
|
187
|
-
begin
|
|
188
|
-
chunks.next
|
|
189
|
-
rescue StopIteration
|
|
190
|
-
yield
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
sock
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
it "parses status code" do
|
|
197
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
|
198
|
-
reader = subject.new(socket, 0)
|
|
199
|
-
expect(reader.status).to eq(200)
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
it "parses headers" do
|
|
203
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
|
204
|
-
reader = subject.new(socket, 0)
|
|
205
|
-
expect(reader.headers).to eq({
|
|
206
|
-
'cache-control' => 'no-cache',
|
|
207
|
-
'content-type' => 'text/event-stream'
|
|
208
|
-
})
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
it "can read entire response body" do
|
|
212
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
|
213
|
-
reader = subject.new(socket, 0)
|
|
214
|
-
expect(reader.read_all).to eq("line1\r\nline2\n\r\n")
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
it "can read response body as lines" do
|
|
218
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
|
219
|
-
reader = subject.new(socket, 0)
|
|
220
|
-
expect(reader.read_lines.to_a).to eq([
|
|
221
|
-
"line1\r\n",
|
|
222
|
-
"line2\n",
|
|
223
|
-
"\r\n"
|
|
224
|
-
])
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
it "handles chunked encoding" do
|
|
228
|
-
chunked_response = <<-EOT
|
|
229
|
-
HTTP/1.1 200 OK
|
|
230
|
-
Content-Type: text/plain
|
|
231
|
-
Transfer-Encoding: chunked
|
|
232
|
-
|
|
233
|
-
6\r
|
|
234
|
-
things\r
|
|
235
|
-
A\r
|
|
236
|
-
and stuff\r
|
|
237
|
-
0\r
|
|
238
|
-
\r
|
|
239
|
-
EOT
|
|
240
|
-
socket = mock_socket_without_timeout(make_chunks(chunked_response))
|
|
241
|
-
reader = subject.new(socket, 0)
|
|
242
|
-
expect(reader.read_all).to eq("things and stuff")
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
it "raises error if response ends without complete headers" do
|
|
246
|
-
malformed_response = <<-EOT
|
|
247
|
-
HTTP/1.1 200 OK
|
|
248
|
-
Cache-Control: no-cache
|
|
249
|
-
EOT
|
|
250
|
-
socket = mock_socket_without_timeout(make_chunks(malformed_response))
|
|
251
|
-
expect { subject.new(socket, 0) }.to raise_error(EOFError)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
it "throws timeout if thrown by socket read" do
|
|
255
|
-
socket = mock_socket_with_timeout(make_chunks(simple_response))
|
|
256
|
-
reader = subject.new(socket, 0)
|
|
257
|
-
lines = reader.read_lines
|
|
258
|
-
lines.next
|
|
259
|
-
lines.next
|
|
260
|
-
lines.next
|
|
261
|
-
expect { lines.next }.to raise_error(SSE::Errors::ReadTimeoutError)
|
|
262
|
-
end
|
|
263
|
-
end
|