From b73ceae081243f7ac5d7b83656e15e9ad11b2f40 Mon Sep 17 00:00:00 2001
From: dawn <93917549+dawNotPoi@users.noreply.github.com>
Date: Sun, 23 Aug 2026 02:11:51 +0800
Subject: [PATCH] fix(frontend): refresh subscription settings after save
(#6287)
The derived defaults query is cached indefinitely, so subscription links kept using the old path after settings saves. Invalidate it only after successful saves so inbounds and clients refetch generated subscription URLs.
---
frontend/src/api/queries/useAllSettings.ts | 1 +
frontend/src/test/use-all-settings.test.tsx | 61 +++++++++++++++++++++
2 files changed, 62 insertions(+)
diff --git a/frontend/src/api/queries/useAllSettings.ts b/frontend/src/api/queries/useAllSettings.ts
index e83539a26..0d6cb757b 100644
--- a/frontend/src/api/queries/useAllSettings.ts
+++ b/frontend/src/api/queries/useAllSettings.ts
@@ -77,6 +77,7 @@ export function useAllSettings() {
if (!msg?.success) return;
if (saved) markSaved(saved);
queryClient.invalidateQueries({ queryKey: keys.settings.all() });
+ queryClient.invalidateQueries({ queryKey: keys.settings.defaults() });
},
});
diff --git a/frontend/src/test/use-all-settings.test.tsx b/frontend/src/test/use-all-settings.test.tsx
index e7f197bd6..dba588f51 100644
--- a/frontend/src/test/use-all-settings.test.tsx
+++ b/frontend/src/test/use-all-settings.test.tsx
@@ -13,6 +13,67 @@ afterEach(() => {
});
describe('useAllSettings', () => {
+ it('refreshes cached default settings after a successful save', async () => {
+ vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', {}));
+ const queryClient = makeTestQueryClient();
+ const fetchDefaults = vi
+ .fn()
+ .mockResolvedValueOnce({ subURI: 'https://example.com/sub/' })
+ .mockResolvedValueOnce({ subURI: 'https://example.com/my_custom_path/' });
+ const defaultsQuery = {
+ queryKey: keys.settings.defaults(),
+ queryFn: fetchDefaults,
+ staleTime: Infinity,
+ };
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+
+ await queryClient.fetchQuery(defaultsQuery);
+ const { result } = renderHook(() => useAllSettings(), { wrapper });
+
+ await waitFor(() => expect(result.current.fetched).toBe(true));
+ await act(async () => {
+ await result.current.saveAll();
+ });
+
+ const defaults = await queryClient.fetchQuery(defaultsQuery);
+ expect(fetchDefaults).toHaveBeenCalledTimes(2);
+ expect(defaults.subURI).toBe('https://example.com/my_custom_path/');
+ });
+
+ it('keeps cached default settings when a save fails', async () => {
+ vi.spyOn(HttpUtil, 'post').mockImplementation(async (url) => {
+ if (url === '/panel/api/setting/update') return new Msg(false, 'Save failed');
+ return new Msg(true, '', {});
+ });
+ const queryClient = makeTestQueryClient();
+ const fetchDefaults = vi
+ .fn()
+ .mockResolvedValueOnce({ subURI: 'https://example.com/sub/' })
+ .mockResolvedValueOnce({ subURI: 'https://example.com/my_custom_path/' });
+ const defaultsQuery = {
+ queryKey: keys.settings.defaults(),
+ queryFn: fetchDefaults,
+ staleTime: Infinity,
+ };
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+
+ await queryClient.fetchQuery(defaultsQuery);
+ const { result } = renderHook(() => useAllSettings(), { wrapper });
+
+ await waitFor(() => expect(result.current.fetched).toBe(true));
+ await act(async () => {
+ await result.current.saveAll();
+ });
+
+ const defaults = await queryClient.fetchQuery(defaultsQuery);
+ expect(fetchDefaults).toHaveBeenCalledOnce();
+ expect(defaults.subURI).toBe('https://example.com/sub/');
+ });
+
it('accepts legacy overlength regex settings without logging a response validation warning', async () => {
const subJsonUserAgentRegex = 'x'.repeat(2_049);
vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', { subJsonUserAgentRegex }));