From d83beb2148ee079bdd7701c3f4c6d51638dd7383 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 2 Apr 2026 11:13:11 +0400 Subject: [PATCH] fix: Populate `extension` and include `content_type` in attachment webhook payload (#13945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attachment webhook event payloads (`message_created`) were missing the file extension and content type. The `extension` column existed but was never populated, and `content_type` was not included in the payload at all. ## What changed - Added `before_save :set_extension` callback to extract file extension from the filename when saving an attachment. - Added `content_type` (from ActiveStorage) to the `file_metadata` used in `push_event_data`. ### Before ```json { "extension": null, "data_url": "...", "file_size": 11960 } ``` ### After ```json { "extension": "pdf", "content_type": "application/pdf", "data_url": "...", "file_size": 11960 } ``` ## How to reproduce 1. Send a message with a file attachment (e.g., PDF) via any channel 2. Inspect the `message_created` webhook payload 3. Observe `extension` is `null` and `content_type` is missing 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- app/models/attachment.rb | 9 ++++++++ spec/models/attachment_spec.rb | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/app/models/attachment.rb b/app/models/attachment.rb index a1cc062a0..c6b4e1d80 100644 --- a/app/models/attachment.rb +++ b/app/models/attachment.rb @@ -37,6 +37,7 @@ class Attachment < ApplicationRecord belongs_to :account belongs_to :message has_one_attached :file + before_save :set_extension validate :acceptable_file validates :external_url, length: { maximum: Limits::URL_LENGTH_LIMIT } enum file_type: { :image => 0, :audio => 1, :video => 2, :file => 3, :location => 4, :fallback => 5, :share => 6, :story_mention => 7, @@ -111,6 +112,7 @@ class Attachment < ApplicationRecord def file_metadata metadata = { extension: extension, + content_type: file.content_type, data_url: file_url, thumb_url: thumb_url, file_size: file.byte_size, @@ -154,6 +156,13 @@ class Attachment < ApplicationRecord } end + def set_extension + return unless file.attached? + return if extension.present? + + self.extension = File.extname(file.filename.to_s).delete_prefix('.').presence + end + def should_validate_file? return unless file.attached? # we are only limiting attachment types in case of website widget diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index a17839174..5e1dd2107 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -187,6 +187,44 @@ RSpec.describe Attachment do end end + describe 'set_extension' do + it 'sets extension from filename on save' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :file) + attachment.file.attach(io: StringIO.new('fake pdf'), filename: 'test.pdf', content_type: 'application/pdf') + attachment.save! + + expect(attachment.extension).to eq('pdf') + end + + it 'does not overwrite extension if already set' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :file, extension: 'doc') + attachment.file.attach(io: StringIO.new('fake pdf'), filename: 'test.pdf', content_type: 'application/pdf') + attachment.save! + + expect(attachment.extension).to eq('doc') + end + + it 'handles filenames without extension' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :file) + attachment.file.attach(io: StringIO.new('fake data'), filename: 'README', content_type: 'text/plain') + attachment.save! + + expect(attachment.extension).to be_nil + end + end + + describe 'push_event_data includes extension and content_type' do + it 'returns extension and content_type for file attachments' do + attachment = message.attachments.new(account_id: message.account_id, file_type: :file) + attachment.file.attach(io: StringIO.new('fake pdf'), filename: 'test.pdf', content_type: 'application/pdf') + attachment.save! + + event_data = attachment.push_event_data + expect(event_data[:extension]).to eq('pdf') + expect(event_data[:content_type]).to eq('application/pdf') + end + end + describe 'file size validation' do let(:attachment) { message.attachments.new(account_id: message.account_id, file_type: :image) }