From 8438981ef124a8bf2151327fc09807422f02940d Mon Sep 17 00:00:00 2001 From: PluS98 <104105009+Plus98ir@users.noreply.github.com> Date: Fri, 13 Nov 2026 02:01:07 +0300 Subject: [PATCH] fix(hosts): let the Host form clear its SNI (#7095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the last SNI tag in Hosts → Edit Host and saving did nothing: the submit fell back to the hidden legacy `sni` field, so the old name was sent again and the backend rebuilt the pool from it. It came back on the next edit and stayed in every share link built from that host. The tag list is the only SNI input, so the primary SNI is now the first tag or empty. The dynamic ECH helper read the same stale field and follows the list too. Hosts saved with only the legacy field still load, because the form seeds the tag list from it. --- frontend/src/pages/hosts/HostFormModal.tsx | 6 +-- frontend/src/test/host-form-sni.test.tsx | 47 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 frontend/src/test/host-form-sni.test.tsx diff --git a/frontend/src/pages/hosts/HostFormModal.tsx b/frontend/src/pages/hosts/HostFormModal.tsx index 9089d711c..8c6b37b40 100644 --- a/frontend/src/pages/hosts/HostFormModal.tsx +++ b/frontend/src/pages/hosts/HostFormModal.tsx @@ -121,8 +121,7 @@ export default function HostFormModal({ watchedSnis && watchedSnis.filter((s) => typeof s === 'string' && s.trim() !== '').length > 1, ); - const watchedSingleSni = useWatch({ control: methods.control, name: 'sni' }) ?? ''; - const hostSni = (watchedSnis && watchedSnis.length > 0 ? watchedSnis[0] : '') || watchedSingleSni; + const hostSni = watchedSnis?.[0] ?? ''; const hostEchConfig = useWatch({ control: methods.control, name: 'echConfigList' }) ?? ''; const showTls = security === 'tls' || security === 'reality' || security === 'same'; const showTlsExtras = security === 'tls' || security === 'same'; @@ -208,11 +207,10 @@ export default function HostFormModal({ const { enable, ...rest } = values; const isDisabled = !enable; const cleanedSnis = (rest.snis || []).map((s) => s.trim()).filter(Boolean); - const primarySni = cleanedSnis.length > 0 ? cleanedSnis[0] : rest.sni || ''; const payload: BulkAddHostValues = { ...rest, hosts: (rest.hosts || []).filter((h) => h && h.trim() !== ''), - sni: primarySni, + sni: cleanedSnis[0] ?? '', snis: cleanedSnis, sniSharedRandom: cleanedSnis.length > 1 && Boolean(rest.sniSharedRandom), isDisabled, diff --git a/frontend/src/test/host-form-sni.test.tsx b/frontend/src/test/host-form-sni.test.tsx new file mode 100644 index 000000000..5e9420013 --- /dev/null +++ b/frontend/src/test/host-form-sni.test.tsx @@ -0,0 +1,47 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import type { HostRecord } from '@/api/queries/useHostsQuery'; +import HostFormModal from '@/pages/hosts/HostFormModal'; +import { HttpUtil, Msg } from '@/utils'; +import { renderWithProviders } from './test-utils'; + +const host = { + groupId: 'g1', + remark: 'DE WS', + inboundIds: [1], + hosts: ['cdn.example.com'], + security: 'same', + sni: 'dash.example.com', + snis: ['dash.example.com'], +} as HostRecord; + +describe('Host form SNI', () => { + beforeEach(() => { + vi.spyOn(HttpUtil, 'get').mockResolvedValue(new Msg(true, '', [])); + }); + + it('saves an empty SNI after the last SNI tag is removed', async () => { + const save = vi.fn().mockResolvedValue({ success: true }); + renderWithProviders( + {}} + />, + ); + const tag = await screen.findByTitle('dash.example.com'); + const remove = tag.querySelector('.ant-select-selection-item-remove'); + if (!remove) throw new Error('missing SNI tag remove button'); + fireEvent.click(remove); + await waitFor(() => expect(screen.queryByTitle('dash.example.com')).toBeNull()); + + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + await waitFor(() => expect(save).toHaveBeenCalledTimes(1)); + expect(save.mock.calls[0][0]).toMatchObject({ sni: '', snis: [] }); + }); +});