Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add response_metadata to Web API errors #311

Merged
merged 12 commits into from
Mar 10, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
### 0.14.6 (Next)

* [#305](https://github.com/slack-ruby/slack-ruby-client/pull/305): Added `admin.inviteRequests.approve`, `admin.inviteRequests.deny`, `admin.inviteRequests.list`, `admin.inviteRequests.approved.list`, `admin.inviteRequests.denied.list`, `admin.teams.create`, `admin.teams.list`, `admin.teams.admins.list`, `admin.teams.owners.list`, `admin.teams.settings`, `admin.teams.settings.setIcon`, `admin.teams.settings.setName`, `admin.teams.settings.setDescription`, `admin.users.assign`, `admin.users.invite`, `admin.users.remove`, `admin.users.setAdmin`, `admin.users.setOwner` and `admin.users.setRegular` endpoints - [@manuelmeurer](https://github.com/manuelmeurer).
* [#311](https://github.com/slack-ruby/slack-ruby-client/pull/311): Added `verbose_errors` option for web client - [@jmanian](https://github.com/jmanian).
* Your contribution here.

### 0.14.5 (2019/12/23)
Expand Down
73 changes: 41 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ A Ruby client for the Slack [Web](https://api.slack.com/web), [RealTime Messagin
- [Using the Legacy API Token](#using-the-legacy-api-token)
- [Global Settings](#global-settings)
- [Web Client](#web-client)
- [Test Auth](#test-auth)
- [Send Messages](#send-messages)
- [List Channels](#list-channels)
- [Upload a File](#upload-a-file)
- [Get Channel Info](#get-channel-info)
- [Get User Info](#get-user-info)
- [Search for a User](#search-for-a-user)
- [Other](#other)
- [Web Client Examples](#web-client-examples)
- [Test Auth](#test-auth)
- [Send Messages](#send-messages)
- [List Channels](#list-channels)
- [Upload a File](#upload-a-file)
- [Get Channel Info](#get-channel-info)
- [Get User Info](#get-user-info)
- [Search for a User](#search-for-a-user)
- [Other](#other)
- [Web Client Options](#web-client-options)
- [Pagination Support](#pagination-support)
- [Error Handling](#error-handling)
Expand Down Expand Up @@ -122,14 +123,18 @@ logger | An optional logger, defaults to `::Logger.new(STDOUT)` at `Logger

The Slack Web API allows you to build applications that interact with Slack.

#### Test Auth
#### Web Client Examples

Here are some examples of how to use the web client with the Web API.

##### Test Auth

```ruby
client = Slack::Web::Client.new
client.auth_test
```

#### Send Messages
##### Send Messages

Send messages with [chat_PostMessage](https://api.slack.com/methods/chat.postMessage).

Expand All @@ -141,7 +146,7 @@ See a fully working example in [examples/hi_web](examples/hi_web/hi.rb).

![](examples/hi_web/hi.gif)

#### List Channels
##### List Channels

List channels with [channels_list](https://api.slack.com/methods/channels.list).

Expand All @@ -151,7 +156,7 @@ channels = client.channels_list.channels
general_channel = channels.detect { |c| c.name == 'general' }
```

#### Upload a File
##### Upload a File

Upload a file with [files_upload](https://api.slack.com/methods/files.upload).

Expand All @@ -166,7 +171,7 @@ client.files_upload(
)
```

### Get Channel Info
##### Get Channel Info

You can use a channel ID or name (prefixed with `#`) in all functions that take a `:channel` argument. Lookup by name is not supported by the Slack API and the `channels_id` method called invokes `channels_list` in order to locate the channel ID.

Expand All @@ -178,7 +183,7 @@ client.channels_info(channel: 'C04KB5X4D') # calls channels_info
client.channels_info(channel: '#general') # calls channels_list followed by channels_info
```

### Get User Info
##### Get User Info

You can use a user ID or name (prefixed with `@`) in all functions that take a `:user` argument. Lookup by name is not supported by the Slack API and the `users_id` method called invokes `users_list` in order to locate the user ID.

Expand All @@ -190,47 +195,49 @@ client.users_info(user: 'U092BDCLV') # calls users_info
client.users_info(user: '@dblock') # calls users_list followed by users_info
```

### Search for a User
##### Search for a User

Constructs an in-memory index of users and searches it. If you want to use this functionality, add the [picky](https://github.com/floere/picky) gem to your project's Gemfile.

```ruby
client.users_search(user: 'dblock')
```

#### Other
##### Other

Refer to the [Slack Web API Method Reference](https://api.slack.com/methods) for the list of all available functions.

#### Web Client Options

You can configure the Web client either globally or via the initializer.
You can configure the Web client globally:

```ruby
Slack::Web::Client.configure do |config|
config.user_agent = 'Slack Ruby Client/1.0'
end
```

Or you can configure most settings (see table below) when you initialize a client:
```ruby
client = Slack::Web::Client.new(user_agent: 'Slack Ruby Client/1.0')
```

The following settings are supported.
The following settings are supported. Certain options can only be configured globally.

setting | description
--------------------|-------------------------------------------------------------------------------------------------
token | Slack API token.
user_agent | User-agent, defaults to _Slack Ruby Client/version_.
proxy | Optional HTTP proxy.
ca_path | Optional SSL certificates path.
ca_file | Optional SSL certificates file.
endpoint | Slack endpoint, default is _https://slack.com/api_.
logger | Optional `Logger` instance that logs HTTP requests.
timeout | Optional open/read timeout in seconds.
open_timeout | Optional connection open timeout in seconds.
default_page_size | Optional page size for paginated requests, default is _100_.
default_max_retries | Optional number of retries for paginated requests, default is _100_.
setting | global only | description
--------------------|-------------|-------------------------------------------------------------------------------------------------
token | | Slack API token.
user_agent | | User-agent, defaults to _Slack Ruby Client/version_.
proxy | | Optional HTTP proxy.
ca_path | | Optional SSL certificates path.
ca_file | | Optional SSL certificates file.
endpoint | | Slack endpoint, default is _https://slack.com/api_.
logger | | Optional `Logger` instance that logs HTTP requests.
verbose_errors | yes | Adds `response_metadata` into the [error message](#error-handling); default is false.
timeout | | Optional open/read timeout in seconds.
open_timeout | | Optional connection open timeout in seconds.
default_page_size | | Optional page size for paginated requests, default is _100_.
default_max_retries | | Optional number of retries for paginated requests, default is _100_.

You can also pass request options, including `timeout` and `open_timeout` into individual calls.

Expand Down Expand Up @@ -270,7 +277,9 @@ all_members # many thousands of team members retrieved 10 at a time

#### Error Handling

If a request fails, a `Slack::Web::Api::Errors::SlackError` will be raised. The error message contains the error code. In case of multiple errors, the error codes are separated by commas. The original response is also accessible using the `response` attribute.
If a request fails, a `Slack::Web::Api::Errors::SlackError` will be raised. By default, the error message contains the error code. In case of multiple errors, the error codes are separated by commas. The original response is also accessible using the `response` attribute.

The `response_metadata` is accessible with `slack_error.response_metadata`, and the error code is accessible with `slack_error.error`, as well as via `message`. If you set the [`verbose_errors` setting](#web-client-options) then the `response_metadata` will be included in the error `message` as JSON, but `slack_error.error` will continue to be the error code alone.

If you exceed [Slack’s rate limits](https://api.slack.com/docs/rate-limits), a `Slack::Web::Api::Errors::TooManyRequestsError` will be raised instead.

Expand Down
12 changes: 12 additions & 0 deletions lib/slack/web/api/errors/slack_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ def initialize(message, response = nil)
super message
@response = response
end

def error
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sort of a horrible name. I used it because it's the name of the field in the response body, but I'm open to changing it. One alternative is error_code (and error_codes below).

response.body.error
end

def errors
response.body.errors
end

def response_metadata
response.body.response_metadata
end
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/slack/web/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Client

def initialize(options = {})
Slack::Web::Config::ATTRIBUTES.each do |key|
send("#{key}=", options[key] || Slack::Web.config.send(key))
send("#{key}=", options.fetch(key, Slack::Web.config.send(key)))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turned out to not be directly relevant, but I noticed that if there was a truthy value in the config that it could not be overridden with a falsey value.

end
@logger ||= Slack::Config.logger || Slack::Logger.default
@token ||= Slack.config.token
Expand Down
8 changes: 8 additions & 0 deletions lib/slack/web/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Web
module Config
extend self

# Attributes that can be set in client initializer
ATTRIBUTES = %i[
proxy
user_agent
Expand All @@ -18,7 +19,13 @@ module Config
default_max_retries
].freeze

# Attributes that can not be set in client initializer
GLOBAL_ATTRIBUTES = %i[
verbose_errors
].freeze

attr_accessor(*Config::ATTRIBUTES)
attr_accessor(*Config::GLOBAL_ATTRIBUTES)

def reset
self.endpoint = 'https://slack.com/api/'
Expand All @@ -28,6 +35,7 @@ def reset
self.token = nil
self.proxy = nil
self.logger = nil
self.verbose_errors = nil
self.timeout = nil
self.open_timeout = nil
self.default_page_size = 100
Expand Down
5 changes: 5 additions & 0 deletions lib/slack/web/faraday/response/raise_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ def on_complete(env)

error_message =
body['error'] || body['errors'].map { |message| message['error'] }.join(',')

if Slack::Web.config.verbose_errors && body['response_metadata']
jmanian marked this conversation as resolved.
Show resolved Hide resolved
error_message = "#{error_message}; #{body['response_metadata'].to_json}"
end

raise Slack::Web::Api::Errors::SlackError.new(error_message, env.response)
end
end
Expand Down
76 changes: 76 additions & 0 deletions spec/fixtures/slack/web/views_open_error.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions spec/slack/web/api/errors/slack_error_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,21 @@
RSpec.describe Slack::Web::Api::Errors::SlackError do
let(:client) { Slack::Web::Client.new }

it 'provides access to the response object', vcr: { cassette_name: 'web/auth_test_error' } do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also leave that old test in-place and add checks for its .message, .error, .response_metadata, it's a simpler case, but will prevent breaking it in the future.

it 'provides access to useful info', vcr: { cassette_name: 'web/views_open_error' } do
begin
client.auth_test
client.views_open(trigger_id: 'trigger_id', view: {})
raise 'Expected to receive Slack::Web::Api::Errors::SlackError.'
rescue described_class => e
expect(e.response).not_to be_nil
expect(e.response.status).to eq 200
expect(e.error).to eql 'invalid_arguments'
expect(e.response_metadata).to eq(
'messages' => [
"[ERROR] missing required field: title [json-pointer:\/view]",
"[ERROR] missing required field: blocks [json-pointer:\/view]",
"[ERROR] missing required field: type [json-pointer:\/view]"
]
)
end
end
end
39 changes: 38 additions & 1 deletion spec/slack/web/faraday/response/raise_error_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
end

context 'with a single error in the body' do
let(:body) { { 'error' => 'already_in_channel' } }
let(:body) do
{
'ok' => false,
'error' => 'already_in_channel',
'response_metadata' => { 'messages' => [] }
}
end

it 'raises a SlackError with the error message' do
expect { raise_error_obj.on_complete(env) }.to(
Expand All @@ -41,6 +47,7 @@
context 'with multiple errors in the body' do
let(:body) do
{
'ok' => false,
'errors' => [
{ 'error' => 'already_in_channel' },
{ 'error' => 'something_else_terrible' }
Expand All @@ -57,5 +64,35 @@
)
end
end

context 'with verbose error logging' do
before { Slack::Web.config.verbose_errors = true }

context 'with no metadata in the response' do
let(:body) { { 'ok' => false, 'error' => 'already_in_channel' } }

it 'raises a SlackError with the error message' do
expect { raise_error_obj.on_complete(env) }.to(
raise_error(Slack::Web::Api::Errors::SlackError, 'already_in_channel')
)
end
end

context 'with metadata in the response' do
let(:body) do
{
'ok' => false,
'error' => 'already_in_channel',
'response_metadata' => { 'messages' => [] }
}
end

it 'raises a SlackError and includes the metadata in the message' do
expect { raise_error_obj.on_complete(env) }.to(
raise_error(Slack::Web::Api::Errors::SlackError, 'already_in_channel; {"messages":[]}')
)
end
end
end
end
end