fix(whatsapp): run webhook setup off the request thread to avoid inbox creation timeouts
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
class Channels::Whatsapp::WebhookSetupJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
# Runs Meta's phone-registration and webhook-subscription calls off the request
|
||||
# thread. Inline, these Graph API calls can exceed the 15s Rack::Timeout and, since
|
||||
# RequestTimeoutException bypasses setup_webhooks' rescue, abort inbox creation and
|
||||
# roll it back — leaving the number connected on Meta but no inbox in Chatwoot.
|
||||
def perform(whatsapp_channel)
|
||||
whatsapp_channel.setup_webhooks
|
||||
end
|
||||
end
|
||||
@@ -35,7 +35,7 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
after_create :sync_templates
|
||||
after_update_commit :log_credentials_transfer, if: :saved_change_to_provider_config?
|
||||
before_destroy :teardown_webhooks
|
||||
after_commit :setup_webhooks, on: :create, if: :should_auto_setup_webhooks?
|
||||
after_commit :enqueue_webhook_setup, on: :create, if: :should_auto_setup_webhooks?
|
||||
|
||||
def name
|
||||
'Whatsapp'
|
||||
@@ -120,6 +120,8 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
delegate :media_url, to: :provider_service
|
||||
delegate :api_headers, to: :provider_service
|
||||
|
||||
# Runs synchronously inside Channels::Whatsapp::WebhookSetupJob; the Meta Graph
|
||||
# calls are kept off the request thread so a slow response can't trip Rack::Timeout.
|
||||
def setup_webhooks
|
||||
perform_webhook_setup
|
||||
rescue StandardError => e
|
||||
@@ -127,6 +129,11 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
prompt_reauthorization!
|
||||
end
|
||||
|
||||
# Enqueue on the same channel record so GlobalID resolves it in the job.
|
||||
def enqueue_webhook_setup
|
||||
Channels::Whatsapp::WebhookSetupJob.perform_later(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_webhook_verify_token
|
||||
|
||||
@@ -15,11 +15,12 @@ class Whatsapp::EmbeddedSignupService
|
||||
phone_info = fetch_phone_info(access_token)
|
||||
|
||||
channel = create_or_reauthorize_channel(access_token, phone_info)
|
||||
# NOTE: We call setup_webhooks explicitly here instead of relying on after_commit callback because:
|
||||
# Enqueue webhook setup explicitly instead of relying on the after_commit callback because:
|
||||
# 1. Reauthorization flow updates an existing channel (not a create), so after_commit on: :create won't trigger
|
||||
# 2. We need to run check_channel_health_and_prompt_reauth after webhook setup completes
|
||||
# 3. The channel is marked with source: 'embedded_signup' to skip the after_commit callback
|
||||
channel.setup_webhooks
|
||||
# 2. The channel is marked with source: 'embedded_signup' to skip the after_commit callback
|
||||
# Run it in a job so Meta's slow phone-registration/subscription calls can't trip Rack::Timeout
|
||||
# and roll back the just-created channel.
|
||||
Channels::Whatsapp::WebhookSetupJob.perform_later(channel)
|
||||
# Skip health check during reauthorization — phone numbers in pending provisioning state
|
||||
# (platform_type: NOT_APPLICABLE) would incorrectly trigger a disconnect email right after
|
||||
# a successful reauth. Only run health check for new channel creation.
|
||||
|
||||
@@ -124,26 +124,25 @@ RSpec.describe Channel::Whatsapp do
|
||||
end
|
||||
|
||||
context 'when channel is created through manual setup' do
|
||||
it 'setups webhooks via after_commit callback' do
|
||||
expect(Whatsapp::WebhookSetupService).to receive(:new).and_return(webhook_service)
|
||||
expect(webhook_service).to receive(:perform)
|
||||
|
||||
it 'enqueues webhook setup via after_commit callback' do
|
||||
# Explicitly set source to nil to test manual setup behavior (not embedded_signup)
|
||||
create(:channel_whatsapp,
|
||||
account: account,
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: {
|
||||
'business_account_id' => 'test_waba_id',
|
||||
'api_key' => 'test_access_token',
|
||||
'source' => nil
|
||||
},
|
||||
validate_provider_config: false,
|
||||
sync_templates: false)
|
||||
expect do
|
||||
create(:channel_whatsapp,
|
||||
account: account,
|
||||
provider: 'whatsapp_cloud',
|
||||
provider_config: {
|
||||
'business_account_id' => 'test_waba_id',
|
||||
'api_key' => 'test_access_token',
|
||||
'source' => nil
|
||||
},
|
||||
validate_provider_config: false,
|
||||
sync_templates: false)
|
||||
end.to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is created with different provider' do
|
||||
it 'does not setup webhooks for 360dialog provider' do
|
||||
it 'does not enqueue webhook setup for 360dialog provider' do
|
||||
expect(Whatsapp::WebhookSetupService).not_to receive(:new)
|
||||
|
||||
create(:channel_whatsapp,
|
||||
|
||||
@@ -20,7 +20,10 @@ describe Whatsapp::EmbeddedSignupService do
|
||||
business_name: 'Test Business'
|
||||
}
|
||||
end
|
||||
let(:channel) { instance_double(Channel::Whatsapp) }
|
||||
let(:channel) do
|
||||
create(:channel_whatsapp, account: account, phone_number: '+15550000001',
|
||||
validate_provider_config: false, sync_templates: false)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
@@ -42,9 +45,6 @@ describe Whatsapp::EmbeddedSignupService do
|
||||
.and_return(channel_creation)
|
||||
allow(channel_creation).to receive(:perform).and_return(channel)
|
||||
|
||||
allow(channel).to receive(:setup_webhooks)
|
||||
allow(channel).to receive(:phone_number).and_return('+1234567890')
|
||||
|
||||
health_service = instance_double(Whatsapp::HealthService)
|
||||
allow(Whatsapp::HealthService).to receive(:new).and_return(health_service)
|
||||
allow(health_service).to receive(:fetch_health_status).and_return({
|
||||
@@ -54,11 +54,8 @@ describe Whatsapp::EmbeddedSignupService do
|
||||
})
|
||||
end
|
||||
|
||||
it 'creates channel and sets up webhooks' do
|
||||
expect(channel).to receive(:setup_webhooks)
|
||||
|
||||
result = service.perform
|
||||
expect(result).to eq(channel)
|
||||
it 'creates channel and enqueues webhook setup' do
|
||||
expect { service.perform }.to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob).with(channel)
|
||||
end
|
||||
|
||||
it 'checks health status after channel creation' do
|
||||
@@ -121,27 +118,15 @@ describe Whatsapp::EmbeddedSignupService do
|
||||
expect { service.perform }.to raise_error('Token error')
|
||||
end
|
||||
|
||||
it 'prompts reauthorization when webhook setup fails' do
|
||||
# Create a real channel to test the actual webhook failure behavior
|
||||
# Webhook setup now runs in Channels::Whatsapp::WebhookSetupJob; the channel's own
|
||||
# setup_webhooks rescue marks it for reauthorization when the Meta calls fail.
|
||||
it 'marks the channel for reauthorization when webhook setup fails' do
|
||||
real_channel = create(:channel_whatsapp, account: account, phone_number: '+1234567890',
|
||||
validate_provider_config: false, sync_templates: false)
|
||||
|
||||
# Mock the channel creation to return our real channel
|
||||
channel_creation = instance_double(Whatsapp::ChannelCreationService)
|
||||
allow(Whatsapp::ChannelCreationService).to receive(:new).and_return(channel_creation)
|
||||
allow(channel_creation).to receive(:perform).and_return(real_channel)
|
||||
|
||||
# Mock webhook setup to fail
|
||||
allow(real_channel).to receive(:perform_webhook_setup).and_raise('Webhook setup error')
|
||||
|
||||
# Verify channel is not marked for reauthorization initially
|
||||
expect(real_channel.reauthorization_required?).to be false
|
||||
|
||||
# The service completes successfully even if webhook fails (webhook error is rescued in setup_webhooks)
|
||||
result = service.perform
|
||||
expect(result).to eq(real_channel)
|
||||
|
||||
# Verify the channel is now marked for reauthorization
|
||||
real_channel.setup_webhooks
|
||||
expect(real_channel.reauthorization_required?).to be true
|
||||
end
|
||||
end
|
||||
@@ -162,8 +147,6 @@ describe Whatsapp::EmbeddedSignupService do
|
||||
).and_return(reauth_service)
|
||||
allow(reauth_service).to receive(:perform).with(access_token, phone_info).and_return(channel)
|
||||
|
||||
allow(channel).to receive(:phone_number).and_return('+1234567890')
|
||||
|
||||
health_service = instance_double(Whatsapp::HealthService)
|
||||
allow(Whatsapp::HealthService).to receive(:new).and_return(health_service)
|
||||
allow(health_service).to receive(:fetch_health_status).and_return({
|
||||
@@ -173,12 +156,10 @@ describe Whatsapp::EmbeddedSignupService do
|
||||
})
|
||||
end
|
||||
|
||||
it 'uses ReauthorizationService and sets up webhooks' do
|
||||
it 'uses ReauthorizationService and enqueues webhook setup' do
|
||||
expect(reauth_service).to receive(:perform)
|
||||
expect(channel).to receive(:setup_webhooks)
|
||||
|
||||
result = service_with_inbox.perform
|
||||
expect(result).to eq(channel)
|
||||
expect { service_with_inbox.perform }.to have_enqueued_job(Channels::Whatsapp::WebhookSetupJob).with(channel)
|
||||
end
|
||||
|
||||
context 'with real channel requiring reauthorization' do
|
||||
|
||||
Reference in New Issue
Block a user