Message pagination now constrains client-provided cursors to the PostgreSQL integer range used by `messages.id`, preventing oversized values from raising database errors while preserving before/after pagination semantics. ## Closes - [CW-7922](https://linear.app/chatwoot/issue/CW-7922/harden-backend-paths-causing-production-sentry-errors) - [Sentry 7663397140](https://chatwoot-p3.sentry.io/issues/7663397140/) - [Sentry 7663397546](https://chatwoot-p3.sentry.io/issues/7663397546/) - [Sentry 7663464772](https://chatwoot-p3.sentry.io/issues/7663464772/) ## How to reproduce Request conversation messages with an extremely large `before` or `after` cursor, such as `4611686018427387903`. PostgreSQL previously rejected the value as outside the range for the integer `messages.id` column. ## What changed - Normalize message cursors before they reach the query. - Clamp them to the valid signed 32-bit integer ID range. - Cover oversized `before` and `after` cursors with finder specs.
70 lines
1.9 KiB
Ruby
70 lines
1.9 KiB
Ruby
class MessageFinder
|
|
MESSAGE_ID_MAX = 2_147_483_647
|
|
|
|
def initialize(conversation, params)
|
|
@conversation = conversation
|
|
@params = params
|
|
end
|
|
|
|
def perform
|
|
current_messages
|
|
end
|
|
|
|
private
|
|
|
|
def conversation_messages
|
|
@conversation.messages.includes(:attachments, :sender, sender: { avatar_attachment: [:blob] })
|
|
end
|
|
|
|
def messages
|
|
return conversation_messages if @params[:filter_internal_messages].blank?
|
|
|
|
conversation_messages.where.not('private = ? OR message_type = ?', true, 2)
|
|
end
|
|
|
|
def current_messages
|
|
return messages.none if oversized_message_id?(@params[:after])
|
|
|
|
if @params[:after].present? && @params[:before].present?
|
|
messages_between(normalized_message_id(@params[:after]), @params[:before].to_i)
|
|
elsif @params[:before].present?
|
|
messages_before(@params[:before].to_i)
|
|
elsif @params[:after].present?
|
|
messages_after(normalized_message_id(@params[:after]))
|
|
else
|
|
messages_latest
|
|
end
|
|
end
|
|
|
|
def messages_after(after_id)
|
|
messages.reorder('created_at asc').where('id > ?', after_id).limit(100)
|
|
end
|
|
|
|
def messages_before(before_id)
|
|
return messages_latest if oversized_message_id?(before_id)
|
|
|
|
before_id = normalized_message_id(before_id)
|
|
messages.reorder('created_at desc').where('id < ?', before_id).limit(20).reverse
|
|
end
|
|
|
|
def messages_between(after_id, before_id)
|
|
message_scope = messages.reorder('created_at asc').where('id >= ?', after_id)
|
|
message_scope = message_scope.where('id < ?', normalized_message_id(before_id)) unless oversized_message_id?(before_id)
|
|
message_scope.limit(1000)
|
|
end
|
|
|
|
def messages_latest
|
|
messages.reorder('created_at desc').limit(20).reverse
|
|
end
|
|
|
|
def normalized_message_id(value)
|
|
value.to_i.clamp(0, MESSAGE_ID_MAX)
|
|
end
|
|
|
|
def oversized_message_id?(value)
|
|
value.to_i > MESSAGE_ID_MAX
|
|
end
|
|
end
|
|
|
|
MessageFinder.prepend_mod_with('MessageFinder')
|