getstream-ruby 7.0.0 → 7.1.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/lib/getstream_ruby/client.rb +109 -39
- 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/webhook.rb +3 -3
- data/lib/getstream_ruby/version.rb +1 -1
- metadata +47 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d60ac76c36bbf0f4fba277c49eca708e4d4784808a1c245a115dc48a05adf4f1
|
|
4
|
+
data.tar.gz: 372a04fd43e84b06c8e46eb64540124a2c4c7702066f03b415195e081137df61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ed574ba891c4692474dfc64358acc9c8c3c2b00b9d93238f333c75d5659263e0bd0006a328102344d50356e097b765bab0f69b6e9ee60ca6930710adc932c5c
|
|
7
|
+
data.tar.gz: 929b48c5fdffa86dfaf3155830493deeecba853d9bd72eae58c63fdbb235672e3920827a385b5789572ebc4fdf7c43c6641e09d8d75cba185f6a3ec0e9d8a163
|
|
@@ -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'
|
|
@@ -14,12 +17,24 @@ require_relative 'generated/video_client'
|
|
|
14
17
|
require_relative 'extensions/moderation_extensions'
|
|
15
18
|
require_relative 'generated/feed'
|
|
16
19
|
require_relative 'generated/webhook'
|
|
20
|
+
require_relative 'generated/models/api_error'
|
|
17
21
|
require_relative 'stream_response'
|
|
22
|
+
require_relative 'error_mapping'
|
|
18
23
|
|
|
19
24
|
module GetStreamRuby
|
|
20
25
|
|
|
21
26
|
class Client
|
|
22
27
|
|
|
28
|
+
# Backdate the JWT `iat` claim by this many seconds.
|
|
29
|
+
#
|
|
30
|
+
# JWT timestamps are whole-second (RFC 7519 NumericDate), so `Time.now.to_i`
|
|
31
|
+
# truncates to the second. The server applies minimal forward leeway on
|
|
32
|
+
# `iat`, so stamping `iat = now` lets a small fraction of requests be
|
|
33
|
+
# rejected ("token used before issue at (iat)") whenever the caller's clock
|
|
34
|
+
# is even marginally ahead of the server and the truncation lands on a
|
|
35
|
+
# second boundary. Backdating absorbs that sub-second skew.
|
|
36
|
+
AUTH_IAT_LEEWAY_SECONDS = 5
|
|
37
|
+
|
|
23
38
|
attr_reader :configuration
|
|
24
39
|
|
|
25
40
|
def initialize(config = nil, api_key: nil, api_secret: nil, **options)
|
|
@@ -36,6 +51,7 @@ module GetStreamRuby
|
|
|
36
51
|
|
|
37
52
|
@configuration.validate!
|
|
38
53
|
@connection = build_connection
|
|
54
|
+
@configuration.log_pool_config_to(@configuration.logger)
|
|
39
55
|
end
|
|
40
56
|
|
|
41
57
|
def feed_resource
|
|
@@ -130,7 +146,50 @@ module GetStreamRuby
|
|
|
130
146
|
request(:post, path, body)
|
|
131
147
|
end
|
|
132
148
|
|
|
133
|
-
|
|
149
|
+
# Polls the task-status endpoint until the task reaches a terminal state.
|
|
150
|
+
#
|
|
151
|
+
# Behaviour:
|
|
152
|
+
# - status="completed": returns the task `result` payload.
|
|
153
|
+
# - status="failed": raises `TaskError` populated from the task's
|
|
154
|
+
# `ErrorResult` (`type`, `description`, `stacktrace`,
|
|
155
|
+
# `version`).
|
|
156
|
+
# - timeout elapsed: raises `TransportError` with `error_type:
|
|
157
|
+
# "timeout"`.
|
|
158
|
+
#
|
|
159
|
+
# @param task_id [String]
|
|
160
|
+
# @param poll_interval [Numeric] seconds between polls (default 1)
|
|
161
|
+
# @param timeout [Numeric] max seconds to wait (default 60)
|
|
162
|
+
# @return [Object] the task `result` payload on success
|
|
163
|
+
# @raise [TaskError] when the task reports `status="failed"`
|
|
164
|
+
# @raise [TransportError] when the timeout elapses (`error_type="timeout"`)
|
|
165
|
+
def wait_for_task(task_id, poll_interval: 1, timeout: 60)
|
|
166
|
+
start_time = monotonic_now
|
|
167
|
+
|
|
168
|
+
loop do
|
|
169
|
+
|
|
170
|
+
response = common.get_task(task_id)
|
|
171
|
+
status = response.status
|
|
172
|
+
|
|
173
|
+
case status
|
|
174
|
+
when 'completed'
|
|
175
|
+
return response.result
|
|
176
|
+
when 'failed'
|
|
177
|
+
raise ErrorMapping.build_task_error(task_id, response.error)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if monotonic_now - start_time >= timeout
|
|
181
|
+
raise TransportError.new(
|
|
182
|
+
"wait_for_task timed out after #{timeout}s for task_id=#{task_id}",
|
|
183
|
+
error_type: 'timeout',
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
sleep(poll_interval)
|
|
188
|
+
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def make_request(method, path, query_params: nil, body: nil, request_timeout: nil)
|
|
134
193
|
# Handle query parameters
|
|
135
194
|
if query_params && !query_params.empty?
|
|
136
195
|
query_string = query_params.map { |k, v| "#{k}=#{v}" }.join('&')
|
|
@@ -138,12 +197,12 @@ module GetStreamRuby
|
|
|
138
197
|
end
|
|
139
198
|
|
|
140
199
|
# Make the request
|
|
141
|
-
request(method, path, body)
|
|
200
|
+
request(method, path, body, request_timeout: request_timeout)
|
|
142
201
|
end
|
|
143
202
|
|
|
144
203
|
private
|
|
145
204
|
|
|
146
|
-
def request(method, path, data = {})
|
|
205
|
+
def request(method, path, data = {}, request_timeout: nil)
|
|
147
206
|
# Add API key to query parameters
|
|
148
207
|
query_params = { api_key: @configuration.api_key }
|
|
149
208
|
|
|
@@ -158,15 +217,20 @@ module GetStreamRuby
|
|
|
158
217
|
req.headers['stream-auth-type'] = 'jwt'
|
|
159
218
|
req.headers['X-Stream-Client'] = user_agent
|
|
160
219
|
req.body = data.to_json
|
|
220
|
+
req.options.timeout = request_timeout if request_timeout
|
|
161
221
|
|
|
162
222
|
end
|
|
163
223
|
|
|
164
224
|
handle_response(response)
|
|
165
225
|
rescue Faraday::Error => e
|
|
166
|
-
raise
|
|
226
|
+
raise TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
167
227
|
end
|
|
168
228
|
|
|
169
229
|
def build_connection
|
|
230
|
+
# Escape hatch #1: user supplied a fully-built Faraday::Connection.
|
|
231
|
+
# Use it as-is; none of the 5 knobs apply.
|
|
232
|
+
return @configuration.http_client if @configuration.http_client
|
|
233
|
+
|
|
170
234
|
Faraday.new(url: @configuration.base_url) do |conn|
|
|
171
235
|
|
|
172
236
|
conn.request :multipart
|
|
@@ -177,28 +241,50 @@ module GetStreamRuby
|
|
|
177
241
|
backoff_factor: 2,
|
|
178
242
|
}
|
|
179
243
|
conn.response :json, content_type: /\bjson$/
|
|
180
|
-
|
|
244
|
+
# :gzip must come after :json (Faraday runs response middleware in reverse).
|
|
245
|
+
conn.request :gzip
|
|
181
246
|
configure_adapter(conn)
|
|
182
|
-
|
|
247
|
+
|
|
248
|
+
conn.options.timeout = @configuration.request_timeout
|
|
249
|
+
conn.options.open_timeout = @configuration.connect_timeout
|
|
183
250
|
|
|
184
251
|
end
|
|
185
252
|
end
|
|
186
253
|
|
|
187
254
|
def configure_adapter(connection)
|
|
188
|
-
adapter
|
|
189
|
-
adapter_options
|
|
190
|
-
|
|
255
|
+
# Escape hatch #2: custom adapter symbol. Use it with the user's
|
|
256
|
+
# adapter_options; do NOT apply pool_size/idle_timeout (those are
|
|
257
|
+
# net_http_persistent-specific).
|
|
258
|
+
if @configuration.faraday_adapter
|
|
259
|
+
adapter = @configuration.faraday_adapter
|
|
260
|
+
adapter_options = @configuration.faraday_adapter_options || {}
|
|
261
|
+
# Header-based keep-alive only on the custom-adapter path.
|
|
262
|
+
# net_http_persistent (default) keeps connections alive natively without any HTTP header.
|
|
263
|
+
connection.headers['Connection'] = 'keep-alive' if @configuration.connection_keep_alive
|
|
264
|
+
connection.adapter(adapter, **adapter_options)
|
|
265
|
+
return
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Default: net_http_persistent with the 5-knob config.
|
|
269
|
+
# Never set Connection: close; net_http_persistent keeps connections alive natively.
|
|
270
|
+
idle = @configuration.idle_timeout
|
|
271
|
+
connection.adapter :net_http_persistent, pool_size: @configuration.max_conns_per_host do |http|
|
|
272
|
+
|
|
273
|
+
http.idle_timeout = idle
|
|
274
|
+
|
|
275
|
+
end
|
|
191
276
|
rescue Faraday::Error, ArgumentError => e
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
277
|
+
# A fallback silently disables pooling, so always WARN (never swallow).
|
|
278
|
+
@configuration.warn_pool_fallback(Faraday.default_adapter, e)
|
|
195
279
|
connection.adapter Faraday.default_adapter
|
|
280
|
+
# Record the adapter actually built so the INFO log reports it accurately.
|
|
281
|
+
@configuration.effective_adapter = Faraday.default_adapter.to_s
|
|
196
282
|
end
|
|
197
283
|
|
|
198
284
|
def generate_auth_header
|
|
199
285
|
JWT.encode(
|
|
200
286
|
{
|
|
201
|
-
iat: Time.now.to_i,
|
|
287
|
+
iat: Time.now.to_i - AUTH_IAT_LEEWAY_SECONDS,
|
|
202
288
|
server: true,
|
|
203
289
|
},
|
|
204
290
|
@configuration.api_secret,
|
|
@@ -211,29 +297,13 @@ module GetStreamRuby
|
|
|
211
297
|
end
|
|
212
298
|
|
|
213
299
|
def handle_response(response)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
JSON.parse(response.body)
|
|
222
|
-
rescue JSON::ParserError
|
|
223
|
-
response.body
|
|
224
|
-
end
|
|
225
|
-
else
|
|
226
|
-
response.body
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
error_message = if parsed_body.is_a?(Hash)
|
|
230
|
-
parsed_body['message'] || parsed_body['detail'] ||
|
|
231
|
-
"Request failed with status #{response.status}"
|
|
232
|
-
else
|
|
233
|
-
"Request failed with status #{response.status}"
|
|
234
|
-
end
|
|
235
|
-
raise APIError, error_message
|
|
236
|
-
end
|
|
300
|
+
return StreamResponse.new(response.body) if (200..299).cover?(response.status)
|
|
301
|
+
|
|
302
|
+
ErrorMapping.raise_api_error(response)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def monotonic_now
|
|
306
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
237
307
|
end
|
|
238
308
|
|
|
239
309
|
def multipart_request?(data)
|
|
@@ -249,10 +319,10 @@ module GetStreamRuby
|
|
|
249
319
|
payload = {}
|
|
250
320
|
|
|
251
321
|
# Handle file field
|
|
252
|
-
raise
|
|
322
|
+
raise ArgumentError, 'file name must be provided' if data.file.nil? || data.file.empty?
|
|
253
323
|
|
|
254
324
|
file_path = data.file
|
|
255
|
-
raise
|
|
325
|
+
raise ArgumentError, "file not found: #{file_path}" unless File.exist?(file_path)
|
|
256
326
|
|
|
257
327
|
# Determine content type
|
|
258
328
|
content_type = detect_content_type(file_path)
|
|
@@ -288,7 +358,7 @@ module GetStreamRuby
|
|
|
288
358
|
|
|
289
359
|
handle_response(response)
|
|
290
360
|
rescue Faraday::Error => e
|
|
291
|
-
raise
|
|
361
|
+
raise TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
292
362
|
end
|
|
293
363
|
|
|
294
364
|
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
|
|
@@ -2,8 +2,112 @@
|
|
|
2
2
|
|
|
3
3
|
module GetStreamRuby
|
|
4
4
|
|
|
5
|
-
class
|
|
6
|
-
class
|
|
7
|
-
|
|
5
|
+
# Base error class for the SDK. Every SDK-raised exception is a subclass.
|
|
6
|
+
class StreamError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Back-compat alias. The prior base class was `Error`; keep it usable so
|
|
9
|
+
# any existing `rescue GetStreamRuby::Error` clauses keep matching.
|
|
10
|
+
Error = StreamError
|
|
11
|
+
|
|
12
|
+
class ConfigurationError < StreamError; end
|
|
13
|
+
|
|
14
|
+
# Raised on any HTTP 4xx/5xx response. Also raised when an HTTP response is
|
|
15
|
+
# received but its body is not a parseable `APIError` envelope, with `code = 0`
|
|
16
|
+
# and `message = "failed to parse error response"`.
|
|
17
|
+
class ApiError < StreamError
|
|
18
|
+
|
|
19
|
+
attr_reader :status_code, :code, :exception_fields, :unrecoverable,
|
|
20
|
+
:raw_response_body, :more_info, :details
|
|
21
|
+
|
|
22
|
+
def initialize(message:, status_code:, code:, exception_fields: nil,
|
|
23
|
+
unrecoverable: nil, raw_response_body: nil,
|
|
24
|
+
more_info: nil, details: nil)
|
|
25
|
+
super(message)
|
|
26
|
+
@status_code = status_code
|
|
27
|
+
@code = code
|
|
28
|
+
@exception_fields = exception_fields || {}
|
|
29
|
+
@unrecoverable = unrecoverable.nil? ? false : unrecoverable
|
|
30
|
+
@raw_response_body = raw_response_body || ''
|
|
31
|
+
@more_info = more_info
|
|
32
|
+
@details = details
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Raised on HTTP 429. Adds parsed `Retry-After` as Float seconds, or nil when
|
|
38
|
+
# the header is absent or unparseable. Per RFC 7231, both integer-seconds and
|
|
39
|
+
# HTTP-date forms are supported. Past HTTP-dates clamp to 0.
|
|
40
|
+
class RateLimitError < ApiError
|
|
41
|
+
|
|
42
|
+
attr_reader :retry_after
|
|
43
|
+
|
|
44
|
+
def initialize(retry_after: nil, **kwargs)
|
|
45
|
+
super(**kwargs)
|
|
46
|
+
@retry_after = retry_after
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Allowed values for `TransportError#error_type`.
|
|
52
|
+
TRANSPORT_ERROR_TYPES = %w[
|
|
53
|
+
connection_reset
|
|
54
|
+
timeout
|
|
55
|
+
dns_failure
|
|
56
|
+
tls_handshake_failed
|
|
57
|
+
unknown
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
60
|
+
# Raised when no HTTP response is received: connection reset, timeout, TLS
|
|
61
|
+
# handshake failure, DNS failure. Always raised inside the matching `rescue`
|
|
62
|
+
# block so Ruby auto-sets `Exception#cause` to the underlying Faraday error.
|
|
63
|
+
class TransportError < StreamError
|
|
64
|
+
|
|
65
|
+
attr_reader :error_type
|
|
66
|
+
|
|
67
|
+
def initialize(message = nil, error_type: 'unknown')
|
|
68
|
+
super(message)
|
|
69
|
+
@error_type = error_type
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Raised by `Client#wait_for_task` when an async task finishes with
|
|
75
|
+
# status="failed". Carries the populated `ErrorResult` fields.
|
|
76
|
+
class TaskError < StreamError
|
|
77
|
+
|
|
78
|
+
attr_reader :task_id, :error_type, :description, :stack_trace, :version
|
|
79
|
+
|
|
80
|
+
def initialize(task_id:, error_type:, description:,
|
|
81
|
+
stack_trace: nil, version: nil)
|
|
82
|
+
super(description)
|
|
83
|
+
@task_id = task_id
|
|
84
|
+
@error_type = error_type
|
|
85
|
+
@description = description
|
|
86
|
+
@stack_trace = stack_trace
|
|
87
|
+
@version = version
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Deprecated alias for ApiError. Will be removed in v9.0.
|
|
93
|
+
# Implemented via `const_missing` so the first access emits a `Kernel.warn`
|
|
94
|
+
# once-only and the constant is cached afterwards (no per-rescue noise).
|
|
95
|
+
@apierror_alias_warned = false
|
|
96
|
+
|
|
97
|
+
def self.const_missing(name)
|
|
98
|
+
if name == :APIError
|
|
99
|
+
unless @apierror_alias_warned
|
|
100
|
+
Kernel.warn(
|
|
101
|
+
'[DEPRECATION] GetStreamRuby::APIError is renamed to ' \
|
|
102
|
+
'GetStreamRuby::ApiError. The old constant will be removed in v9.0.',
|
|
103
|
+
)
|
|
104
|
+
@apierror_alias_warned = true
|
|
105
|
+
end
|
|
106
|
+
const_set(:APIError, ApiError)
|
|
107
|
+
ApiError
|
|
108
|
+
else
|
|
109
|
+
super
|
|
110
|
+
end
|
|
111
|
+
end
|
|
8
112
|
|
|
9
113
|
end
|
|
@@ -817,7 +817,7 @@ module StreamChat
|
|
|
817
817
|
# detection decides whether to decompress.
|
|
818
818
|
#
|
|
819
819
|
# {parse_sqs} sits on top of this and works transparently for both wire
|
|
820
|
-
# formats
|
|
820
|
+
# formats: no caller code change, no flag, no header.
|
|
821
821
|
#
|
|
822
822
|
# @param message_body [String]
|
|
823
823
|
# @return [String]
|
|
@@ -829,7 +829,7 @@ module StreamChat
|
|
|
829
829
|
begin
|
|
830
830
|
Base64.strict_decode64(message_body)
|
|
831
831
|
rescue ArgumentError
|
|
832
|
-
# Not base64
|
|
832
|
+
# Not base64, so treat input as raw bytes (uncompressed wire format).
|
|
833
833
|
message_body.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
834
834
|
end
|
|
835
835
|
gunzip_payload(decoded)
|
|
@@ -840,7 +840,7 @@ module StreamChat
|
|
|
840
840
|
# pre-extracted Message string flows through.
|
|
841
841
|
#
|
|
842
842
|
# Heuristic: try to JSON-parse the input. If it yields a Hash with a
|
|
843
|
-
# String +Message+ field, that's the envelope shape
|
|
843
|
+
# String +Message+ field, that's the envelope shape; return the Message.
|
|
844
844
|
# Otherwise the input is presumed to BE the pre-extracted Message
|
|
845
845
|
# (base64-encoded bytes are not valid JSON, so this falls through cleanly).
|
|
846
846
|
def self.unwrap_sns_notification_body(body)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: getstream-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.
|
|
4
|
+
version: 7.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GetStream
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dotenv
|
|
@@ -30,14 +30,28 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '2.
|
|
33
|
+
version: '2.5'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '2.
|
|
40
|
+
version: '2.5'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: faraday-gzip
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
56
|
name: faraday-multipart
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,6 +66,20 @@ dependencies:
|
|
|
52
66
|
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
68
|
version: '1.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: faraday-net_http_persistent
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '2.3'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '2.3'
|
|
55
83
|
- !ruby/object:Gem::Dependency
|
|
56
84
|
name: faraday-retry
|
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -94,6 +122,20 @@ dependencies:
|
|
|
94
122
|
- - "~>"
|
|
95
123
|
- !ruby/object:Gem::Version
|
|
96
124
|
version: '2.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: net-http-persistent
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '4.0'
|
|
132
|
+
type: :runtime
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '4.0'
|
|
97
139
|
description: Official Ruby SDK for GetStream's activity feeds and chat APIs
|
|
98
140
|
email:
|
|
99
141
|
- support@getstream.io
|
|
@@ -105,6 +147,7 @@ files:
|
|
|
105
147
|
- lib/getstream_ruby.rb
|
|
106
148
|
- lib/getstream_ruby/client.rb
|
|
107
149
|
- lib/getstream_ruby/configuration.rb
|
|
150
|
+
- lib/getstream_ruby/error_mapping.rb
|
|
108
151
|
- lib/getstream_ruby/errors.rb
|
|
109
152
|
- lib/getstream_ruby/extensions/moderation_extensions.rb
|
|
110
153
|
- lib/getstream_ruby/generated/base_model.rb
|