From e9e2182d083cf7f93e29c3cfd0d82e4189c19a22 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 25 Feb 2026 20:49:13 +0100 Subject: [PATCH 1/3] docs: clarify RFC 3986 assumption in redactCredentialsFromConnectionString regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot review comment about the regex potentially leaking password fragments when passwords contain unescaped '@'. The regex is correct for spec-compliant URIs — per RFC 3986, '@' in credentials MUST be percent-encoded as '%40'. Added an explicit comment documenting this assumption. --- src/documentdb/utils/connectionStringHelpers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/documentdb/utils/connectionStringHelpers.ts b/src/documentdb/utils/connectionStringHelpers.ts index f3a29249..eade39ee 100644 --- a/src/documentdb/utils/connectionStringHelpers.ts +++ b/src/documentdb/utils/connectionStringHelpers.ts @@ -19,8 +19,13 @@ import { DocumentDBConnectionString } from './DocumentDBConnectionString'; * @returns The text with credentials replaced by `` */ export const redactCredentialsFromConnectionString = (text: string): string => { - // Matches the credentials portion (everything before the last '@' that follows the scheme) + // Matches the credentials portion (user:password before the '@' delimiter) // in mongodb:// or mongodb+srv:// URIs. + // + // Note: Per RFC 3986, the '@' character in usernames or passwords MUST be + // percent-encoded as '%40'. This regex relies on that assumption — it stops + // at the first literal '@' after the scheme, which is the credential delimiter + // in any spec-compliant URI. Unencoded '@' in passwords is malformed input. return text.replace(/(mongodb(?:\+srv)?:\/\/)[^\s@]*@/gi, '$1@'); }; From 960118e1bda187ceca32130734bcd54e6573b66f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Wed, 25 Feb 2026 20:50:16 +0100 Subject: [PATCH 2/3] fix: clear Stage 3 tips/error timer on cancel and re-trigger Address Copilot review comment about the setTimeout timer in handleGetAISuggestions never being cleared. The returned cleanup function was ignored since this is an event handler, not a React effect. Fix: Store the timer ID in a useRef (stage3TipsTimerRef), clear it at the start of handleGetAISuggestions (re-trigger) and in handleCancelAI (cancel), and remove the dead return statement. This prevents stale tips/error cards from flashing when the user cancels within 1 second or rapidly re-triggers the AI request. --- .../queryInsightsTab/QueryInsightsTab.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx b/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx index ac3fe7eb..1b7eb6e7 100644 --- a/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx +++ b/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx @@ -109,6 +109,9 @@ export const QueryInsightsMain = (): JSX.Element => { // AbortController ref for cancelling in-flight Stage 3 AI requests const stage3AbortControllerRef = useRef(null); + // Timer ref for the delayed tips/error card shown during Stage 3 loading + const stage3TipsTimerRef = useRef | null>(null); + // Feedback dialog state const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false); const [feedbackSentiment, setFeedbackSentiment] = useState<'positive' | 'negative'>('positive'); @@ -452,13 +455,19 @@ export const QueryInsightsMain = (): JSX.Element => { // Transition to Stage 3 loading (this will reset UI flags) transitionToStage(3, 'loading'); + // Clear any pending tips/error card timer from a previous request + if (stage3TipsTimerRef.current) { + clearTimeout(stage3TipsTimerRef.current); + stage3TipsTimerRef.current = null; + } + // Check if Stage 2 has query execution errors const hasExecutionError = queryInsightsState.stage2Data?.concerns && queryInsightsState.stage2Data.concerns.some((concern) => concern.includes('Query Execution Failed')); // Show appropriate card after 1 second delay - const timer = setTimeout(() => { + stage3TipsTimerRef.current = setTimeout(() => { if (hasExecutionError) { setShowErrorCard(true); } else { @@ -550,11 +559,15 @@ export const QueryInsightsMain = (): JSX.Element => { ...prev, stage3Promise: promise, })); - - return () => clearTimeout(timer); }; const handleCancelAI = () => { + // Clear any pending tips/error card timer to prevent stale UI after cancel + if (stage3TipsTimerRef.current) { + clearTimeout(stage3TipsTimerRef.current); + stage3TipsTimerRef.current = null; + } + // Abort the in-flight tRPC request so the server can stop work early if (stage3AbortControllerRef.current) { stage3AbortControllerRef.current.abort(); From 3af91115dcce723bfb857f5f03f699905b48b2e1 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Mon, 9 Mar 2026 12:05:35 +0100 Subject: [PATCH 3/3] Add unmount cleanup for query insights timer --- .../components/queryInsightsTab/QueryInsightsTab.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx b/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx index 1b7eb6e7..8485ff0c 100644 --- a/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx +++ b/src/webviews/documentdb/collectionView/components/queryInsightsTab/QueryInsightsTab.tsx @@ -116,6 +116,15 @@ export const QueryInsightsMain = (): JSX.Element => { const [feedbackDialogOpen, setFeedbackDialogOpen] = useState(false); const [feedbackSentiment, setFeedbackSentiment] = useState<'positive' | 'negative'>('positive'); + useEffect(() => { + return () => { + if (stage3TipsTimerRef.current !== null) { + clearTimeout(stage3TipsTimerRef.current); + stage3TipsTimerRef.current = null; + } + }; + }, []); + /** * Display error message to user for the given stage * Only displays once per error state to avoid duplicate toasts