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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 725fbf9f73fef4a9c33f241c9911d226222041e245084e2dbca69cbc0418eaef
4
- data.tar.gz: 637cc6dd6bc256959b15ae6a98e62f1131f3bfdf6183e8c3f64acd6e6cfc33d2
3
+ metadata.gz: d60ac76c36bbf0f4fba277c49eca708e4d4784808a1c245a115dc48a05adf4f1
4
+ data.tar.gz: 372a04fd43e84b06c8e46eb64540124a2c4c7702066f03b415195e081137df61
5
5
  SHA512:
6
- metadata.gz: 21e076ab8023229dbdda7feff3405716929d315aa96c0d0478df09c3f7b27db8a6d24e9ea399782e16cc52f355807e33adcebe558e2c040acfcb66875b8d9ed8
7
- data.tar.gz: 96f2ce649e6c7f40e1651696f39c7e3612ea822e93351c3a86f0ce352b854f7a4ac12e7216f95df9933754da6a107dcb2ac6aa85892c9bc453f437c5cbf252a3
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
- def make_request(method, path, query_params: nil, body: nil)
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 APIError, "Request failed: #{e.message}"
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
- conn.headers['Connection'] = 'keep-alive' if @configuration.connection_keep_alive
244
+ # :gzip must come after :json (Faraday runs response middleware in reverse).
245
+ conn.request :gzip
181
246
  configure_adapter(conn)
182
- conn.options.timeout = @configuration.timeout
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 = @configuration.faraday_adapter || Faraday.default_adapter
189
- adapter_options = @configuration.faraday_adapter_options || {}
190
- connection.adapter(adapter, **adapter_options)
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
- @configuration.logger&.warn(
193
- "Falling back to #{Faraday.default_adapter}: could not use adapter #{adapter} (#{e.message})",
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
- case response.status
215
- when 200..299
216
- StreamResponse.new(response.body)
217
- else
218
- # Parse JSON response body if it's a string
219
- parsed_body = if response.body.is_a?(String)
220
- begin
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 APIError, 'file name must be provided' if data.file.nil? || data.file.empty?
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 APIError, "file not found: #{file_path}" unless File.exist?(file_path)
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 APIError, "Request failed: #{e.message}"
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
- if use_env
23
- @api_key = api_key || ENV.fetch('STREAM_API_KEY', nil)
24
- @api_secret = api_secret || ENV.fetch('STREAM_API_SECRET', nil)
25
- @base_url = base_url || ENV['STREAM_BASE_URL'] || 'https://chat.stream-io-api.com'
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
- @faraday_adapter = (faraday_adapter || ENV.fetch('STREAM_FARADAY_ADAPTER', nil))&.to_sym
36
- @faraday_adapter_options = faraday_adapter_options || default_adapter_options
37
- @connection_keep_alive = if connection_keep_alive.nil?
38
- ENV.fetch('STREAM_CONNECTION_KEEP_ALIVE', 'true') == 'true'
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 Error < StandardError; end
6
- class ConfigurationError < Error; end
7
- class APIError < Error; end
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 no caller code change, no flag, no header.
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 treat input as raw bytes (uncompressed wire format).
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 return the Message.
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GetStreamRuby
4
4
 
5
- VERSION = '7.0.0'
5
+ VERSION = '7.1.1'
6
6
 
7
7
  end
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.0.0
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-05-22 00:00:00.000000000 Z
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.0'
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.0'
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