From 4cce7f6ad89a6e3e0d967c0e6c7aae34d67fbef0 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Wed, 1 Apr 2026 15:59:12 +0400 Subject: [PATCH] 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) } ] )