getstream-ruby 7.0.0 → 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/lib/getstream_ruby/client.rb +98 -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/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: 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
|
|
@@ -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,7 +17,9 @@ 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
|
|
|
@@ -36,6 +41,7 @@ module GetStreamRuby
|
|
|
36
41
|
|
|
37
42
|
@configuration.validate!
|
|
38
43
|
@connection = build_connection
|
|
44
|
+
@configuration.log_pool_config_to(@configuration.logger)
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
def feed_resource
|
|
@@ -130,7 +136,50 @@ module GetStreamRuby
|
|
|
130
136
|
request(:post, path, body)
|
|
131
137
|
end
|
|
132
138
|
|
|
133
|
-
|
|
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)
|
|
134
183
|
# Handle query parameters
|
|
135
184
|
if query_params && !query_params.empty?
|
|
136
185
|
query_string = query_params.map { |k, v| "#{k}=#{v}" }.join('&')
|
|
@@ -138,12 +187,12 @@ module GetStreamRuby
|
|
|
138
187
|
end
|
|
139
188
|
|
|
140
189
|
# Make the request
|
|
141
|
-
request(method, path, body)
|
|
190
|
+
request(method, path, body, request_timeout: request_timeout)
|
|
142
191
|
end
|
|
143
192
|
|
|
144
193
|
private
|
|
145
194
|
|
|
146
|
-
def request(method, path, data = {})
|
|
195
|
+
def request(method, path, data = {}, request_timeout: nil)
|
|
147
196
|
# Add API key to query parameters
|
|
148
197
|
query_params = { api_key: @configuration.api_key }
|
|
149
198
|
|
|
@@ -158,15 +207,20 @@ module GetStreamRuby
|
|
|
158
207
|
req.headers['stream-auth-type'] = 'jwt'
|
|
159
208
|
req.headers['X-Stream-Client'] = user_agent
|
|
160
209
|
req.body = data.to_json
|
|
210
|
+
req.options.timeout = request_timeout if request_timeout
|
|
161
211
|
|
|
162
212
|
end
|
|
163
213
|
|
|
164
214
|
handle_response(response)
|
|
165
215
|
rescue Faraday::Error => e
|
|
166
|
-
raise
|
|
216
|
+
raise TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
167
217
|
end
|
|
168
218
|
|
|
169
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
|
+
|
|
170
224
|
Faraday.new(url: @configuration.base_url) do |conn|
|
|
171
225
|
|
|
172
226
|
conn.request :multipart
|
|
@@ -177,22 +231,44 @@ module GetStreamRuby
|
|
|
177
231
|
backoff_factor: 2,
|
|
178
232
|
}
|
|
179
233
|
conn.response :json, content_type: /\bjson$/
|
|
180
|
-
|
|
234
|
+
# :gzip must come after :json (Faraday runs response middleware in reverse).
|
|
235
|
+
conn.request :gzip
|
|
181
236
|
configure_adapter(conn)
|
|
182
|
-
|
|
237
|
+
|
|
238
|
+
conn.options.timeout = @configuration.request_timeout
|
|
239
|
+
conn.options.open_timeout = @configuration.connect_timeout
|
|
183
240
|
|
|
184
241
|
end
|
|
185
242
|
end
|
|
186
243
|
|
|
187
244
|
def configure_adapter(connection)
|
|
188
|
-
adapter
|
|
189
|
-
adapter_options
|
|
190
|
-
|
|
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
|
|
191
266
|
rescue Faraday::Error, ArgumentError => e
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
267
|
+
# A fallback silently disables pooling, so always WARN (never swallow).
|
|
268
|
+
@configuration.warn_pool_fallback(Faraday.default_adapter, e)
|
|
195
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
|
|
196
272
|
end
|
|
197
273
|
|
|
198
274
|
def generate_auth_header
|
|
@@ -211,29 +287,13 @@ module GetStreamRuby
|
|
|
211
287
|
end
|
|
212
288
|
|
|
213
289
|
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
|
|
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)
|
|
237
297
|
end
|
|
238
298
|
|
|
239
299
|
def multipart_request?(data)
|
|
@@ -249,10 +309,10 @@ module GetStreamRuby
|
|
|
249
309
|
payload = {}
|
|
250
310
|
|
|
251
311
|
# Handle file field
|
|
252
|
-
raise
|
|
312
|
+
raise ArgumentError, 'file name must be provided' if data.file.nil? || data.file.empty?
|
|
253
313
|
|
|
254
314
|
file_path = data.file
|
|
255
|
-
raise
|
|
315
|
+
raise ArgumentError, "file not found: #{file_path}" unless File.exist?(file_path)
|
|
256
316
|
|
|
257
317
|
# Determine content type
|
|
258
318
|
content_type = detect_content_type(file_path)
|
|
@@ -288,7 +348,7 @@ module GetStreamRuby
|
|
|
288
348
|
|
|
289
349
|
handle_response(response)
|
|
290
350
|
rescue Faraday::Error => e
|
|
291
|
-
raise
|
|
351
|
+
raise TransportError.new("Request failed: #{e.message}", error_type: ErrorMapping.classify_faraday_error(e))
|
|
292
352
|
end
|
|
293
353
|
|
|
294
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
|
|
@@ -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.0
|
|
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-09 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
|