ld-eventsource 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 294a32bc78de81b8f7c5ffa0a8b9ffc25847b0ecd20eee8f27caea576ae2b6fc
4
+ data.tar.gz: 5c0bb7736f090364098525477ec192b89ee02b96e0d12542bf72f08112180ba2
5
+ SHA512:
6
+ metadata.gz: b6466b0b4c060b681640e2b47cc1844bb93a9588076fbcc901a5922be303860538d6e064d1f9ea897062ffeafcb86690e68e26e0c28d6f795ea68bb41dc7d364
7
+ data.tar.gz: 67a62d448f673893e9fc397cb536ad6ba4194292e3f8c3058d96bcc39b0f8bf3ce6bf12b1bdeb8e843ff962e3324505b9cddc3e42486cdb915456dbe1dba4dcc
@@ -0,0 +1,90 @@
1
+ version: 2
2
+
3
+ workflows:
4
+ version: 2
5
+ test:
6
+ jobs:
7
+ - test-misc-rubies
8
+ - test-2.2
9
+ - test-2.3
10
+ - test-2.4
11
+ - test-2.5
12
+ - test-jruby-9.2
13
+
14
+ ruby-docker-template: &ruby-docker-template
15
+ steps:
16
+ - checkout
17
+ - run: |
18
+ if [[ $CIRCLE_JOB == test-jruby* ]]; then
19
+ gem install jruby-openssl; # required by bundler, no effect on Ruby MRI
20
+ fi
21
+ - run: ruby -v
22
+ - run: gem install bundler -v "~> 1.17"
23
+ - run: bundle install
24
+ - run: mkdir ./rspec
25
+ - run: bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
26
+ - store_test_results:
27
+ path: ./rspec
28
+ - store_artifacts:
29
+ path: ./rspec
30
+
31
+ jobs:
32
+ test-2.2:
33
+ <<: *ruby-docker-template
34
+ docker:
35
+ - image: circleci/ruby:2.2.10-jessie
36
+ test-2.3:
37
+ <<: *ruby-docker-template
38
+ docker:
39
+ - image: circleci/ruby:2.3.7-jessie
40
+ test-2.4:
41
+ <<: *ruby-docker-template
42
+ docker:
43
+ - image: circleci/ruby:2.4.5-stretch
44
+ test-2.5:
45
+ <<: *ruby-docker-template
46
+ docker:
47
+ - image: circleci/ruby:2.5.3-stretch
48
+ test-jruby-9.2:
49
+ <<: *ruby-docker-template
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
+ gem install bundler -v "~> 1.17";
77
+ bundle install;
78
+ mv Gemfile.lock "Gemfile.lock.$i"
79
+ done
80
+ - run:
81
+ name: run tests for all versions
82
+ shell: /bin/bash -leo pipefail
83
+ command: |
84
+ set -e;
85
+ for i in $RUBIES;
86
+ do
87
+ rvm use $i;
88
+ cp "Gemfile.lock.$i" Gemfile.lock;
89
+ bundle exec rspec spec;
90
+ done
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.bundle
10
+ *.so
11
+ *.o
12
+ *.a
13
+ mkmf.log
14
+ *.gem
15
+ .DS_Store
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Change log
2
+
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
+
5
+ ## [1.0.0] - 2019-01-03
6
+
7
+ Initial release.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,47 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ld-eventsource (1.0.0)
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.0.5)
13
+ diff-lcs (1.3)
14
+ hitimes (1.3.0)
15
+ http_tools (0.4.5)
16
+ rake (10.5.0)
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
+ rake (~> 10.0)
43
+ rspec (~> 3.2)
44
+ rspec_junit_formatter (~> 0.3.0)
45
+
46
+ BUNDLED WITH
47
+ 1.17.3
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2018 Catamorphic, Co.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ LaunchDarkly SSE Client for Ruby
2
+ ================================
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/ld-eventsource.svg)](http://badge.fury.io/rb/ld-eventsource) [![Circle CI](https://circleci.com/gh/launchdarkly/ruby-eventsource/tree/master.svg?style=svg)](https://circleci.com/gh/launchdarkly/ruby-eventsource/tree/master)
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 [`socketry`](https://rubygems.org/gems/socketry) 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
+
8
+ Parts of this code are based on https://github.com/Tonkpils/celluloid-eventsource, but it does not use Celluloid.
9
+
10
+ Supported Ruby versions
11
+ -----------------------
12
+
13
+ This gem has a minimum Ruby version of 2.2.6, or 9.1.6 for JRuby.
14
+
15
+ Quick setup
16
+ -----------
17
+
18
+ 1. Install the Ruby SDK with `gem`:
19
+
20
+ ```shell
21
+ gem install ld-eventsource
22
+ ```
23
+
24
+ 2. Import the code:
25
+
26
+ ```ruby
27
+ require 'ld-eventsource'
28
+ ```
29
+
30
+ 3. Create a new SSE client instance and register your event handler:
31
+
32
+ ```ruby
33
+ sse_client = SSE::Client.new("http://hostname/resource/path") do |client|
34
+ client.on_event do |event|
35
+ puts "I received an event: #{event.type}, #{event.data}"
36
+ end
37
+ end
38
+ ```
39
+
40
+ For other options available with the `Client` constructor, see the [API documentation](https://www.rubydoc.info/gems/ld-eventsource).
41
+
42
+ Contributing
43
+ ------------
44
+
45
+ We welcome questions, suggestions, and pull requests at our [Github repository](https://github.com/launchdarkly/ruby-eventsource). Pull requests should be done from a fork.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task default: :spec
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "ld-eventsource/version"
6
+
7
+ # rubocop:disable Metrics/BlockLength
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "ld-eventsource"
10
+ spec.version = SSE::VERSION
11
+ spec.authors = ["LaunchDarkly"]
12
+ spec.email = ["team@launchdarkly.com"]
13
+ spec.summary = "LaunchDarkly SSE client"
14
+ spec.description = "LaunchDarkly SSE client for Ruby"
15
+ spec.homepage = "https://github.com/launchdarkly/ruby-eventsource"
16
+ spec.license = "Apache-2.0"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rspec", "~> 3.2"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
27
+
28
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
29
+ spec.add_runtime_dependency "http_tools", '~> 0.4.5'
30
+ spec.add_runtime_dependency "socketry", "~> 0.5.1"
31
+ end
@@ -0,0 +1,14 @@
1
+ require "ld-eventsource/client"
2
+ require "ld-eventsource/version"
3
+
4
+ #
5
+ # A client for the Server-Sent Events protocol.
6
+ #
7
+ module SSE
8
+ #
9
+ # Internal components of the SSE implementation. All classes in this module should be
10
+ # considered unsupported and subject to change.
11
+ #
12
+ module Impl
13
+ end
14
+ end
@@ -0,0 +1,296 @@
1
+ require "ld-eventsource/impl/backoff"
2
+ require "ld-eventsource/impl/event_parser"
3
+ require "ld-eventsource/impl/streaming_http"
4
+ require "ld-eventsource/events"
5
+ require "ld-eventsource/errors"
6
+
7
+ require "concurrent/atomics"
8
+ require "logger"
9
+ require "thread"
10
+ require "uri"
11
+
12
+ module SSE
13
+ #
14
+ # A lightweight SSE client implementation. The client uses a worker thread to read from the
15
+ # streaming HTTP connection. Events are dispatched from the same worker thread.
16
+ #
17
+ # The client will attempt to recover from connection failures as follows:
18
+ #
19
+ # * The first time the connection is dropped, it will wait about one second (or whatever value is
20
+ # specified for `reconnect_time`) before attempting to reconnect. The actual delay has a
21
+ # pseudo-random jitter value added.
22
+ # * If the connection fails again within the time range specified by `reconnect_reset_interval`,
23
+ # it will exponentially increase the delay between attempts (and also apply a random jitter).
24
+ # However, if the connection stays up for at least that amount of time, the delay will be reset
25
+ # to the minimum.
26
+ # * Each time a new connection is made, the client will send a `Last-Event-Id` header so the server
27
+ # can pick up where it left off (if the server has been sending ID values for events).
28
+ #
29
+ # It is also possible to force the connection to be restarted if the server sends no data within an
30
+ # interval specified by `read_timeout`. Using a read timeout is advisable because otherwise it is
31
+ # possible in some circumstances for a connection failure to go undetected. To keep the connection
32
+ # from timing out if there are no events to send, the server could send a comment line (`":"`) at
33
+ # regular intervals as a heartbeat.
34
+ #
35
+ class Client
36
+ # The default value for `connect_timeout` in {#initialize}.
37
+ DEFAULT_CONNECT_TIMEOUT = 10
38
+
39
+ # The default value for `read_timeout` in {#initialize}.
40
+ DEFAULT_READ_TIMEOUT = 300
41
+
42
+ # The default value for `reconnect_time` in {#initialize}.
43
+ DEFAULT_RECONNECT_TIME = 1
44
+
45
+ # The maximum number of seconds that the client will wait before reconnecting.
46
+ MAX_RECONNECT_TIME = 30
47
+
48
+ # The default value for `reconnect_reset_interval` in {#initialize}.
49
+ DEFAULT_RECONNECT_RESET_INTERVAL = 60
50
+
51
+ #
52
+ # Creates a new SSE client.
53
+ #
54
+ # Once the client is created, it immediately attempts to open the SSE connection. You will
55
+ # normally want to register your event handler before this happens, so that no events are missed.
56
+ # To do this, provide a block after the constructor; the block will be executed before opening
57
+ # the connection.
58
+ #
59
+ # @example Specifying an event handler at initialization time
60
+ # client = SSE::Client.new(uri) do |c|
61
+ # c.on_event do |event|
62
+ # puts "I got an event: #{event.type}, #{event.data}"
63
+ # end
64
+ # end
65
+ #
66
+ # @param uri [String] the URI to connect to
67
+ # @param headers [Hash] ({}) custom headers to send with each HTTP request
68
+ # @param connect_timeout [Float] (DEFAULT_CONNECT_TIMEOUT) maximum time to wait for a
69
+ # connection, in seconds
70
+ # @param read_timeout [Float] (DEFAULT_READ_TIMEOUT) the connection will be dropped and
71
+ # restarted if this number of seconds elapse with no data; nil for no timeout
72
+ # @param reconnect_time [Float] (DEFAULT_RECONNECT_TIME) the initial delay before reconnecting
73
+ # after a failure, in seconds; this can increase as described in {Client}
74
+ # @param reconnect_reset_interval [Float] (DEFAULT_RECONNECT_RESET_INTERVAL) if a connection
75
+ # stays alive for at least this number of seconds, the reconnect interval will return to the
76
+ # initial value
77
+ # @param last_event_id [String] (nil) the initial value that the client should send in the
78
+ # `Last-Event-Id` header, if any
79
+ # @param proxy [String] (nil) optional URI of a proxy server to use (you can also specify a
80
+ # proxy with the `HTTP_PROXY` or `HTTPS_PROXY` environment variable)
81
+ # @param logger [Logger] a Logger instance for the client to use for diagnostic output;
82
+ # defaults to a logger with WARN level that goes to standard output
83
+ # @yieldparam [Client] client the new client instance, before opening the connection
84
+ #
85
+ def initialize(uri,
86
+ headers: {},
87
+ connect_timeout: DEFAULT_CONNECT_TIMEOUT,
88
+ read_timeout: DEFAULT_READ_TIMEOUT,
89
+ reconnect_time: DEFAULT_RECONNECT_TIME,
90
+ reconnect_reset_interval: DEFAULT_RECONNECT_RESET_INTERVAL,
91
+ last_event_id: nil,
92
+ proxy: nil,
93
+ logger: nil)
94
+ @uri = URI(uri)
95
+ @stopped = Concurrent::AtomicBoolean.new(false)
96
+
97
+ @headers = headers.clone
98
+ @connect_timeout = connect_timeout
99
+ @read_timeout = read_timeout
100
+ @logger = logger || default_logger
101
+
102
+ if proxy
103
+ @proxy = proxy
104
+ else
105
+ proxy_uri = @uri.find_proxy
106
+ if !proxy_uri.nil? && (proxy_uri.scheme == 'http' || proxy_uri.scheme == 'https')
107
+ @proxy = proxy_uri
108
+ end
109
+ end
110
+
111
+ @backoff = Impl::Backoff.new(reconnect_time || DEFAULT_RECONNECT_TIME, MAX_RECONNECT_TIME,
112
+ reconnect_reset_interval: reconnect_reset_interval)
113
+
114
+ @on = { event: ->(_) {}, error: ->(_) {} }
115
+ @last_id = last_event_id
116
+
117
+ yield self if block_given?
118
+
119
+ Thread.new do
120
+ run_stream
121
+ end
122
+ end
123
+
124
+ #
125
+ # Specifies a block or Proc to receive events from the stream. This will be called once for every
126
+ # valid event received, with a single parameter of type {StreamEvent}. It is called from the same
127
+ # worker thread that reads the stream, so no more events will be dispatched until it returns.
128
+ #
129
+ # Any exception that propagates out of the handler will cause the stream to disconnect and
130
+ # reconnect, on the assumption that data may have been lost and that restarting the stream will
131
+ # cause it to be resent.
132
+ #
133
+ # Any previously specified event handler will be replaced.
134
+ #
135
+ # @yieldparam event [StreamEvent]
136
+ #
137
+ def on_event(&action)
138
+ @on[:event] = action
139
+ end
140
+
141
+ #
142
+ # Specifies a block or Proc to receive connection errors. This will be called with a single
143
+ # parameter that is an instance of some exception class-- normally, either some I/O exception or
144
+ # one of the classes in {SSE::Errors}. It is called from the same worker thread that
145
+ # reads the stream, so no more events or errors will be dispatched until it returns.
146
+ #
147
+ # If the error handler decides that this type of error is not recoverable, it has the ability
148
+ # to prevent any further reconnect attempts by calling {Client#close} on the Client. For instance,
149
+ # you might want to do this if the server returned a `401 Unauthorized` error and no other authorization
150
+ # credentials are available, since any further requests would presumably also receive a 401.
151
+ #
152
+ # Any previously specified error handler will be replaced.
153
+ #
154
+ # @yieldparam error [StandardError]
155
+ #
156
+ def on_error(&action)
157
+ @on[:error] = action
158
+ end
159
+
160
+ #
161
+ # Permanently shuts down the client and its connection. No further events will be dispatched. This
162
+ # has no effect if called a second time.
163
+ #
164
+ def close
165
+ if @stopped.make_true
166
+ @cxn.close if !@cxn.nil?
167
+ @cxn = nil
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def default_logger
174
+ log = ::Logger.new($stdout)
175
+ log.level = ::Logger::WARN
176
+ log.progname = 'ld-eventsource'
177
+ log
178
+ end
179
+
180
+ def run_stream
181
+ while !@stopped.value
182
+ @cxn = nil
183
+ begin
184
+ @cxn = connect
185
+ # There's a potential race if close was called in the middle of the previous line, i.e. after we
186
+ # connected but before @cxn was set. Checking the variable again is a bit clunky but avoids that.
187
+ return if @stopped.value
188
+ read_stream(@cxn) if !@cxn.nil?
189
+ rescue Errno::EBADF
190
+ # Don't log this as an error - it probably means we closed our own connection deliberately
191
+ @logger.info { "Stream connection closed" }
192
+ rescue StandardError => e
193
+ # This should not be possible because connect catches all StandardErrors
194
+ log_and_dispatch_error(e, "Unexpected error from event source")
195
+ end
196
+ begin
197
+ @cxn.close if !@cxn.nil?
198
+ rescue StandardError => e
199
+ log_and_dispatch_error(e, "Unexpected error while closing stream")
200
+ end
201
+ end
202
+ end
203
+
204
+ # Try to establish a streaming connection. Returns the StreamingHTTPConnection object if successful.
205
+ def connect
206
+ loop do
207
+ return if @stopped.value
208
+ interval = @backoff.next_interval
209
+ if interval > 0
210
+ @logger.info { "Will retry connection after #{'%.3f' % interval} seconds" }
211
+ sleep(interval)
212
+ end
213
+ begin
214
+ @logger.info { "Connecting to event stream at #{@uri}" }
215
+ cxn = Impl::StreamingHTTPConnection.new(@uri,
216
+ proxy: @proxy,
217
+ headers: build_headers,
218
+ connect_timeout: @connect_timeout,
219
+ read_timeout: @read_timeout
220
+ )
221
+ if cxn.status == 200
222
+ content_type = cxn.headers["content-type"]
223
+ if content_type && content_type.start_with?("text/event-stream")
224
+ return cxn # we're good to proceed
225
+ else
226
+ cxn.close
227
+ err = Errors::HTTPContentTypeError.new(cxn.headers["content-type"])
228
+ @on[:error].call(err)
229
+ @logger.warn { "Event source returned unexpected content type '#{cxn.headers["content-type"]}'" }
230
+ end
231
+ else
232
+ body = cxn.read_all # grab the whole response body in case it has error details
233
+ cxn.close
234
+ @logger.info { "Server returned error status #{cxn.status}" }
235
+ err = Errors::HTTPStatusError.new(cxn.status, body)
236
+ @on[:error].call(err)
237
+ end
238
+ rescue Errno::EBADF
239
+ raise # See EBADF comment in run_stream
240
+ rescue StandardError => e
241
+ cxn.close if !cxn.nil?
242
+ log_and_dispatch_error(e, "Unexpected error from event source")
243
+ end
244
+ # if unsuccessful, continue the loop to connect again
245
+ end
246
+ end
247
+
248
+ # Pipe the output of the StreamingHTTPConnection into the EventParser, and dispatch events as
249
+ # they arrive.
250
+ def read_stream(cxn)
251
+ # Tell the Backoff object that the connection is now in a valid state. It uses that information so
252
+ # it can automatically reset itself if enough time passes between failures.
253
+ @backoff.mark_success
254
+
255
+ event_parser = Impl::EventParser.new(cxn.read_lines)
256
+ event_parser.items.each do |item|
257
+ return if @stopped.value
258
+ case item
259
+ when StreamEvent
260
+ dispatch_event(item)
261
+ when Impl::SetRetryInterval
262
+ @logger.debug { "Received 'retry:' directive, setting interval to #{item.milliseconds}ms" }
263
+ @backoff.base_interval = item.milliseconds.to_f / 1000
264
+ end
265
+ end
266
+ end
267
+
268
+ def dispatch_event(event)
269
+ @logger.debug { "Received event: #{event}" }
270
+ @last_id = event.id
271
+
272
+ # Pass the event to the caller
273
+ @on[:event].call(event)
274
+ end
275
+
276
+ def log_and_dispatch_error(e, message)
277
+ @logger.warn { "#{message}: #{e.inspect}"}
278
+ @logger.debug { "Exception trace: #{e.backtrace}" }
279
+ begin
280
+ @on[:error].call(e)
281
+ rescue StandardError => ee
282
+ @logger.warn { "Error handler threw an exception: #{ee.inspect}"}
283
+ @logger.debug { "Exception trace: #{ee.backtrace}" }
284
+ end
285
+ end
286
+
287
+ def build_headers
288
+ h = {
289
+ 'Accept' => 'text/event-stream',
290
+ 'Cache-Control' => 'no-cache'
291
+ }
292
+ h['Last-Event-Id'] = @last_id if !@last_id.nil?
293
+ h.merge(@headers)
294
+ end
295
+ end
296
+ end