Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions lib/stream-chat/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,28 @@ class StreamAPIException < StandardError
def initialize(response)
super()
@response = response

# Seed defaults first so the typed readers never return nil. A 5xx can
# arrive with an empty or non-JSON body (e.g. a load balancer emitting a
# bare 503), and an error envelope may omit "code"/"message" or send a
# non-integer "code". Without these defaults, reading error_code on such
# an exception raised a Sorbet TypeError that masked the real HTTP error.
@json_response = T.let(false, T::Boolean)
@error_code = T.let(-1, Integer)
@error_message = T.let('unknown', String)

begin
parsed_response = JSON.parse(response.body)
@json_response = T.let(true, T::Boolean)
@error_code = T.let(parsed_response.fetch('code', 'unknown'), Integer)
@error_message = T.let(parsed_response.fetch('message', 'unknown'), String)
rescue JSON::ParserError
@json_response = false
return
end
return unless parsed_response.is_a?(Hash)

@json_response = true
code = parsed_response['code']
@error_code = code if code.is_a?(Integer)
msg = parsed_response['message']
@error_message = msg if msg.is_a?(String)
end

sig { returns(String) }
Expand Down
68 changes: 68 additions & 0 deletions spec/errors_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# typed: false
# frozen_string_literal: true

require 'stream-chat'

describe StreamChat::StreamAPIException do
def response_with(status, body)
Faraday::Response.new(Faraday::Env.from(status: status, body: body))
end

context 'with a well-formed JSON error body' do
subject(:exception) do
described_class.new(response_with(429, { code: 9, message: 'rate limit' }.to_json))
end

it 'exposes the integer code and message' do
expect(exception.json_response).to be true
expect(exception.error_code).to eq(9)
expect(exception.error_message).to eq('rate limit')
expect(exception.message).to eq('StreamChat error code 9: rate limit')
end
end

context 'with a 503 and an empty body' do
subject(:exception) { described_class.new(response_with(503, '')) }

it 'does not raise when the typed readers are accessed' do
expect(exception.json_response).to be false
expect(exception.error_code).to eq(-1)
expect(exception.error_message).to eq('unknown')
end

it 'falls back to the HTTP status in the message' do
expect(exception.message).to eq('StreamChat error HTTP code: 503')
end
end

context 'with a non-JSON body' do
subject(:exception) { described_class.new(response_with(502, '<html>502 Bad Gateway</html>')) }

it 'falls back to defaults and the HTTP status' do
expect(exception.json_response).to be false
expect(exception.error_code).to eq(-1)
expect(exception.message).to eq('StreamChat error HTTP code: 502')
end
end

context 'with a non-integer code (edge rate-limit envelope)' do
subject(:exception) do
described_class.new(response_with(429, { code: 'rate_limit_exceeded', message: 'slow down' }.to_json))
end

it 'keeps the sentinel code rather than raising a TypeError' do
expect(exception.json_response).to be true
expect(exception.error_code).to eq(-1)
expect(exception.error_message).to eq('slow down')
end
end

context 'with a JSON body missing code and message' do
subject(:exception) { described_class.new(response_with(500, {}.to_json)) }

it 'returns the default code and message' do
expect(exception.error_code).to eq(-1)
expect(exception.error_message).to eq('unknown')
end
end
end
Loading