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:
@@ -37,6 +37,7 @@ class Attachment < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
belongs_to :message
|
belongs_to :message
|
||||||
has_one_attached :file
|
has_one_attached :file
|
||||||
|
before_save :set_extension
|
||||||
validate :acceptable_file
|
validate :acceptable_file
|
||||||
validates :external_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
|
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,
|
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
|
def file_metadata
|
||||||
metadata = {
|
metadata = {
|
||||||
extension: extension,
|
extension: extension,
|
||||||
|
content_type: file.content_type,
|
||||||
data_url: file_url,
|
data_url: file_url,
|
||||||
thumb_url: thumb_url,
|
thumb_url: thumb_url,
|
||||||
file_size: file.byte_size,
|
file_size: file.byte_size,
|
||||||
@@ -154,6 +156,13 @@ class Attachment < ApplicationRecord
|
|||||||
}
|
}
|
||||||
end
|
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?
|
def should_validate_file?
|
||||||
return unless file.attached?
|
return unless file.attached?
|
||||||
# we are only limiting attachment types in case of website widget
|
# we are only limiting attachment types in case of website widget
|
||||||
|
|||||||
@@ -187,6 +187,44 @@ RSpec.describe Attachment do
|
|||||||
end
|
end
|
||||||
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
|
describe 'file size validation' do
|
||||||
let(:attachment) { message.attachments.new(account_id: message.account_id, file_type: :image) }
|
let(:attachment) { message.attachments.new(account_id: message.account_id, file_type: :image) }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user