From 5f6bd951b9d81e435c35ec9211bd9ffcedd8d274 Mon Sep 17 00:00:00 2001 From: Renato Ascencio Date: Thu, 14 May 2026 23:41:18 -0600 Subject: [PATCH] fix: portals#create returns 500 when custom_domain is omitted (#14400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description `POST /api/v1/accounts/:account_id/portals` returns a generic 500 (`{"status":500,"error":"Internal Server Error"}`) whenever the request body omits `custom_domain`. Root cause: `parsed_custom_domain` calls `URI.parse(@portal.custom_domain)` and `URI.parse(nil)` raises `URI::InvalidURIError`. Existing callers either had to know to pass `"custom_domain": ""` as a workaround or hit a 500 with no useful diagnostic. This PR guards `parsed_custom_domain` against blank values so the existing fall-through (`else @portal.custom_domain`) applies — equivalent to passing an empty string. It also moves the `process_attached_logo` guard from the helper into the `create` call site so `create` mirrors `update` (`process_attached_logo if params[:blob_id].present?`) and avoids an unnecessary signed-blob lookup on every create that doesn't include a logo. Fixes #14397 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Two new request specs in `spec/controllers/api/v1/accounts/portals_controller_spec.rb` covering the regression: - `creates portal when custom_domain is omitted from request body` — the previously-broken case, now returns 200. - `creates portal when custom_domain is blank` — verifies the existing workaround (`"custom_domain": ""`) still works after the change. Manually verified against `chatwoot/chatwoot:latest` Docker image before the fix (500) and against this branch (200) using the curl repro from the issue. ```bash curl -X POST "https:///api/v1/accounts//portals" \ -H "Content-Type: application/json" \ -H "api_access_token: " \ -d '{"name":"Test Portal","slug":"test-portal","color":"#3b82f6"}' ``` Before: `{"status":500,"error":"Internal Server Error"}` After: `200 OK` with the portal payload. ## 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 - [ ] I have made corresponding changes to the documentation (no doc change needed — controller behaviour, fully backward-compatible) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sojan Jose --- .../api/v1/accounts/portals_controller.rb | 4 ++- .../v1/accounts/portals_controller_spec.rb | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb index 6d3332d08..27c4126a6 100644 --- a/app/controllers/api/v1/accounts/portals_controller.rb +++ b/app/controllers/api/v1/accounts/portals_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController @portal = Current.account.portals.build(portal_params.merge(live_chat_widget_params)) @portal.custom_domain = parsed_custom_domain @portal.save! - process_attached_logo + process_attached_logo if params[:blob_id].present? end def update @@ -98,6 +98,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController end def parsed_custom_domain + return @portal.custom_domain if @portal.custom_domain.blank? + domain = URI.parse(@portal.custom_domain) domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain end diff --git a/spec/controllers/api/v1/accounts/portals_controller_spec.rb b/spec/controllers/api/v1/accounts/portals_controller_spec.rb index 4c45e4f0f..81240c557 100644 --- a/spec/controllers/api/v1/accounts/portals_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/portals_controller_spec.rb @@ -100,6 +100,41 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do expect(json_response['name']).to eql('test_portal') expect(json_response['custom_domain']).to eql('support.chatwoot.dev') end + + it 'creates portal when custom_domain is omitted from request body' do + portal_params = { + portal: { + name: 'test_portal_no_domain', + slug: 'test_kbase_no_domain' + } + } + post "/api/v1/accounts/#{account.id}/portals", + params: portal_params, + headers: admin.create_new_auth_token + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['name']).to eql('test_portal_no_domain') + expect(json_response['custom_domain']).to be_nil + end + + it 'creates portal when custom_domain is blank' do + portal_params = { + portal: { + name: 'test_portal_blank_domain', + slug: 'test_kbase_blank_domain', + custom_domain: '' + } + } + post "/api/v1/accounts/#{account.id}/portals", + params: portal_params, + headers: admin.create_new_auth_token + + expect(response).to have_http_status(:success) + json_response = response.parsed_body + expect(json_response['name']).to eql('test_portal_blank_domain') + expect(json_response['custom_domain']).to be_blank + end end end