Files
leadchat/app/services/twilio/delivery_status_service.rb
Sojan Jose 6b7180d051 fix(twilio): prevent dead jobs on missing channel lookup (#13522)
## Why
We observed `Webhooks::TwilioEventsJob` failures ending up in Sidekiq
dead jobs when Twilio callback payloads could not be mapped to a
`Channel::TwilioSms` record. In this scenario, channel lookup raised
`ActiveRecord::RecordNotFound`, which caused retries and eventual dead
jobs instead of a graceful drop.

Related Sentry issue/search:
-
https://chatwoot-p3.sentry.io/issues/?project=6382945&query=Webhooks%3A%3ATwilioEventsJob%20ActiveRecord%3A%3ARecordNotFound

## What changed
This PR keeps the existing lookup flow but makes it non-raising:
- `app/services/twilio/incoming_message_service.rb`
  - `find_by!` -> `find_by` for account SID + phone lookup
  - Added warning log when channel lookup misses
- `app/services/twilio/delivery_status_service.rb`
  - `find_by!` -> `find_by` for account SID + phone lookup
  - Added warning log when channel lookup misses

## Reproduction
Configure a Twilio webhook callback that reaches Chatwoot but does not
match an existing Twilio channel lookup path. Before this change, the
job raises `RecordNotFound` and can end up in dead jobs after retries.
After this change, the job logs the miss and exits safely.

## Testing
- `bundle exec rspec
spec/services/twilio/incoming_message_service_spec.rb
spec/services/twilio/delivery_status_service_spec.rb`
- `bundle exec rubocop app/services/twilio/incoming_message_service.rb
app/services/twilio/delivery_status_service.rb`
2026-02-13 14:06:12 -08:00

72 lines
2.1 KiB
Ruby

class Twilio::DeliveryStatusService
pattr_initialize [:params!]
# Reference: https://www.twilio.com/docs/messaging/api/message-resource#message-status-values
def perform
return if twilio_channel.blank?
return unless supported_status?
process_statuses if message.present?
end
private
def process_statuses
@message.status = status
@message.external_error = external_error if error_occurred?
@message.save!
end
def supported_status?
%w[sent delivered read failed undelivered].include?(params[:MessageStatus])
end
def status
params[:MessageStatus] == 'undelivered' ? 'failed' : params[:MessageStatus]
end
def external_error
return nil unless error_occurred?
error_message = params[:ErrorMessage].presence
error_code = params[:ErrorCode]
if error_message.present?
"#{error_code} - #{error_message}"
elsif error_code.present?
I18n.t('conversations.messages.delivery_status.error_code', error_code: error_code)
end
end
def error_occurred?
params[:ErrorCode].present? && %w[failed undelivered].include?(params[:MessageStatus])
end
def twilio_channel
@twilio_channel ||= if params[:MessagingServiceSid].present?
::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid])
elsif params[:AccountSid].present? && params[:From].present?
::Channel::TwilioSms.find_by(account_sid: params[:AccountSid], phone_number: params[:From])
end
log_channel_not_found if @twilio_channel.blank?
@twilio_channel
end
def message
return unless params[:MessageSid]
@message ||= twilio_channel.inbox.messages.find_by(source_id: params[:MessageSid])
end
def log_channel_not_found
Rails.logger.warn(
'[TWILIO] Delivery status channel lookup failed ' \
"account_sid=#{params[:AccountSid]} " \
"from=#{params[:From]} " \
"messaging_service_sid=#{params[:MessagingServiceSid]} " \
"message_sid=#{params[:MessageSid]}"
)
end
end