fix: Populate extension and include content_type in attachment webhook payload (#13945)

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) <noreply@anthropic.com>
This commit is contained in:
Muhsin Keloth
2026-04-02 11:13:11 +04:00
committed by GitHub
parent 8daf6cf6cb
commit d83beb2148
2 changed files with 47 additions and 0 deletions

View File

@@ -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

View File

@@ -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) }