From f2cb23d6e90c7ce00f486a02b8c6727a53648057 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:55:49 +0530 Subject: [PATCH 1/2] fix: handle Socket::ResolutionError in browser push notifications (#13957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Linear Ticket https://linear.app/chatwoot/issue/CW-6707/socketresolutionerror-failed-to-open-tcp-connection-to-permanently https://linear.app/chatwoot/issue/CW-6707/socketresolutionerror-failed-to-open-tcp-connection-to-permanently#comment-14e0f9ff ## Description Browser push notifications fail with Socket::ResolutionError when the push subscription endpoint's domain can't be resolved via DNS (e.g., defunct push service, transient DNS failure). This error wasn't handled in handle_browser_push_error, so it fell through to the catch-all else branch and got reported to Sentry on every notification attempt — 1,637 times in the last 7 days. The dead subscription was never cleaned up or the error suppressed, so every subsequent notification for the affected user triggered the same Sentry alert. Added Socket::ResolutionError to the existing transient network error handler alongside Errno::ECONNRESET, Net::OpenTimeout, and Net::ReadTimeout. The error is logged but not reported to Sentry, and the subscription is kept intact in case it's a temporary DNS blip. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Verified that Socket::ResolutionError is a subclass of StandardError and matches the when clause ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules Co-authored-by: Vishnu Narayanan --- app/services/notification/push_notification_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/notification/push_notification_service.rb b/app/services/notification/push_notification_service.rb index 125ad9113..90f835ecb 100644 --- a/app/services/notification/push_notification_service.rb +++ b/app/services/notification/push_notification_service.rb @@ -79,7 +79,7 @@ class Notification::PushNotificationService subscription.destroy! when WebPush::TooManyRequests Rails.logger.warn "WebPush rate limited for #{user.email} on account #{notification.account.id}: #{error.message}" - when Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout + when Errno::ECONNRESET, Net::OpenTimeout, Net::ReadTimeout, Socket::ResolutionError Rails.logger.error "WebPush operation error: #{error.message}" else ChatwootExceptionTracker.new(error, account: notification.account).capture_exception From 4cce7f6ad89a6e3e0d967c0e6c7aae34d67fbef0 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 1 Apr 2026 15:59:12 +0400 Subject: [PATCH 2/2] fix(line): Use non-expiring URLs for image and video messages (#13949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Images and videos sent from Chatwoot to LINE inboxes fail to display on the LINE mobile app — users see expired markers, broken thumbnails, or missing images. This happens because LINE mobile lazy-loads images rather than downloading them immediately, and the ActiveStorage signed URLs expire after 5 minutes. Closes https://linear.app/chatwoot/issue/CW-6696/line-messaging-with-image-or-video-may-not-show-when-client-inactive ## How to reproduce 1. Create a LINE inbox and start a chat from the LINE mobile app 2. Close the LINE mobile app 3. Send an image from Chatwoot to that chat 4. Wait 7-8 minutes (past the 5-minute URL expiration) 5. Open the LINE mobile app — the image is broken/expired ## What changed - **`originalContentUrl`**: switched from `download_url` (signed, 5-min expiry) to `file_url` (permanent redirect-based URL) - **`previewImageUrl`**: switched to `thumb_url` (250px resized thumbnail meeting LINE's 1MB/240x240 recommendation), with fallback to `file_url` for non-image attachments like video 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Sojan Jose --- app/services/line/send_on_line_service.rb | 9 +++++++-- spec/services/line/send_on_line_service_spec.rb | 16 ++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/services/line/send_on_line_service.rb b/app/services/line/send_on_line_service.rb index b0b6d828d..3c9d6cf17 100644 --- a/app/services/line/send_on_line_service.rb +++ b/app/services/line/send_on_line_service.rb @@ -44,10 +44,15 @@ class Line::SendOnLineService < Base::SendOnChannelService # Support only image and video for now, https://developers.line.biz/en/reference/messaging-api/#image-message next unless attachment.file_type == 'image' || attachment.file_type == 'video' + # Use file_url (permanent redirect-based URL) instead of download_url (signed URL that expires in 5 minutes). + # LINE mobile app lazy-loads images and may fetch them well after the message is sent. + original_url = attachment.file_url + preview_url = attachment.thumb_url.presence || original_url + { type: attachment.file_type, - originalContentUrl: attachment.download_url, - previewImageUrl: attachment.download_url + originalContentUrl: original_url, + previewImageUrl: preview_url } end end diff --git a/spec/services/line/send_on_line_service_spec.rb b/spec/services/line/send_on_line_service_spec.rb index a7520b8d8..4451a53b9 100644 --- a/spec/services/line/send_on_line_service_spec.rb +++ b/spec/services/line/send_on_line_service_spec.rb @@ -161,7 +161,9 @@ describe Line::SendOnLineService do it 'sends the message with text and attachments' do attachment = message.attachments.new(account_id: message.account_id, file_type: :image) attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') - expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png} + attachment.save! + expected_original_url_regex = %r{rails/active_storage/blobs/redirect/[a-zA-Z0-9=_\-+]+/avatar\.png} + expected_preview_url_regex = %r{rails/active_storage/representations/redirect/[a-zA-Z0-9=_\-+]+/[a-zA-Z0-9=_\-+]+/avatar\.png} expect(line_client).to receive(:push_message).with( message.conversation.contact_inbox.source_id, @@ -169,8 +171,8 @@ describe Line::SendOnLineService do { type: 'text', text: message.content }, { type: 'image', - originalContentUrl: match(expected_url_regex), - previewImageUrl: match(expected_url_regex) + originalContentUrl: match(expected_original_url_regex), + previewImageUrl: match(expected_preview_url_regex) } ] ) @@ -181,16 +183,18 @@ describe Line::SendOnLineService do it 'sends the message with attachments only' do attachment = message.attachments.new(account_id: message.account_id, file_type: :image) attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png') + attachment.save! message.update!(content: nil) - expected_url_regex = %r{rails/active_storage/disk/[a-zA-Z0-9=_\-+]+/avatar\.png} + expected_original_url_regex = %r{rails/active_storage/blobs/redirect/[a-zA-Z0-9=_\-+]+/avatar\.png} + expected_preview_url_regex = %r{rails/active_storage/representations/redirect/[a-zA-Z0-9=_\-+]+/[a-zA-Z0-9=_\-+]+/avatar\.png} expect(line_client).to receive(:push_message).with( message.conversation.contact_inbox.source_id, [ { type: 'image', - originalContentUrl: match(expected_url_regex), - previewImageUrl: match(expected_url_regex) + originalContentUrl: match(expected_original_url_regex), + previewImageUrl: match(expected_preview_url_regex) } ] )