Files
moreminimore-chat/app/finders/message_finder.rb
Sony Mathew b3a1dbca81 fix: oversized message cursors (#15400)
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.
2026-08-13 17:34:05 +05:30

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')