Skip to content

Commit ccf0b94

Browse files
authored
feat: specific exception classes for errors (#142)
1 parent f798adb commit ccf0b94

4 files changed

Lines changed: 158 additions & 28 deletions

File tree

lib/stream/client.rb

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -203,17 +203,12 @@ def make_http_request(method, relative_url, params = nil, data = nil, headers =
203203
class RaiseHttpException < Faraday::Middleware
204204
def call(env)
205205
@app.call(env).on_complete do |response|
206-
case response[:status].to_i
206+
status_code = response[:status].to_i
207+
case status_code
207208
when 200..203
208209
return response
209-
when 401
210-
raise StreamApiResponseException, error_message(response, 'Bad feed')
211-
when 403
212-
raise StreamApiResponseException, error_message(response, 'Bad auth/headers')
213-
when 404
214-
raise StreamApiResponseException, error_message(response, 'url not found')
215-
when 204...600
216-
raise StreamApiResponseException, error_message(response, _build_error_message(response.body))
210+
else
211+
parse_error(response, status_code: status_code)
217212
end
218213
end
219214
end
@@ -225,11 +220,48 @@ def initialize(app)
225220

226221
private
227222

228-
def _build_error_message(response)
229-
response = JSON.parse(response)
230-
msg = "#{response['exception']} details: #{response['detail']}"
231-
if response.key?('exception_fields')
232-
response['exception_fields'].map do |field, messages|
223+
EXCEPTION_CLASS_MAPPING = {
224+
2 => StreamApiResponseApiKeyException,
225+
3 => StreamApiResponseSignatureException,
226+
4 => StreamApiResponseInputException,
227+
5 => StreamApiResponseCustomFieldException,
228+
6 => StreamApiResponseFeedConfigException,
229+
7 => StreamApiResponseSiteSuspendedException,
230+
8 => StreamApiResponseInvalidPaginationException,
231+
9 => StreamApiResponseRateLimitReached,
232+
10 => StreamApiResponseMissingUserException,
233+
11 => StreamApiResponseRankingException,
234+
12 => StreamApiResponseMissingRankingException,
235+
13 => StreamApiResponseOldStorageBackendException,
236+
14 => StreamApiResponseJinjaRuntimeException,
237+
15 => StreamApiResponseBestPracticeException,
238+
16 => StreamApiResponseDoesNotExistException,
239+
17 => StreamApiResponseNotAllowedException,
240+
22 => StreamApiResponseConflictException
241+
}.freeze
242+
private_constant :EXCEPTION_CLASS_MAPPING
243+
244+
def parse_error(response, status_code:)
245+
body = JSON.parse(response.body)
246+
code = body['code']
247+
248+
exception_class = EXCEPTION_CLASS_MAPPING[code] || StreamApiResponseException
249+
case status_code
250+
when 401
251+
raise exception_class, error_message(response, 'Bad feed')
252+
when 403
253+
raise exception_class, error_message(response, 'Bad auth/headers')
254+
when 404
255+
raise exception_class, error_message(response, 'url not found')
256+
when 204...600
257+
raise exception_class, error_message(response, _build_error_message(body))
258+
end
259+
end
260+
261+
def _build_error_message(body)
262+
msg = "#{body['exception']} details: #{body['detail']}"
263+
if body.key?('exception_fields')
264+
body['exception_fields'].map do |field, messages|
233265
msg << "\n#{field}: #{messages}"
234266
end
235267
end

lib/stream/errors.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,39 @@ class Error < StandardError; end
33

44
class StreamApiResponseException < Error; end
55

6+
class StreamApiResponseApiKeyException < StreamApiResponseException; end
7+
8+
class StreamApiResponseSignatureException < StreamApiResponseException; end
9+
10+
class StreamApiResponseInputException < StreamApiResponseException; end
11+
12+
class StreamApiResponseCustomFieldException < StreamApiResponseException; end
13+
14+
class StreamApiResponseFeedConfigException < StreamApiResponseException; end
15+
16+
class StreamApiResponseSiteSuspendedException < StreamApiResponseException; end
17+
18+
class StreamApiResponseInvalidPaginationException < StreamApiResponseException; end
19+
20+
class StreamApiResponseRateLimitReached < StreamApiResponseException; end
21+
22+
class StreamApiResponseMissingRankingException < StreamApiResponseFeedConfigException; end
23+
24+
class StreamApiResponseMissingUserException < StreamApiResponseException; end
25+
26+
class StreamApiResponseRankingException < StreamApiResponseFeedConfigException; end
27+
28+
class StreamApiResponseOldStorageBackendException < StreamApiResponseException; end
29+
30+
class StreamApiResponseJinjaRuntimeException < StreamApiResponseException; end
31+
32+
class StreamApiResponseBestPracticeException < StreamApiResponseException; end
33+
34+
class StreamApiResponseDoesNotExistException < StreamApiResponseException; end
35+
36+
class StreamApiResponseNotAllowedException < StreamApiResponseException; end
37+
38+
class StreamApiResponseConflictException < StreamApiResponseException; end
39+
640
class StreamInputData < Error; end
741
end

spec/errors_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,23 @@
44
describe Stream::Error do
55
it { expect(Stream::Error).to be < StandardError }
66
it { expect(Stream::StreamApiResponseException).to be < Stream::Error }
7+
it { expect(Stream::StreamApiResponseDoesNotExistException).to be < Stream::StreamApiResponseException }
8+
it { expect(Stream::StreamApiResponseApiKeyException).to be < Stream::StreamApiResponseException }
9+
it { expect(Stream::StreamApiResponseSignatureException).to be < Stream::StreamApiResponseException }
10+
it { expect(Stream::StreamApiResponseInputException).to be < Stream::StreamApiResponseException }
11+
it { expect(Stream::StreamApiResponseCustomFieldException).to be < Stream::StreamApiResponseException }
12+
it { expect(Stream::StreamApiResponseFeedConfigException).to be < Stream::StreamApiResponseException }
13+
it { expect(Stream::StreamApiResponseSiteSuspendedException).to be < Stream::StreamApiResponseException }
14+
it { expect(Stream::StreamApiResponseInvalidPaginationException).to be < Stream::StreamApiResponseException }
15+
it { expect(Stream::StreamApiResponseRateLimitReached).to be < Stream::StreamApiResponseException }
16+
it { expect(Stream::StreamApiResponseMissingUserException).to be < Stream::StreamApiResponseException }
17+
it { expect(Stream::StreamApiResponseMissingRankingException).to be < Stream::StreamApiResponseFeedConfigException }
18+
it { expect(Stream::StreamApiResponseRankingException).to be < Stream::StreamApiResponseFeedConfigException }
19+
it { expect(Stream::StreamApiResponseOldStorageBackendException).to be < Stream::StreamApiResponseException }
20+
it { expect(Stream::StreamApiResponseJinjaRuntimeException).to be < Stream::StreamApiResponseException }
21+
it { expect(Stream::StreamApiResponseBestPracticeException).to be < Stream::StreamApiResponseException }
22+
it { expect(Stream::StreamApiResponseDoesNotExistException).to be < Stream::StreamApiResponseException }
23+
it { expect(Stream::StreamApiResponseNotAllowedException).to be < Stream::StreamApiResponseException }
24+
it { expect(Stream::StreamApiResponseConflictException).to be < Stream::StreamApiResponseException }
725
it { expect(Stream::StreamInputData).to be < Stream::Error }
826
end

spec/integration_spec.rb

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
describe 'Integration tests' do
1010
before(:all) do
1111
@client = Stream::Client.new(ENV.fetch('STREAM_API_KEY'), ENV.fetch('STREAM_API_SECRET'), nil, location: ENV.fetch('STREAM_REGION', nil), default_timeout: 10)
12+
@url = @client.get_http_client.conn.url_prefix.to_s.gsub(%r{/+$}, '')
1213
@feed42 = @client.feed('flat', generate_uniq_feed_name)
1314
@feed43 = @client.feed('flat', generate_uniq_feed_name)
1415

@@ -363,7 +364,7 @@
363364
example 'add incomplete activity' do
364365
expect do
365366
@feed42.add_activity({})
366-
end.to raise_error Stream::StreamApiResponseException
367+
end.to raise_error Stream::StreamApiResponseInputException
367368
end
368369

369370
it 'should be able to follow many feeds in one request' do
@@ -379,12 +380,11 @@
379380
{ source: 'badfeed:1', target: 'alsobad:1' },
380381
{ source: 'extrabadfeed:1', target: 'reallybad:3' }
381382
]
382-
url = @client.get_http_client.conn.url_prefix.to_s.gsub(%r{/+$}, '')
383383
expect do
384384
@client.follow_many(follows, 5000)
385385
end.to raise_error(
386-
Stream::StreamApiResponseException,
387-
%r{^POST #{url}/follow_many/\?activity_copy_limit=5000&api_key=[^:]+: 400: InputException details: activity_copy_limit must be a non-negative number not greater than 1000$}
386+
Stream::StreamApiResponseInputException,
387+
%r{^POST #{@url}/follow_many/\?activity_copy_limit=5000&api_key=[^:]+: 400: InputException details: activity_copy_limit must be a non-negative number not greater than 1000$}
388388
)
389389
end
390390

@@ -401,12 +401,11 @@
401401
{ source: 'user:1', target: 'timeline:1' },
402402
{ source: 'user:2', target: 42, keep_history: false }
403403
]
404-
url = @client.get_http_client.conn.url_prefix.to_s.gsub(%r{/+$}, '')
405404
expect do
406405
@client.unfollow_many(unfollows)
407406
end.to raise_error(
408-
Stream::StreamApiResponseException,
409-
%r{^POST #{url}/unfollow_many/\?api_key=[^:]+: 400: InputException details: invalid request payload$}
407+
Stream::StreamApiResponseInputException,
408+
%r{^POST #{@url}/unfollow_many/\?api_key=[^:]+: 400: InputException details: invalid request payload$}
410409
)
411410
end
412411

@@ -416,9 +415,48 @@
416415
@client.add_to_many(activity_data, feeds)
417416
end
418417

418+
# Create a `invalid_aggregation_format` aggregated feed group with the aggregation format
419+
# `{{test[]}}`.
420+
it 'should return an error if the aggregation format is invalid' do
421+
feed_name = generate_uniq_feed_name
422+
feed = @client.feed('invalid_aggregation_format', feed_name)
423+
feed.add_activity(actor: 1, verb: 'tweet', object: 1)
424+
425+
expect do
426+
feed.get(limit: 5)
427+
end.to raise_error(
428+
Stream::StreamApiResponseJinjaRuntimeException,
429+
%r{^GET #{@url}/feed/invalid_aggregation_format/#{feed_name}/\?api_key=[^&]+&limit=5: 400: JinjaRuntimeException details: the format contains syntax errors$}
430+
)
431+
end
432+
433+
it 'should return an error if the api key is invalid' do
434+
client = Stream::Client.new('invalid', 'invalid', nil, location: ENV.fetch('STREAM_REGION', nil), default_timeout: 10)
435+
feed_name = generate_uniq_feed_name
436+
feed = client.feed('notification', feed_name)
437+
expect do
438+
feed.get(limit: 5)
439+
end.to raise_error(
440+
Stream::StreamApiResponseApiKeyException,
441+
%r{^GET #{@url}/feed/notification/#{feed_name}/\?api_key=[^&]+&limit=5: 401: Bad feed$}
442+
)
443+
end
444+
445+
it 'should return an error if the api secret is invalid' do
446+
client = Stream::Client.new(ENV.fetch('STREAM_API_KEY'), 'invalid', nil, location: ENV.fetch('STREAM_REGION', nil), default_timeout: 10)
447+
feed_name = generate_uniq_feed_name
448+
feed = client.feed('notification', feed_name)
449+
expect do
450+
feed.get(limit: 5)
451+
end.to raise_error(
452+
Stream::StreamApiResponseNotAllowedException,
453+
%r{^GET #{@url}/feed/notification/#{feed_name}/\?api_key=[^&]+&limit=5: 403: Bad auth/headers$}
454+
)
455+
end
456+
419457
example 'updating many feed activities' do
420458
activities = []
421-
(0..10).each do |i|
459+
11.times do |i|
422460
activities << {
423461
actor: 'user:1',
424462
verb: 'do',
@@ -464,7 +502,7 @@
464502
end
465503
example 'add object to collection twice' do
466504
@client.collections.add('animals', { type: 'bear' }, id: @item_id)
467-
expect { @client.collections.add('animals', {}, id: @item_id) }.to raise_error Stream::StreamApiResponseException
505+
expect { @client.collections.add('animals', {}, id: @item_id) }.to raise_error Stream::StreamApiResponseInputException
468506
end
469507
example 'get collection item' do
470508
@client.collections.add('animals', { type: 'fox' }, id: @item_id)
@@ -482,7 +520,7 @@
482520
example 'collection item delete' do
483521
@client.collections.add('animals', { type: 'snake' }, id: @item_id)
484522
@client.collections.delete('animals', @item_id)
485-
expect { @client.collections.get('animals', @item_id) }.to raise_error Stream::StreamApiResponseException
523+
expect { @client.collections.get('animals', @item_id) }.to raise_error Stream::StreamApiResponseDoesNotExistException
486524
end
487525
end
488526

@@ -604,7 +642,7 @@
604642
})
605643
activity.delete('duration')
606644

607-
expect { @client.get_activities }.to raise_error Stream::StreamApiResponseException
645+
expect { @client.get_activities }.to raise_error Stream::StreamApiResponseInputException
608646

609647
# get by ID
610648
by_id = @client.get_activities(
@@ -837,7 +875,7 @@
837875
end
838876
example 'add user twice with error' do
839877
@client.users.add(@user_id)
840-
expect { @client.users.add(@user_id) }.to raise_error Stream::StreamApiResponseException
878+
expect { @client.users.add(@user_id) }.to raise_error Stream::StreamApiResponseConflictException
841879
end
842880
example 'get user' do
843881
create_response = @client.users.add(@user_id, data: { animal: 'wolf' })
@@ -848,6 +886,14 @@
848886

849887
expect(get_response).to eq create_response
850888
end
889+
example 'get user with does not exist error' do
890+
expect do
891+
@client.users.get(@user_id)
892+
end.to raise_error(
893+
Stream::StreamApiResponseDoesNotExistException,
894+
%r{^GET #{@url}/user/#{@user_id}/\?api_key=[^:]+: 404: url not found$}
895+
)
896+
end
851897
example 'update user' do
852898
@client.users.add(@user_id)
853899
response = @client.users.update(@user_id, data: { animal: 'dog' })
@@ -857,7 +903,7 @@
857903
example 'delete user' do
858904
@client.users.add(@user_id)
859905
@client.users.delete(@user_id)
860-
expect { @client.users.get(@user_id) }.to raise_error Stream::StreamApiResponseException
906+
expect { @client.users.get(@user_id) }.to raise_error Stream::StreamApiResponseDoesNotExistException
861907
end
862908
end
863909

@@ -903,7 +949,7 @@
903949
example 'delete reaction' do
904950
reaction = @client.reactions.add('like', @activity['id'], 'jim')
905951
@client.reactions.delete(reaction['id'])
906-
expect { @client.reactions.get(reaction['id']) }.to raise_error Stream::StreamApiResponseException
952+
expect { @client.reactions.get(reaction['id']) }.to raise_error Stream::StreamApiResponseDoesNotExistException
907953
end
908954
example 'filter reactions' do
909955
parent = @client.reactions.add('like', @activity['id'], 'jim')

0 commit comments

Comments
 (0)