Skip to content

Commit 565c386

Browse files
dblockCopilot
andcommitted
Add configured client copies with error callbacks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b771787 commit 565c386

5 files changed

Lines changed: 132 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### 3.3.0 (Next)
22

3+
* [#606](https://github.com/slack-ruby/slack-ruby-client/pull/606): Add configured client copies with error callbacks for Slack and transport failures - [@dblock](https://github.com/dblock).
34
* [#605](https://github.com/slack-ruby/slack-ruby-client/pull/605): Add configured client copies with request and response callbacks, including pagination - [@dblock](https://github.com/dblock).
45
* [#603](https://github.com/slack-ruby/slack-ruby-client/pull/603): Enforce consistent exception messages with rubocop-exception_messages and run lint separately from the test matrix - [@dblock](https://github.com/dblock).
56
* [#602](https://github.com/slack-ruby/slack-ruby-client/pull/602): Make gli an optional dependency; only the `slack` command-line client needs it - [@corsonknowles](https://github.com/corsonknowles).

‎README.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,21 @@ Request callbacks receive a `Faraday::Request` after authentication and request
321321

322322
Chaining registers callbacks in order. Copies preserve client configuration but use independent connections and callback lists; the original client is unchanged. Callback exceptions propagate and request callback failures prevent dispatch. Callbacks are synchronous; avoid storing sensitive authorization headers, and synchronize any shared state if using a client concurrently.
323323

324+
Use `with_error` to observe Slack and transport errors without recovering from them:
325+
326+
```ruby
327+
observed_client = client.with_error do |error|
328+
logger.warn(
329+
message: error.message,
330+
errors: error.respond_to?(:errors) && error.response ? error.errors : nil,
331+
response_metadata: error.respond_to?(:response_metadata) && error.response ? error.response_metadata : nil
332+
)
333+
end
334+
observed_client.auth_test
335+
```
336+
337+
Error callbacks receive the exception and then it is re-raised unchanged. They run for every failed HTTP attempt, including failures that pagination subsequently retries. Successful calls and exceptions from request or response callbacks do not trigger them. If an error callback raises, its exception propagates instead and later callbacks are not invoked. As with other callbacks, these methods return a configured copy and can be chained.
338+
324339
You can control what proxy options are used by modifying the `http_proxy` environment variable per [Net::HTTP's documentation](https://docs.ruby-lang.org/en/2.0.0/Net/HTTP.html#class-Net::HTTP-label-Proxies).
325340

326341
Note that Docker on OSX seems to incorrectly set the proxy, causing `Faraday::ConnectionFailed, ERROR -- : Failed to open TCP connection to : (getaddrinfo: Name or service not known)`. You might need to manually unset `http_proxy` in that case, eg. `http_proxy="" bundle exec ruby ./my_bot.rb`.

‎lib/slack/web/client.rb‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ def with_request(&block)
3131
with_callback(:@request_callbacks, block)
3232
end
3333

34+
def with_error(&block)
35+
raise ArgumentError, 'error callback block is required' unless block
36+
37+
with_callback(:@error_callbacks, block)
38+
end
39+
3440
class << self
3541
def configure
3642
block_given? ? yield(Config) : Config

‎lib/slack/web/faraday/request.rb‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def request(method, path, options)
2929

3030
def perform_request(method, path, options)
3131
configuring_request = false
32-
connection.send(method) do |request|
32+
perform_http_request(method, -> { configuring_request }) do |request|
3333
case method
3434
when :get, :delete
3535
request.url(path, options)
@@ -45,9 +45,18 @@ def perform_request(method, path, options)
4545
Array(@request_callbacks).each { |callback| callback.call(request) }
4646
configuring_request = false
4747
end
48-
rescue ::Faraday::ParsingError => e
48+
rescue ::Faraday::Error => e
4949
raise if configuring_request
5050

51+
Array(@error_callbacks).each { |callback| callback.call(e) }
52+
raise
53+
end
54+
55+
def perform_http_request(method, configuring_request, &block)
56+
connection.send(method, &block)
57+
rescue ::Faraday::ParsingError => e
58+
raise if configuring_request.call
59+
5160
raise Slack::Web::Api::Errors::ParsingError, e.response
5261
end
5362
end
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe Slack::Web::Client do
6+
subject(:client) { described_class.new(token: 'example-token') }
7+
8+
it 'observes Slack errors and re-raises the same exception' do
9+
stub_request(:post, 'https://slack.com/api/auth.test').to_return(
10+
body: '{"ok":false,"error":"invalid_arguments","response_metadata":{"messages":["missing field"]}}'
11+
)
12+
errors = []
13+
observed = client.with_error { |error| errors << error }
14+
expect { observed.auth_test }.to(raise_error do |error|
15+
expect(error).to equal(errors.first)
16+
expect(error.response_metadata.messages).to eq(['missing field'])
17+
expect(error.backtrace.first).to include('raise_error.rb')
18+
end)
19+
expect(errors.size).to eq(1)
20+
end
21+
22+
it 'does not mutate the original client' do
23+
stub_request(:post, 'https://slack.com/api/auth.test').to_timeout
24+
errors = []
25+
client.with_error { |error| errors << error }
26+
expect { client.auth_test }.to raise_error(Slack::Web::Api::Errors::TimeoutError)
27+
expect(errors).to be_empty
28+
end
29+
30+
it 'observes transport errors' do
31+
stub_request(:post, 'https://slack.com/api/auth.test').to_timeout
32+
errors = []
33+
observed = client.with_error { |error| errors << error }
34+
expect { observed.auth_test }.to raise_error(Slack::Web::Api::Errors::TimeoutError)
35+
expect(errors.first).to be_a(Slack::Web::Api::Errors::TimeoutError)
36+
end
37+
38+
it 'observes translated parsing errors' do
39+
stub_request(:post, 'https://slack.com/api/auth.test').to_return(body: 'not json')
40+
errors = []
41+
observed = client.with_error { |error| errors << error }
42+
expect { observed.auth_test }.to(raise_error do |error|
43+
expect(error).to be_a(Slack::Web::Api::Errors::ParsingError)
44+
expect(error).to equal(errors.first)
45+
expect(error.cause).to be_a(Faraday::ParsingError)
46+
end)
47+
end
48+
49+
it 'observes each pagination retry without stopping retries' do
50+
stub_request(:post, 'https://slack.com/api/users.list')
51+
.to_return(status: 429, headers: { 'Retry-After' => '0' }, body: '{"ok":false}')
52+
.then.to_return(body: '{"ok":true,"members":[]}')
53+
errors = []
54+
responses = []
55+
observed = client.with_error { |error| errors << error }
56+
observed = observed.with_response { |response| responses << response }
57+
pages = []
58+
observed.users_list { |page| pages << page }
59+
expect(errors.first).to be_a(Slack::Web::Api::Errors::TooManyRequestsError)
60+
expect(errors.size).to eq(1)
61+
expect(responses.map(&:body)).to eq(pages)
62+
expect(pages.size).to eq(1)
63+
end
64+
65+
it 'registers chained error callbacks in order' do
66+
stub_request(:post, 'https://slack.com/api/auth.test').to_timeout
67+
calls = []
68+
observed = client.with_error { calls << 1 }.with_error { calls << 2 }
69+
expect { observed.auth_test }.to raise_error(Slack::Web::Api::Errors::TimeoutError)
70+
expect(calls).to eq([1, 2])
71+
end
72+
73+
%i[with_request with_response].each do |callback_method|
74+
it "does not observe exceptions from #{callback_method} callbacks" do
75+
stub_request(:post, 'https://slack.com/api/auth.test').to_return(body: '{"ok":true}')
76+
errors = []
77+
observed = client.with_error { |error| errors << error }
78+
callback_error = Faraday::ParsingError.new('callback error')
79+
observed = observed.public_send(callback_method) { raise callback_error }
80+
expect { observed.auth_test }.to(raise_error { |error| expect(error).to equal(callback_error) })
81+
expect(errors).to be_empty
82+
end
83+
end
84+
85+
it 'propagates error callback failures without recursive notification' do
86+
stub_request(:post, 'https://slack.com/api/auth.test').to_timeout
87+
calls = []
88+
observed = client.with_error do
89+
calls << :called
90+
raise ArgumentError, 'observer failed'
91+
end
92+
expect { observed.auth_test }.to raise_error(ArgumentError, 'observer failed')
93+
expect(calls).to eq([:called])
94+
end
95+
96+
it 'requires a block' do
97+
expect { client.with_error }.to raise_error(ArgumentError, 'error callback block is required')
98+
end
99+
end

0 commit comments

Comments
 (0)