From eaecc43ca4aad7c09789630f77b696aa3071749e Mon Sep 17 00:00:00 2001 From: Marco Cabral Date: Tue, 11 Aug 2026 22:34:21 -0300 Subject: [PATCH] fix: search returning 500 when a conversation has no messages (#15328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description `SearchService#filter_conversations` matches conversations on the display id and on the contact name, email, phone number and identifier. It never looks at message content, so a conversation with no messages is a valid result whenever its contact matches. The search views did not account for that. They rendered `conversation.messages.try(:first)`, which is `nil` for such a conversation, and `api/v1/models/_message` calls `message.id` on it: ``` ActionView::Template::Error (undefined method 'id' for nil): 1: json.id message.id app/views/api/v1/models/_message.json.jbuilder:1 app/views/api/v1/accounts/search/_message.json.jbuilder:1 app/views/api/v1/accounts/search/conversations.json.jbuilder:8 ``` A single conversation without messages is enough to turn the whole search request into a 500 for that query, so the agent loses conversation search entirely until that conversation gets a message. Both views that render a conversation search result were affected, so this applies to `GET /search/conversations` and to the combined `GET /search`. The fix guards the message partial the same way the neighbouring `contact`, `inbox` and `agent` partials in those same views are already guarded. When there is no message the key is rendered as an empty object, which is what already happens for a missing contact, inbox or assignee. **How to reproduce** 1. Create a conversation without any message (for example via `POST /api/v1/accounts/{id}/conversations` without a `message`). 2. Search for the contact's name or phone number: `GET /api/v1/accounts/{id}/search/conversations?q=`. 3. The request returns 500. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Added one spec per affected endpoint in `spec/controllers/api/v1/accounts/search_controller_spec.rb`, each creating a conversation with no messages whose contact matches the query and asserting that it is returned. Both fail with a 500 before the change. Also reproduced manually on a running instance: searching a contact that had a conversation with no messages returned 500, and returns 200 with `"message": {}` after the change. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] Any dependent changes have been merged and published in downstream modules --- Related: #15289 documents the atomic `POST /conversations` with an inline `message`, which avoids creating conversations without messages in the first place. This fix is independent of it — it protects the search regardless of how the conversation ended up without messages (created by an agent before replying, by an API integration, by campaign tooling, or by a flow that did not complete). Co-authored-by: Sojan Jose --- .../_conversation_search_result.json.jbuilder | 3 +- .../search/conversations.json.jbuilder | 3 +- .../api/v1/accounts/search_controller_spec.rb | 34 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/views/api/v1/accounts/search/_conversation_search_result.json.jbuilder b/app/views/api/v1/accounts/search/_conversation_search_result.json.jbuilder index a0b7e0203..bb5782b4c 100644 --- a/app/views/api/v1/accounts/search/_conversation_search_result.json.jbuilder +++ b/app/views/api/v1/accounts/search/_conversation_search_result.json.jbuilder @@ -2,7 +2,8 @@ json.id conversation.display_id json.account_id conversation.account_id json.created_at conversation.created_at.to_i json.message do - json.partial! 'message', formats: [:json], message: conversation.messages.try(:first) + first_message = conversation.messages.first + json.partial! 'message', formats: [:json], message: first_message if first_message.present? end json.contact do json.partial! 'contact', formats: [:json], contact: conversation.contact if conversation.try(:contact).present? diff --git a/app/views/api/v1/accounts/search/conversations.json.jbuilder b/app/views/api/v1/accounts/search/conversations.json.jbuilder index 70d48a89e..b95ffd03f 100644 --- a/app/views/api/v1/accounts/search/conversations.json.jbuilder +++ b/app/views/api/v1/accounts/search/conversations.json.jbuilder @@ -5,7 +5,8 @@ json.payload do json.account_id conversation.account_id json.created_at conversation.created_at.to_i json.message do - json.partial! 'message', formats: [:json], message: conversation.messages.try(:first) + first_message = conversation.messages.first + json.partial! 'message', formats: [:json], message: first_message if first_message.present? end json.contact do json.partial! 'contact', formats: [:json], contact: conversation.contact if conversation.try(:contact).present? diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb index 9b7b0b2f1..a6c32245a 100644 --- a/spec/controllers/api/v1/accounts/search_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/search_controller_spec.rb @@ -44,6 +44,23 @@ RSpec.describe 'Search', type: :request do expect(response_data[:payload][:contacts].length).to eq 1 expect(response_data[:payload][:articles].length).to eq 1 end + + it 'returns matching conversations that have no messages' do + empty_contact = create(:contact, name: 'test empty', account: account) + empty_conversation = create(:conversation, account: account, contact: empty_contact) + create(:inbox_member, user: agent, inbox: empty_conversation.inbox) + + get "/api/v1/accounts/#{account.id}/search", + headers: agent.create_new_auth_token, + params: { q: 'test' }, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body, symbolize_names: true) + + conversation_ids = response_data[:payload][:conversations].pluck(:id) + expect(conversation_ids).to include(empty_conversation.display_id) + end end end @@ -169,6 +186,23 @@ RSpec.describe 'Search', type: :request do expect(response_data[:payload][:conversations].length).to eq 1 end + it 'returns matching conversations that have no messages' do + empty_contact = create(:contact, name: 'test empty', account: account) + empty_conversation = create(:conversation, account: account, contact: empty_contact) + create(:inbox_member, user: agent, inbox: empty_conversation.inbox) + + get "/api/v1/accounts/#{account.id}/search/conversations", + headers: agent.create_new_auth_token, + params: { q: 'test' }, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body, symbolize_names: true) + + conversation_ids = response_data[:payload][:conversations].pluck(:id) + expect(conversation_ids).to include(empty_conversation.display_id) + end + context 'with advanced_search feature enabled', :opensearch do before do account.enable_features!('advanced_search')