getstream-ruby 6.1.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -6
- data/lib/getstream_ruby/client.rb +145 -38
- data/lib/getstream_ruby/configuration.rb +110 -30
- data/lib/getstream_ruby/error_mapping.rb +144 -0
- data/lib/getstream_ruby/errors.rb +107 -3
- data/lib/getstream_ruby/generated/common_client.rb +26 -0
- data/lib/getstream_ruby/generated/feed.rb +3 -1
- data/lib/getstream_ruby/generated/feeds_client.rb +3 -1
- data/lib/getstream_ruby/generated/models/async_export_error_event.rb +1 -1
- data/lib/getstream_ruby/generated/models/delete_feeds_batch_request.rb +6 -1
- data/lib/getstream_ruby/generated/models/labels_request.rb +5 -0
- data/lib/getstream_ruby/generated/models/query_bookmarks_request.rb +11 -1
- data/lib/getstream_ruby/generated/models/search_roles_response.rb +36 -0
- data/lib/getstream_ruby/generated/webhook.rb +401 -170
- data/lib/getstream_ruby/version.rb +1 -1
- metadata +48 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 388126d2a284a9b97ac8eb6cd1793088ccfc51992504de016192d7974d582e3e
|
|
4
|
+
data.tar.gz: 3fb211b156d236f9b27734651215351d9a3a370d6d6a1da52bfe5fa779cdf7b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ebae01df899c155f88efa4296b92766df5cc08f9dbd6b81188de15ddccc791eb7a5d4c1f8767c8e20c95601c109fb07de9e586bcb028786f8f6ce4904ad76352
|
|
7
|
+
data.tar.gz: 487609c26921506f9b5ea2cccedec98911adcd831189b785ede0963d75df993fa4ff768745574c37ac4d2f10764677f104c8ff403b1bef87f2ea867874abdd63
|
data/README.md
CHANGED
|
@@ -350,18 +350,20 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/getstr
|
|
|
350
350
|
|
|
351
351
|
## Release Process
|
|
352
352
|
|
|
353
|
-
Releases use two paths
|
|
353
|
+
Releases use two paths, both handled by `.github/workflows/release.yml`:
|
|
354
354
|
|
|
355
|
-
- Default
|
|
356
|
-
- Fallback
|
|
355
|
+
- **Default**: automatic release when a PR is merged to `main`/`master`. The PR title (and body) drives the semver bump.
|
|
356
|
+
- **Fallback**: manual release via the `Release` workflow's `workflow_dispatch` (admin use). Select a `version_bump` (`patch`/`minor`/`major`). `use_current_version=true` skips the bump and publishes whatever is already in `lib/getstream_ruby/version.rb`.
|
|
357
357
|
|
|
358
|
-
Automatic semver bump rules
|
|
358
|
+
Automatic semver bump rules:
|
|
359
359
|
|
|
360
360
|
- `feat:` -> minor
|
|
361
361
|
- `fix:` (or `bug:`) -> patch
|
|
362
|
-
- `feat
|
|
362
|
+
- `feat!:`, `<type>(scope)!:`, or `BREAKING CHANGE` in the PR body/title -> major
|
|
363
363
|
|
|
364
|
-
PRs with other
|
|
364
|
+
PRs with any other prefix do not trigger a release.
|
|
365
|
+
|
|
366
|
+
The release pipeline runs lint (`make format-check && make lint && make security`), the unit suite (`make test`), and all three integration suites (chat, feed, video) on the merged commit before publishing to RubyGems. Each step is idempotent; a failed run can be re-dispatched from the Actions UI.
|
|
365
367
|
|
|
366
368
|
## License
|
|
367
369
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'faraday'
|
|
4
|
+
require 'faraday/gzip'
|
|
4
5
|
require 'faraday/retry'
|
|
5
6
|
require 'faraday/multipart'
|
|
7
|
+
require 'faraday/net_http_persistent'
|
|
8
|
+
require 'logger'
|
|
6
9
|
require 'json'
|
|
7
10
|
require 'jwt'
|
|
8
11
|
require_relative 'generated/base_model'
|
|
@@ -13,7 +16,10 @@ require_relative 'generated/chat_client'
|
|
|
13
16
|
require_relative 'generated/video_client'
|
|
14
17
|
require_relative 'extensions/moderation_extensions'
|
|
15
18
|
require_relative 'generated/feed'
|
|
19
|
+
require_relative 'generated/webhook'
|
|
20
|
+
require_relative 'generated/models/api_error'
|
|
16
21
|
require_relative 'stream_response'
|
|
22
|
+
require_relative 'error_mapping'
|
|
17
23
|
|
|
18
24
|
module GetStreamRuby
|
|
19
25
|
|
|
@@ -35,6 +41,7 @@ module GetStreamRuby
|
|
|
35
41
|
|
|
36
42
|
@configuration.validate!
|
|
37
43
|
@connection = build_connection
|
|
44
|
+
@configuration.log_pool_config_to(@configuration.logger)
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
def feed_resource
|
|
@@ -76,6 +83,52 @@ module GetStreamRuby
|
|
|
76
83
|
GetStream::Generated::Feed.new(self, feed_group_id, feed_id)
|
|
77
84
|
end
|
|
78
85
|
|
|
86
|
+
# Verify a webhook signature using this client's API secret (CHA-2961).
|
|
87
|
+
#
|
|
88
|
+
# Convenience wrapper around StreamChat::Webhook.verify_signature that
|
|
89
|
+
# supplies the secret automatically. The module-level method is still
|
|
90
|
+
# available for callers that need to verify with an arbitrary secret.
|
|
91
|
+
#
|
|
92
|
+
# @param body [String] The raw request body (already-decompressed)
|
|
93
|
+
# @param signature [String] The signature from the X-Signature header
|
|
94
|
+
# @return [Boolean] true if the signature is valid, false otherwise
|
|
95
|
+
def verify_signature(body, signature)
|
|
96
|
+
StreamChat::Webhook.verify_signature(body, signature, @configuration.api_secret)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Verify and parse a webhook payload in one call, using this client's API
|
|
100
|
+
# secret (CHA-2961).
|
|
101
|
+
#
|
|
102
|
+
# Handles gzip-compressed bodies transparently. Raises
|
|
103
|
+
# StreamChat::Webhook::InvalidWebhookError on signature mismatch or parse
|
|
104
|
+
# failures; distinguish failure modes via the message substring.
|
|
105
|
+
#
|
|
106
|
+
# @param body [String] raw request body (possibly gzip-compressed)
|
|
107
|
+
# @param signature [String] X-Signature header value
|
|
108
|
+
# @return [Object] the typed event class instance or
|
|
109
|
+
# StreamChat::Webhook::UnknownEvent
|
|
110
|
+
# @raise [StreamChat::Webhook::InvalidWebhookError]
|
|
111
|
+
def verify_and_parse_webhook(body, signature)
|
|
112
|
+
StreamChat::Webhook.verify_and_parse_webhook(body, signature, @configuration.api_secret)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Decode + parse a Stream-delivered SQS message body.
|
|
116
|
+
#
|
|
117
|
+
# Convenience wrapper around StreamChat::Webhook.parse_sqs. No signature is
|
|
118
|
+
# required; SQS deliveries are authenticated via AWS IAM.
|
|
119
|
+
def parse_sqs(message_body)
|
|
120
|
+
StreamChat::Webhook.parse_sqs(message_body)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Decode + parse a Stream-delivered SNS notification body.
|
|
124
|
+
#
|
|
125
|
+
# Accepts either the raw SNS HTTP envelope JSON or the pre-extracted Message
|
|
126
|
+
# string. Convenience wrapper around StreamChat::Webhook.parse_sns. No signature
|
|
127
|
+
# is required; SNS deliveries are authenticated via AWS IAM.
|
|
128
|
+
def parse_sns(notification_body)
|
|
129
|
+
StreamChat::Webhook.parse_sns(notification_body)
|
|
130
|
+
end
|
|
131
|
+
|
|
79
132
|
# @param path [String] The API path
|
|
80
133
|
# @param body [Hash] The request body
|
|
81
134
|
# @return [GetStreamRuby::StreamResponse] The API response
|
|
@@ -83,7 +136,50 @@ module GetStreamRuby
|
|
|
83
136
|
request(:post, path, body)
|
|
84
137
|
end
|
|
85
138
|
|
|
86
|
-
|
|
139
|
+
# Polls the task-status endpoint until the task reaches a terminal state.
|
|
140
|
+
#
|
|
141
|
+
# Behaviour:
|
|
142
|
+
# - status="completed": returns the task `result` payload.
|
|
143
|
+
# - status="failed": raises `TaskError` populated from the task's
|
|
144
|
+
# `ErrorResult` (`type`, `description`, `stacktrace`,
|
|
145
|
+
# `version`).
|
|
146
|
+
# - timeout elapsed: raises `TransportError` with `error_type:
|
|
147
|
+
# "timeout"`.
|
|
148
|
+
#
|
|
149
|
+
# @param task_id [String]
|
|
150
|
+
# @param poll_interval [Numeric] seconds between polls (default 1)
|
|
151
|
+
# @param timeout [Numeric] max seconds to wait (default 60)
|
|
152
|
+
# @return [Object] the task `result` payload on success
|
|
153
|
+
# @raise [TaskError] when the task reports `status="failed"`
|
|
154
|
+
# @raise [TransportError] when the timeout elapses (`error_type="timeout"`)
|
|
155
|
+
def wait_for_task(task_id, poll_interval: 1, timeout: 60)
|
|
156
|
+
start_time = monotonic_now
|
|
157
|
+
|
|
158
|
+
loop do
|
|
159
|
+
|
|
160
|
+
response = common.get_task(task_id)
|
|
161
|
+
status = response.status
|
|
162
|
+
|
|
163
|
+
case status
|
|
164
|
+
when 'completed'
|
|
165
|
+
return response.result
|
|
166
|
+
when 'failed'
|
|
167
|
+
raise ErrorMapping.build_task_error(task_id, response.error)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if monotonic_now - start_time >= timeout
|
|
171
|
+
raise TransportError.new(
|
|
172
|
+
"wait_for_task timed out after #{timeout}s for task_id=#{task_id}",
|
|
173
|
+
error_type: 'timeout',
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
sleep(poll_interval)
|
|
178
|
+
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def make_request(method, path, query_params: nil, body: nil, request_timeout: nil)
|
|
87
183
|
# Handle query parameters
|
|
88
184
|
if query_params && !query_params.empty?
|
|
89
185
|
query_string = query_params.map { |k, v| "#{k}=#{v}" }.join('&')
|
|
@@ -91,12 +187,12 @@ module GetStreamRuby
|
|
|
91
187
|
end
|
|
92
188
|
|
|
93
189
|
# Make the request
|
|
94
|
-
request(method, path, body)
|
|
190
|
+
request(method, path, body, request_timeout: request_timeout)
|
|
95
191
|
end
|
|
96
192
|
|
|
97
193
|
private
|
|
98
194
|
|
|
99
|
-
def request(method, path, data = {})
|
|
195
|
+
def request(method, path, data = {}, request_timeout: nil)
|
|
100
196
|
# Add API key to query parameters
|
|
101
197
|
query_params = { api_key: @configuration.api_key }
|
|
102
198
|
|
|
@@ -111,15 +207,20 @@ module GetStreamRuby
|
|
|
111
207
|
req.headers['stream-auth-type'] = 'jwt'
|
|
112
208
|
req.headers['X-Stream-Client'] = user_agent
|
|
113
209
|
req.body = data.to_json
|
|
210
|
+
req.options.timeout = request_timeout if request_timeout
|
|
114
211
|
|
|
115
212
|
end
|
|
116
213
|
|
|
117
214
|
handle_response(response)
|
|
118
215
|
rescue Faraday::Error => e
|
|
119
|
-
raise
|
|
216
|
+
raise TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
120
217
|
end
|
|
121
218
|
|
|
122
219
|
def build_connection
|
|
220
|
+
# Escape hatch #1: user supplied a fully-built Faraday::Connection.
|
|
221
|
+
# Use it as-is; none of the 5 knobs apply.
|
|
222
|
+
return @configuration.http_client if @configuration.http_client
|
|
223
|
+
|
|
123
224
|
Faraday.new(url: @configuration.base_url) do |conn|
|
|
124
225
|
|
|
125
226
|
conn.request :multipart
|
|
@@ -130,22 +231,44 @@ module GetStreamRuby
|
|
|
130
231
|
backoff_factor: 2,
|
|
131
232
|
}
|
|
132
233
|
conn.response :json, content_type: /\bjson$/
|
|
133
|
-
|
|
234
|
+
# :gzip must come after :json (Faraday runs response middleware in reverse).
|
|
235
|
+
conn.request :gzip
|
|
134
236
|
configure_adapter(conn)
|
|
135
|
-
|
|
237
|
+
|
|
238
|
+
conn.options.timeout = @configuration.request_timeout
|
|
239
|
+
conn.options.open_timeout = @configuration.connect_timeout
|
|
136
240
|
|
|
137
241
|
end
|
|
138
242
|
end
|
|
139
243
|
|
|
140
244
|
def configure_adapter(connection)
|
|
141
|
-
adapter
|
|
142
|
-
adapter_options
|
|
143
|
-
|
|
245
|
+
# Escape hatch #2: custom adapter symbol. Use it with the user's
|
|
246
|
+
# adapter_options; do NOT apply pool_size/idle_timeout (those are
|
|
247
|
+
# net_http_persistent-specific).
|
|
248
|
+
if @configuration.faraday_adapter
|
|
249
|
+
adapter = @configuration.faraday_adapter
|
|
250
|
+
adapter_options = @configuration.faraday_adapter_options || {}
|
|
251
|
+
# Header-based keep-alive only on the custom-adapter path.
|
|
252
|
+
# net_http_persistent (default) keeps connections alive natively without any HTTP header.
|
|
253
|
+
connection.headers['Connection'] = 'keep-alive' if @configuration.connection_keep_alive
|
|
254
|
+
connection.adapter(adapter, **adapter_options)
|
|
255
|
+
return
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Default: net_http_persistent with the 5-knob config.
|
|
259
|
+
# Never set Connection: close; net_http_persistent keeps connections alive natively.
|
|
260
|
+
idle = @configuration.idle_timeout
|
|
261
|
+
connection.adapter :net_http_persistent, pool_size: @configuration.max_conns_per_host do |http|
|
|
262
|
+
|
|
263
|
+
http.idle_timeout = idle
|
|
264
|
+
|
|
265
|
+
end
|
|
144
266
|
rescue Faraday::Error, ArgumentError => e
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
267
|
+
# A fallback silently disables pooling, so always WARN (never swallow).
|
|
268
|
+
@configuration.warn_pool_fallback(Faraday.default_adapter, e)
|
|
148
269
|
connection.adapter Faraday.default_adapter
|
|
270
|
+
# Record the adapter actually built so the INFO log reports it accurately.
|
|
271
|
+
@configuration.effective_adapter = Faraday.default_adapter.to_s
|
|
149
272
|
end
|
|
150
273
|
|
|
151
274
|
def generate_auth_header
|
|
@@ -164,29 +287,13 @@ module GetStreamRuby
|
|
|
164
287
|
end
|
|
165
288
|
|
|
166
289
|
def handle_response(response)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
JSON.parse(response.body)
|
|
175
|
-
rescue JSON::ParserError
|
|
176
|
-
response.body
|
|
177
|
-
end
|
|
178
|
-
else
|
|
179
|
-
response.body
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
error_message = if parsed_body.is_a?(Hash)
|
|
183
|
-
parsed_body['message'] || parsed_body['detail'] ||
|
|
184
|
-
"Request failed with status #{response.status}"
|
|
185
|
-
else
|
|
186
|
-
"Request failed with status #{response.status}"
|
|
187
|
-
end
|
|
188
|
-
raise APIError, error_message
|
|
189
|
-
end
|
|
290
|
+
return StreamResponse.new(response.body) if (200..299).cover?(response.status)
|
|
291
|
+
|
|
292
|
+
ErrorMapping.raise_api_error(response)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def monotonic_now
|
|
296
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
190
297
|
end
|
|
191
298
|
|
|
192
299
|
def multipart_request?(data)
|
|
@@ -202,10 +309,10 @@ module GetStreamRuby
|
|
|
202
309
|
payload = {}
|
|
203
310
|
|
|
204
311
|
# Handle file field
|
|
205
|
-
raise
|
|
312
|
+
raise ArgumentError, 'file name must be provided' if data.file.nil? || data.file.empty?
|
|
206
313
|
|
|
207
314
|
file_path = data.file
|
|
208
|
-
raise
|
|
315
|
+
raise ArgumentError, "file not found: #{file_path}" unless File.exist?(file_path)
|
|
209
316
|
|
|
210
317
|
# Determine content type
|
|
211
318
|
content_type = detect_content_type(file_path)
|
|
@@ -241,7 +348,7 @@ module GetStreamRuby
|
|
|
241
348
|
|
|
242
349
|
handle_response(response)
|
|
243
350
|
rescue Faraday::Error => e
|
|
244
|
-
raise
|
|
351
|
+
raise TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
245
352
|
end
|
|
246
353
|
|
|
247
354
|
def detect_content_type(file_path)
|
|
@@ -1,45 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'logger'
|
|
4
|
+
|
|
3
5
|
module GetStreamRuby
|
|
4
6
|
|
|
5
7
|
class Configuration
|
|
6
8
|
|
|
7
9
|
attr_accessor :api_key, :api_secret, :base_url, :timeout, :logger, :faraday_adapter, :faraday_adapter_options,
|
|
8
|
-
:connection_keep_alive
|
|
10
|
+
:connection_keep_alive, :max_conns_per_host, :idle_timeout, :connect_timeout,
|
|
11
|
+
:request_timeout, :http_client, :effective_adapter
|
|
9
12
|
|
|
10
13
|
def initialize(api_key: nil, api_secret: nil, use_env: true, **options)
|
|
11
|
-
base_url = options[:base_url]
|
|
12
|
-
timeout = options[:timeout]
|
|
13
14
|
http_options = options[:http_options] || {}
|
|
14
|
-
faraday_adapter = options[:faraday_adapter] || http_options[:faraday_adapter]
|
|
15
|
-
faraday_adapter_options = options[:faraday_adapter_options] || http_options[:faraday_adapter_options]
|
|
16
|
-
connection_keep_alive = if options.key?(:connection_keep_alive)
|
|
17
|
-
options[:connection_keep_alive]
|
|
18
|
-
else
|
|
19
|
-
http_options[:connection_keep_alive]
|
|
20
|
-
end
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@timeout = timeout || (ENV['STREAM_TIMEOUT'] || 30).to_i
|
|
27
|
-
else
|
|
28
|
-
# Manual configuration only - no environment variables
|
|
29
|
-
@api_key = api_key
|
|
30
|
-
@api_secret = api_secret
|
|
31
|
-
@base_url = base_url || 'https://chat.stream-io-api.com'
|
|
32
|
-
@timeout = timeout || 30
|
|
33
|
-
end
|
|
16
|
+
assign_credentials_and_url(api_key, api_secret, options[:base_url], use_env: use_env)
|
|
17
|
+
assign_timeouts_and_pool(options, use_env: use_env)
|
|
18
|
+
assign_adapter(options, http_options)
|
|
19
|
+
assign_keep_alive(options, http_options)
|
|
34
20
|
|
|
35
|
-
@
|
|
36
|
-
@
|
|
37
|
-
@
|
|
38
|
-
|
|
39
|
-
else
|
|
40
|
-
connection_keep_alive
|
|
41
|
-
end
|
|
42
|
-
@logger = nil
|
|
21
|
+
# Keep @timeout in sync with @request_timeout for backwards compatibility.
|
|
22
|
+
@timeout = @request_timeout
|
|
23
|
+
@http_client = options[:http_client]
|
|
24
|
+
@logger = options[:logger]
|
|
43
25
|
end
|
|
44
26
|
|
|
45
27
|
def valid?
|
|
@@ -57,9 +39,54 @@ module GetStreamRuby
|
|
|
57
39
|
api_secret: @api_secret,
|
|
58
40
|
base_url: @base_url,
|
|
59
41
|
timeout: @timeout,
|
|
42
|
+
request_timeout: @request_timeout,
|
|
43
|
+
max_conns_per_host: @max_conns_per_host,
|
|
44
|
+
idle_timeout: @idle_timeout,
|
|
45
|
+
connect_timeout: @connect_timeout,
|
|
46
|
+
http_client: @http_client,
|
|
60
47
|
faraday_adapter: @faraday_adapter,
|
|
61
48
|
faraday_adapter_options: @faraday_adapter_options.dup,
|
|
62
49
|
connection_keep_alive: @connection_keep_alive,
|
|
50
|
+
logger: @logger,
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Emit a single INFO line listing the 5 effective pool knobs plus the active escape hatch (CHA-2956).
|
|
55
|
+
# If no logger is supplied, a default $stdout INFO logger is used.
|
|
56
|
+
# The faraday_adapter label reflects the adapter actually built
|
|
57
|
+
# (effective_adapter, set by Client#configure_adapter) so a silent fallback
|
|
58
|
+
# to the default adapter is never misreported as the requested adapter.
|
|
59
|
+
def log_pool_config_to(logger)
|
|
60
|
+
logger ||= Logger.new($stdout).tap { |l| l.level = Logger::INFO }
|
|
61
|
+
flag = @http_client ? 'user_http_client=true' : 'user_http_client=false'
|
|
62
|
+
adapter_label = if @http_client
|
|
63
|
+
'user-supplied'
|
|
64
|
+
elsif @effective_adapter
|
|
65
|
+
@effective_adapter
|
|
66
|
+
elsif @faraday_adapter
|
|
67
|
+
@faraday_adapter.to_s
|
|
68
|
+
else
|
|
69
|
+
'default'
|
|
70
|
+
end
|
|
71
|
+
fmt = 'connection pool: max_conns_per_host=%<m>d idle_timeout=%<i>d ' \
|
|
72
|
+
'connect_timeout=%<c>d request_timeout=%<r>d %<flag>s faraday_adapter=%<a>s'
|
|
73
|
+
logger.info(
|
|
74
|
+
format(
|
|
75
|
+
fmt,
|
|
76
|
+
m: @max_conns_per_host, i: @idle_timeout, c: @connect_timeout,
|
|
77
|
+
r: @request_timeout, flag: flag, a: adapter_label
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Emit a WARNING that the requested adapter could not be built and pooling
|
|
83
|
+
# is disabled (CHA-2956). A fallback must never be silent, so when no logger
|
|
84
|
+
# is configured this uses a default $stdout logger, exactly like
|
|
85
|
+
# log_pool_config_to.
|
|
86
|
+
def warn_pool_fallback(fallback_adapter, error)
|
|
87
|
+
warn_logger = @logger || Logger.new($stdout).tap { |l| l.level = Logger::WARN }
|
|
88
|
+
warn_logger.warn(
|
|
89
|
+
"Falling back to #{fallback_adapter}: could not configure net_http_persistent (#{error.message})",
|
|
63
90
|
)
|
|
64
91
|
end
|
|
65
92
|
|
|
@@ -90,6 +117,59 @@ module GetStreamRuby
|
|
|
90
117
|
{}
|
|
91
118
|
end
|
|
92
119
|
|
|
120
|
+
def assign_credentials_and_url(api_key, api_secret, base_url, use_env:)
|
|
121
|
+
if use_env
|
|
122
|
+
@api_key = api_key || ENV.fetch('STREAM_API_KEY', nil)
|
|
123
|
+
@api_secret = api_secret || ENV.fetch('STREAM_API_SECRET', nil)
|
|
124
|
+
@base_url = base_url || ENV.fetch('STREAM_BASE_URL', nil) || 'https://chat.stream-io-api.com'
|
|
125
|
+
else
|
|
126
|
+
@api_key = api_key
|
|
127
|
+
@api_secret = api_secret
|
|
128
|
+
@base_url = base_url || 'https://chat.stream-io-api.com'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def assign_timeouts_and_pool(options, use_env:)
|
|
133
|
+
timeout = options[:timeout]
|
|
134
|
+
request_timeout = options[:request_timeout]
|
|
135
|
+
max_conns_per_host = options[:max_conns_per_host]
|
|
136
|
+
idle_timeout = options[:idle_timeout]
|
|
137
|
+
connect_timeout = options[:connect_timeout]
|
|
138
|
+
|
|
139
|
+
if use_env
|
|
140
|
+
env_request_timeout = ENV.fetch('STREAM_REQUEST_TIMEOUT', nil) || ENV.fetch('STREAM_TIMEOUT', nil)
|
|
141
|
+
@request_timeout = (request_timeout || timeout || env_request_timeout || 30).to_i
|
|
142
|
+
@max_conns_per_host = (max_conns_per_host || ENV.fetch('STREAM_MAX_CONNS_PER_HOST', nil) || 5).to_i
|
|
143
|
+
@idle_timeout = (idle_timeout || ENV.fetch('STREAM_IDLE_TIMEOUT', nil) || 55).to_i
|
|
144
|
+
@connect_timeout = (connect_timeout || ENV.fetch('STREAM_CONNECT_TIMEOUT', nil) || 10).to_i
|
|
145
|
+
else
|
|
146
|
+
@request_timeout = (request_timeout || timeout || 30).to_i
|
|
147
|
+
@max_conns_per_host = (max_conns_per_host || 5).to_i
|
|
148
|
+
@idle_timeout = (idle_timeout || 55).to_i
|
|
149
|
+
@connect_timeout = (connect_timeout || 10).to_i
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def assign_adapter(options, http_options)
|
|
154
|
+
faraday_adapter = options[:faraday_adapter] || http_options[:faraday_adapter]
|
|
155
|
+
faraday_adapter_options = options[:faraday_adapter_options] || http_options[:faraday_adapter_options]
|
|
156
|
+
@faraday_adapter = (faraday_adapter || ENV.fetch('STREAM_FARADAY_ADAPTER', nil))&.to_sym
|
|
157
|
+
@faraday_adapter_options = faraday_adapter_options || default_adapter_options
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def assign_keep_alive(options, http_options)
|
|
161
|
+
connection_keep_alive = if options.key?(:connection_keep_alive)
|
|
162
|
+
options[:connection_keep_alive]
|
|
163
|
+
else
|
|
164
|
+
http_options[:connection_keep_alive]
|
|
165
|
+
end
|
|
166
|
+
@connection_keep_alive = if connection_keep_alive.nil?
|
|
167
|
+
ENV.fetch('STREAM_CONNECTION_KEEP_ALIVE', 'true') == 'true'
|
|
168
|
+
else
|
|
169
|
+
connection_keep_alive
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
93
173
|
end
|
|
94
174
|
|
|
95
175
|
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module GetStreamRuby
|
|
7
|
+
|
|
8
|
+
# Translates HTTP responses and Faraday errors into SDK exceptions.
|
|
9
|
+
module ErrorMapping
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Raises the appropriate `ApiError` / `RateLimitError` for a non-2xx
|
|
14
|
+
# `Faraday::Response`.
|
|
15
|
+
def raise_api_error(response)
|
|
16
|
+
parsed_body = parse_error_body(response.body)
|
|
17
|
+
raw_body = stringify_body(response.body)
|
|
18
|
+
|
|
19
|
+
if parsed_body.is_a?(Hash)
|
|
20
|
+
api_error = GetStream::Generated::Models::APIError.new(parsed_body)
|
|
21
|
+
attrs = api_error_attrs(api_error, response.status, raw_body)
|
|
22
|
+
|
|
23
|
+
if response.status == 429
|
|
24
|
+
raise RateLimitError.new(
|
|
25
|
+
retry_after: parse_retry_after(response_header(response, 'Retry-After')),
|
|
26
|
+
**attrs,
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
raise ApiError.new(**attrs)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
raise ApiError.new(
|
|
34
|
+
message: 'failed to parse error response',
|
|
35
|
+
status_code: response.status,
|
|
36
|
+
code: 0,
|
|
37
|
+
exception_fields: {},
|
|
38
|
+
unrecoverable: false,
|
|
39
|
+
raw_response_body: raw_body,
|
|
40
|
+
more_info: nil,
|
|
41
|
+
details: nil,
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def api_error_attrs(model, status, raw_body)
|
|
46
|
+
{
|
|
47
|
+
message: model.message || "Request failed with status #{status}",
|
|
48
|
+
status_code: status,
|
|
49
|
+
code: model.code || 0,
|
|
50
|
+
exception_fields: model.exception_fields || {},
|
|
51
|
+
unrecoverable: model.unrecoverable.nil? ? false : model.unrecoverable,
|
|
52
|
+
raw_response_body: raw_body,
|
|
53
|
+
more_info: model.more_info,
|
|
54
|
+
details: model.details,
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def parse_error_body(body)
|
|
59
|
+
return body if body.is_a?(Hash)
|
|
60
|
+
return nil unless body.is_a?(String) && !body.empty?
|
|
61
|
+
|
|
62
|
+
JSON.parse(body)
|
|
63
|
+
rescue JSON::ParserError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def stringify_body(body)
|
|
68
|
+
return '' if body.nil?
|
|
69
|
+
return body if body.is_a?(String)
|
|
70
|
+
|
|
71
|
+
body.to_json
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def response_header(response, name)
|
|
75
|
+
headers = response.headers
|
|
76
|
+
return nil if headers.nil?
|
|
77
|
+
|
|
78
|
+
# Faraday normalizes header names to lowercase, but tolerate either form.
|
|
79
|
+
headers[name] || headers[name.downcase] || headers[name.to_s]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Parse Retry-After header. Returns Float seconds. Returns nil when absent or
|
|
83
|
+
# unparseable. Past HTTP-dates clamp to 0.
|
|
84
|
+
def parse_retry_after(header)
|
|
85
|
+
return nil if header.nil?
|
|
86
|
+
|
|
87
|
+
value = header.to_s.strip
|
|
88
|
+
return nil if value.empty?
|
|
89
|
+
return value.to_f if value.match?(/\A\d+\z/)
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
target = Time.httpdate(value)
|
|
93
|
+
delta = target - Time.now
|
|
94
|
+
delta.negative? ? 0.0 : delta.to_f
|
|
95
|
+
rescue ArgumentError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def classify_faraday_error(error)
|
|
101
|
+
case error
|
|
102
|
+
when Faraday::TimeoutError
|
|
103
|
+
'timeout'
|
|
104
|
+
when Faraday::SSLError
|
|
105
|
+
'tls_handshake_failed'
|
|
106
|
+
when Faraday::ConnectionFailed
|
|
107
|
+
classify_connection_failure(error)
|
|
108
|
+
else
|
|
109
|
+
'unknown'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def classify_connection_failure(error)
|
|
114
|
+
wrapped = error.respond_to?(:wrapped_exception) ? error.wrapped_exception : nil
|
|
115
|
+
case wrapped
|
|
116
|
+
when SocketError
|
|
117
|
+
'dns_failure'
|
|
118
|
+
else
|
|
119
|
+
'connection_reset'
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_task_error(task_id, error_payload)
|
|
124
|
+
hash = if error_payload.respond_to?(:to_h)
|
|
125
|
+
error_payload.to_h
|
|
126
|
+
else
|
|
127
|
+
error_payload || {}
|
|
128
|
+
end
|
|
129
|
+
TaskError.new(
|
|
130
|
+
task_id: task_id,
|
|
131
|
+
error_type: lookup(hash, :type) || '',
|
|
132
|
+
description: lookup(hash, :description) || '',
|
|
133
|
+
stack_trace: lookup(hash, :stacktrace),
|
|
134
|
+
version: lookup(hash, :version),
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def lookup(hash, key)
|
|
139
|
+
hash[key] || hash[key.to_s]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
end
|