feat(webhook): retry webhook job on any error (#180)

This commit is contained in:
Gabriel Jablonski 2026-01-02 23:59:08 -03:00 committed by GitHub
parent 3c2d535de2
commit 8655e5b025
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 10 additions and 117 deletions

View File

@ -11,12 +11,9 @@ class Webhooks::Trigger
def execute
perform_request
rescue RestClient::NotFound
Rails.logger.warn "Webhook returned 404: #{@url}"
raise CustomExceptions::Webhook::RetriableError, "Webhook endpoint not found: #{@url}"
rescue StandardError => e
Webhooks::ErrorHandler.perform(@payload, @webhook_type, e)
Rails.logger.warn "Exception: Invalid webhook URL #{@url} : #{e.message}"
Rails.logger.warn "Webhook request failed for #{@url}: #{e.message}"
raise CustomExceptions::Webhook::RetriableError, "Webhook request failed: #{e.message}"
end
private

View File

@ -41,7 +41,7 @@ describe Webhooks::Trigger do
trigger.execute(url, payload, webhook_type)
end
it 'updates message status if webhook fails for message-created event' do
it 'raises RetriableError when webhook fails' do
payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id }
expect(RestClient::Request).to receive(:execute)
@ -53,88 +53,18 @@ describe Webhooks::Trigger do
timeout: webhook_timeout
).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once
expect { trigger.execute(url, payload, webhook_type) }.to change { message.reload.status }.from('sent').to('failed')
expect { trigger.execute(url, payload, webhook_type) }.to raise_error(CustomExceptions::Webhook::RetriableError)
end
it 'updates message status if webhook fails for message-updated event' do
payload = { event: 'message_updated', conversation: { id: conversation.id }, id: message.id }
it 'does not call ErrorHandler directly (deferred to job discard)' do
payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id }
expect(RestClient::Request).to receive(:execute)
.with(
method: :post,
url: url,
payload: payload.to_json,
headers: { content_type: :json, accept: :json },
timeout: webhook_timeout
).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once
expect { trigger.execute(url, payload, webhook_type) }.to change { message.reload.status }.from('sent').to('failed')
.and_raise(RestClient::ExceptionWithResponse.new('error', 500))
expect(Webhooks::ErrorHandler).not_to receive(:perform)
expect { trigger.execute(url, payload, webhook_type) }.to raise_error(CustomExceptions::Webhook::RetriableError)
end
context 'when webhook type is agent bot' do
let(:webhook_type) { :agent_bot_webhook }
it 'reopens conversation and enqueues activity message if pending' do
conversation.update!(status: :pending)
payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id }
expect(RestClient::Request).to receive(:execute)
.with(
method: :post,
url: url,
payload: payload.to_json,
headers: { content_type: :json, accept: :json },
timeout: webhook_timeout
).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once
expect do
perform_enqueued_jobs do
trigger.execute(url, payload, webhook_type)
end
end.not_to(change { message.reload.status })
expect(conversation.reload.status).to eq('open')
activity_message = conversation.reload.messages.order(:created_at).last
expect(activity_message.message_type).to eq('activity')
expect(activity_message.content).to eq(agent_bot_error_content)
end
it 'does not change message status or enqueue activity when conversation is not pending' do
payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id }
expect(RestClient::Request).to receive(:execute)
.with(
method: :post,
url: url,
payload: payload.to_json,
headers: { content_type: :json, accept: :json },
timeout: webhook_timeout
).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once
expect do
trigger.execute(url, payload, webhook_type)
end.not_to(change { message.reload.status })
expect(Conversations::ActivityMessageJob).not_to have_been_enqueued
expect(conversation.reload.status).to eq('open')
end
end
end
it 'does not update message status if webhook fails for other events' do
payload = { event: 'conversation_created', conversation: { id: conversation.id }, id: message.id }
expect(RestClient::Request).to receive(:execute)
.with(
method: :post,
url: url,
payload: payload.to_json,
headers: { content_type: :json, accept: :json },
timeout: webhook_timeout
).and_raise(RestClient::ExceptionWithResponse.new('error', 500)).once
expect { trigger.execute(url, payload, webhook_type) }.not_to(change { message.reload.status })
end
context 'when webhook timeout configuration is blank' do
@ -174,38 +104,4 @@ describe Webhooks::Trigger do
trigger.execute(url, payload, webhook_type)
end
end
context 'when webhook returns 404' do
it 'raises CustomExceptions::Webhook::RetriableError' do
payload = { hello: :hello }
expect(RestClient::Request).to receive(:execute)
.with(
method: :post,
url: url,
payload: payload.to_json,
headers: { content_type: :json, accept: :json },
timeout: webhook_timeout
).and_raise(RestClient::NotFound.new)
expect { trigger.execute(url, payload, webhook_type) }.to raise_error(CustomExceptions::Webhook::RetriableError)
end
it 'does not call handle_error for 404 responses' do
payload = { event: 'message_created', conversation: { id: conversation.id }, id: message.id }
expect(RestClient::Request).to receive(:execute)
.with(
method: :post,
url: url,
payload: payload.to_json,
headers: { content_type: :json, accept: :json },
timeout: webhook_timeout
).and_raise(RestClient::NotFound.new)
expect(Messages::StatusUpdateService).not_to receive(:new)
expect { trigger.execute(url, payload, webhook_type) }.to raise_error(CustomExceptions::Webhook::RetriableError)
expect(message.reload.status).to eq('sent')
end
end
end