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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 725fbf9f73fef4a9c33f241c9911d226222041e245084e2dbca69cbc0418eaef
4
- data.tar.gz: 637cc6dd6bc256959b15ae6a98e62f1131f3bfdf6183e8c3f64acd6e6cfc33d2
3
+ metadata.gz: 388126d2a284a9b97ac8eb6cd1793088ccfc51992504de016192d7974d582e3e
4
+ data.tar.gz: 3fb211b156d236f9b27734651215351d9a3a370d6d6a1da52bfe5fa779cdf7b9
5
5
  SHA512:
6
- metadata.gz: 21e076ab8023229dbdda7feff3405716929d315aa96c0d0478df09c3f7b27db8a6d24e9ea399782e16cc52f355807e33adcebe558e2c040acfcb66875b8d9ed8
7
- data.tar.gz: 96f2ce649e6c7f40e1651696f39c7e3612ea822e93351c3a86f0ce352b854f7a4ac12e7216f95df9933754da6a107dcb2ac6aa85892c9bc453f437c5cbf252a3
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
- def make_request(method, path, query_params: nil, body: nil)
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 APIError, "Request failed: #{e.message}"
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
- conn.headers['Connection'] = 'keep-alive' if @configuration.connection_keep_alive
234
+ # :gzip must come after :json (Faraday runs response middleware in reverse).
235
+ conn.request :gzip
181
236
  configure_adapter(conn)
182
- conn.options.timeout = @configuration.timeout
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 = @configuration.faraday_adapter || Faraday.default_adapter
189
- adapter_options = @configuration.faraday_adapter_options || {}
190
- connection.adapter(adapter, **adapter_options)
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
- @configuration.logger&.warn(
193
- "Falling back to #{Faraday.default_adapter}: could not use adapter #{adapter} (#{e.message})",
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
- 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
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 APIError, 'file name must be provided' if data.file.nil? || data.file.empty?
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 APIError, "file not found: #{file_path}" unless File.exist?(file_path)
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 APIError, "Request failed: #{e.message}"
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
- 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.0'
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.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-05-22 00:00:00.000000000 Z
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.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