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

Update signal-cli-rest-api with more verbose error messages and report them #1696

Merged
merged 5 commits into from
Sep 18, 2023
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
28 changes: 28 additions & 0 deletions app/adapters/signal_adapter/api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module SignalAdapter
class Api
class << self
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess a class with a static method will do. ErrorNotifier is built the same way`.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, don't know why I didn't think of that hehe

def perform_request(request)
uri = request.uri
response = Net::HTTP.start(uri.host, uri.port) do |http|
http.request(request)
end
case response
when Net::HTTPSuccess
yield response if block_given?
else
error_message = JSON.parse(response.body)['error']
exception = SignalAdapter::BadRequestError.new(error_code: response.code, message: error_message)
context = {
code: response.code,
message: response.message,
headers: response.to_hash,
body: error_message
}
ErrorNotifier.report(exception, context: context)
end
end
end
end
end
9 changes: 9 additions & 0 deletions app/adapters/signal_adapter/bad_request_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module SignalAdapter
class BadRequestError < StandardError
def initialize(error_code:, message:)
super("Message was not delivered with error code `#{error_code}` and message `#{message}`")
Copy link
Collaborator

Choose a reason for hiding this comment

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

This might become a little unreadable on Sentry in case of multi-line messages, but let's see..

end
end
end
18 changes: 6 additions & 12 deletions app/adapters/signal_adapter/outbound/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,17 @@ class File < ApplicationJob

def perform(message:)
@message = message
url = URI.parse("#{Setting.signal_cli_rest_api_endpoint}/v2/send")
request = Net::HTTP::Post.new(url.to_s, {
uri = URI.parse("#{Setting.signal_cli_rest_api_endpoint}/v2/send")
request = Net::HTTP::Post.new(uri, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It matters that we pass URI not string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, this is just a copy/paste that survived from code you added d521f28#diff-2e66abeab283cf6d1ae8e78dbcd46410ae0b8eb01361bc44708e1426c6f89f28R31

I don't know why it matters. Maybe, you could further explain

Accept: 'application/json',
'Content-Type': 'application/json'
})
request.body = data.to_json
response = Net::HTTP.start(url.host, url.port) do |http|
http.request(request)
SignalAdapter::Api.perform_request(request) do
# TODO: Do something on success. For example, mark the message as delivered?
# Or should we use deliver receipts as the source of truth.
Rails.logger.debug 'Great!'
end
response.value # may raise exception
rescue Net::HTTPClientException => e
ErrorNotifier.report(e, context: {
code: e.response.code,
message: e.response.message,
headers: e.response.to_hash,
body: e.response.body
})
end

def data
Expand Down
18 changes: 6 additions & 12 deletions app/adapters/signal_adapter/outbound/text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,17 @@ class Text < ApplicationJob
def perform(recipient:, text:)
@recipient = recipient
@text = text
url = URI.parse("#{Setting.signal_cli_rest_api_endpoint}/v2/send")
request = Net::HTTP::Post.new(url.to_s, {
uri = URI.parse("#{Setting.signal_cli_rest_api_endpoint}/v2/send")
request = Net::HTTP::Post.new(uri, {
Accept: 'application/json',
'Content-Type': 'application/json'
})
request.body = data.to_json
response = Net::HTTP.start(url.host, url.port) do |http|
http.request(request)
SignalAdapter::Api.perform_request(request) do
# TODO: Do something on success. For example, mark the message as delivered?
# Or should we use deliver receipts as the source of truth.
Rails.logger.debug 'Great!'
end
response.value # may raise exception
rescue Net::HTTPClientException => e
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you remove the rescue then you will not even get to handle_response if an exception is raised.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok, I see from the comment now that response.value may raise the error and it was also removed, nevermind. But I find the behaviour surprising.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I find the behavior as it exists surprising, to be honest. We call response.value because if it is an unsuccessful response calling .value on it raises an exception, we even explain that the only purpose for line 22 is that it may raise an exception.

I think a better way to handle it is to either check the class of the response, or check its code to determine whether the response was successful, or not.

ErrorNotifier.report(e, context: {
code: e.response.code,
message: e.response.message,
headers: e.response.to_hash,
body: e.response.body
})
end

def data
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ services:
- postgres_data:/var/lib/postgresql/data

signal:
image: bbernhard/signal-cli-rest-api:0.67
image: bbernhard/signal-cli-rest-api:0.119-dev
Copy link
Collaborator

Choose a reason for hiding this comment

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

@mattwr18 Are we going to roll our own image or not?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't think so. There is already a pre-release https://github.com/bbernhard/signal-cli-rest-api/releases/tag/0.68-pre

There, he advises to use this image for testing things out.

environment:
- MODE=native
#- AUTO_RECEIVE_SCHEDULE=0 22 * * * #enable this parameter on demand (see description below)
Expand Down
46 changes: 46 additions & 0 deletions spec/adapters/signal_adapter/api_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

require 'rails_helper'
require 'webmock/rspec'

RSpec.describe SignalAdapter::Api do
let(:api) { described_class }

describe '#perform_request' do
let(:uri) { URI.parse('http://signal:8080/v2/send') }
let(:request) { Net::HTTP::Post.new(uri) }

before do
allow(ErrorNotifier).to receive(:report)
end

describe 'http response code' do
describe '200' do
before(:each) do
stub_request(:post, uri).to_return(status: 200)
end
specify { expect { |block| api.perform_request(request, &block) }.to yield_control }

describe 'ErrorNotifier' do
subject { ErrorNotifier }
before { api.perform_request(request) }
it { should_not have_received(:report) }
end
end

describe '400' do
before(:each) do
stub_request(:post, uri).to_return(status: 400, body: { error: 'Ouch!' }.to_json)
end

specify { expect { |block| api.perform_request(request, &block) }.not_to yield_control }

describe 'ErrorNotifier' do
subject { ErrorNotifier }
before { api.perform_request(request) }
it { should have_received(:report) }
end
end
end
end
end
5 changes: 3 additions & 2 deletions spec/adapters/signal_adapter/outbound/file_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@
end

describe 'on error' do
before(:each) { stub_request(:post, 'http://signal:8080/v2/send').to_return(status: 400) }
let(:error_message) { 'User is not registered' }
before(:each) { stub_request(:post, 'http://signal:8080/v2/send').to_return(status: 400, body: { error: error_message }.to_json) }

it 'reports the error' do
expect(Sentry).to receive(:capture_exception).with(Net::HTTPClientException)
expect(Sentry).to receive(:capture_exception).with(SignalAdapter::BadRequestError.new(error_code: 400, message: error_message))

subject.call
end
Expand Down
5 changes: 3 additions & 2 deletions spec/adapters/signal_adapter/outbound/text_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@
end

describe 'on error' do
before(:each) { stub_request(:post, 'http://signal:8080/v2/send').to_return(status: 400) }
let(:error_message) { 'User is not registered' }
before(:each) { stub_request(:post, 'http://signal:8080/v2/send').to_return(status: 400, body: { error: error_message }.to_json) }

it 'reports the error' do
expect(Sentry).to receive(:capture_exception).with(Net::HTTPClientException)
expect(Sentry).to receive(:capture_exception).with(SignalAdapter::BadRequestError.new(error_code: 400, message: error_message))

subject.call
end
Expand Down