Skip to content

Commit 3879fd4

Browse files
committed
fix: improve CloudSettingsService error handling for fetch failures
- Add retry mechanism with exponential backoff (max 3 retries) - Add detailed network diagnostics for fetch failures - Log proxy configuration, Node.js version, and VSCode version - Provide helpful error messages for common network issues - Add comprehensive tests for retry logic and error handling This should help users debug "fetch failed" errors when using Gemini embedder for codebase indexing by providing more context about the failure and attempting automatic retries. Fixes #6626
1 parent a88238f commit 3879fd4

File tree

2 files changed

+208
-7
lines changed

2 files changed

+208
-7
lines changed

packages/cloud/src/CloudSettingsService.ts

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { RefreshTimer } from "./RefreshTimer"
1414
import type { SettingsService } from "./SettingsService"
1515

1616
const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"
17+
const MAX_FETCH_RETRIES = 3
18+
const INITIAL_RETRY_DELAY = 1000 // 1 second
1719

1820
export interface SettingsServiceEvents {
1921
"settings-updated": [
@@ -73,15 +75,81 @@ export class CloudSettingsService extends EventEmitter<SettingsServiceEvents> im
7375
}
7476
}
7577

78+
/**
79+
* Performs network diagnostics to help debug connectivity issues
80+
*/
81+
private async performNetworkDiagnostics(url: string): Promise<void> {
82+
this.log("[cloud-settings] Performing network diagnostics...")
83+
84+
// Check if we're in a proxy environment
85+
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy
86+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy
87+
const noProxy = process.env.NO_PROXY || process.env.no_proxy
88+
89+
if (httpProxy || httpsProxy) {
90+
this.log(" Proxy configuration detected:")
91+
if (httpProxy) this.log(` HTTP_PROXY: ${httpProxy}`)
92+
if (httpsProxy) this.log(` HTTPS_PROXY: ${httpsProxy}`)
93+
if (noProxy) this.log(` NO_PROXY: ${noProxy}`)
94+
}
95+
96+
// Log Node.js version (can affect fetch behavior)
97+
this.log(` Node.js version: ${process.version}`)
98+
99+
// Log VSCode version
100+
this.log(` VSCode version: ${vscode.version}`)
101+
102+
// Try to parse the URL to check components
103+
try {
104+
const parsedUrl = new URL(url)
105+
this.log(` URL components:`)
106+
this.log(` Protocol: ${parsedUrl.protocol}`)
107+
this.log(` Hostname: ${parsedUrl.hostname}`)
108+
this.log(` Port: ${parsedUrl.port || "(default)"}`)
109+
this.log(` Path: ${parsedUrl.pathname}`)
110+
} catch (e) {
111+
this.log(` Failed to parse URL: ${e}`)
112+
}
113+
}
114+
115+
/**
116+
* Attempts to fetch with retry logic and enhanced error handling
117+
*/
118+
private async fetchWithRetry(url: string, options: RequestInit, retryCount: number = 0): Promise<Response> {
119+
try {
120+
const response = await fetch(url, options)
121+
return response
122+
} catch (error) {
123+
if (retryCount >= MAX_FETCH_RETRIES) {
124+
throw error
125+
}
126+
127+
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount)
128+
this.log(
129+
`[cloud-settings] Fetch failed, retrying in ${delay}ms (attempt ${retryCount + 1}/${MAX_FETCH_RETRIES})`,
130+
)
131+
132+
// Wait before retrying
133+
await new Promise((resolve) => setTimeout(resolve, delay))
134+
135+
return this.fetchWithRetry(url, options, retryCount + 1)
136+
}
137+
}
138+
76139
private async fetchSettings(): Promise<boolean> {
77140
const token = this.authService.getSessionToken()
78141

79142
if (!token) {
80143
return false
81144
}
82145

146+
const apiUrl = getRooCodeApiUrl()
147+
const fullUrl = `${apiUrl}/api/organization-settings`
148+
83149
try {
84-
const response = await fetch(`${getRooCodeApiUrl()}/api/organization-settings`, {
150+
this.log(`[cloud-settings] Attempting to fetch from: ${fullUrl}`)
151+
152+
const response = await this.fetchWithRetry(fullUrl, {
85153
headers: {
86154
Authorization: `Bearer ${token}`,
87155
},
@@ -119,7 +187,39 @@ export class CloudSettingsService extends EventEmitter<SettingsServiceEvents> im
119187

120188
return true
121189
} catch (error) {
122-
this.log("[cloud-settings] Error fetching organization settings:", error)
190+
// Enhanced error logging with more details
191+
if (error instanceof Error) {
192+
this.log("[cloud-settings] Error fetching organization settings:")
193+
this.log(" Error name:", error.name)
194+
this.log(" Error message:", error.message)
195+
196+
// Check for specific error types
197+
if (error.message.includes("fetch failed")) {
198+
this.log(" This appears to be a network connectivity issue.")
199+
this.log(" Possible causes:")
200+
this.log(" - Network proxy configuration")
201+
this.log(" - Firewall blocking the request")
202+
this.log(" - DNS resolution issues")
203+
this.log(" - VSCode extension host network restrictions")
204+
this.log(` Target URL: ${fullUrl}`)
205+
206+
// Perform additional network diagnostics
207+
await this.performNetworkDiagnostics(fullUrl)
208+
209+
// Log additional error details if available
210+
if ("cause" in error && error.cause) {
211+
this.log(" Underlying cause:", error.cause)
212+
}
213+
}
214+
215+
// Log stack trace for debugging
216+
if (error.stack) {
217+
this.log(" Stack trace:", error.stack)
218+
}
219+
} else {
220+
this.log("[cloud-settings] Unknown error type:", error)
221+
}
222+
123223
return false
124224
}
125225
}

packages/cloud/src/__tests__/CloudSettingsService.test.ts

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -347,18 +347,119 @@ describe("CloudSettingsService", () => {
347347
})
348348

349349
it("should handle fetch errors gracefully", async () => {
350+
vi.useFakeTimers()
350351
mockAuthService.getSessionToken.mockReturnValue("valid-token")
352+
353+
// Mock fetch to always fail
351354
vi.mocked(fetch).mockRejectedValue(new Error("Network error"))
352355

353356
// Get the callback function passed to RefreshTimer
354357
const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
355-
const result = await timerCallback()
358+
const resultPromise = timerCallback()
359+
360+
// Advance through all retries
361+
await vi.advanceTimersByTimeAsync(1000) // First retry
362+
await vi.advanceTimersByTimeAsync(2000) // Second retry
363+
await vi.advanceTimersByTimeAsync(4000) // Third retry
364+
365+
const result = await resultPromise
356366

357367
expect(result).toBe(false)
358-
expect(mockLog).toHaveBeenCalledWith(
359-
"[cloud-settings] Error fetching organization settings:",
360-
expect.any(Error),
361-
)
368+
expect(mockLog).toHaveBeenCalledWith("[cloud-settings] Error fetching organization settings:")
369+
expect(mockLog).toHaveBeenCalledWith(" Error name:", "Error")
370+
expect(mockLog).toHaveBeenCalledWith(" Error message:", "Network error")
371+
372+
vi.useRealTimers()
373+
})
374+
375+
it("should retry on fetch failure with exponential backoff", async () => {
376+
vi.useFakeTimers()
377+
mockAuthService.getSessionToken.mockReturnValue("valid-token")
378+
379+
// Mock fetch to fail twice then succeed
380+
vi.mocked(fetch)
381+
.mockRejectedValueOnce(new Error("fetch failed"))
382+
.mockRejectedValueOnce(new Error("fetch failed"))
383+
.mockResolvedValueOnce({
384+
ok: true,
385+
json: vi.fn().mockResolvedValue(mockSettings),
386+
} as unknown as Response)
387+
388+
// Get the callback function passed to RefreshTimer
389+
const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
390+
const resultPromise = timerCallback()
391+
392+
// First retry after 1 second
393+
await vi.advanceTimersByTimeAsync(1000)
394+
395+
// Second retry after 2 seconds (exponential backoff)
396+
await vi.advanceTimersByTimeAsync(2000)
397+
398+
const result = await resultPromise
399+
400+
expect(result).toBe(true)
401+
expect(fetch).toHaveBeenCalledTimes(3)
402+
expect(mockLog).toHaveBeenCalledWith("[cloud-settings] Fetch failed, retrying in 1000ms (attempt 1/3)")
403+
expect(mockLog).toHaveBeenCalledWith("[cloud-settings] Fetch failed, retrying in 2000ms (attempt 2/3)")
404+
405+
vi.useRealTimers()
406+
})
407+
408+
it("should fail after max retries", async () => {
409+
vi.useFakeTimers()
410+
mockAuthService.getSessionToken.mockReturnValue("valid-token")
411+
412+
// Mock fetch to always fail
413+
vi.mocked(fetch).mockRejectedValue(new Error("fetch failed"))
414+
415+
// Get the callback function passed to RefreshTimer
416+
const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
417+
const resultPromise = timerCallback()
418+
419+
// Advance through all retries
420+
await vi.advanceTimersByTimeAsync(1000) // First retry
421+
await vi.advanceTimersByTimeAsync(2000) // Second retry
422+
await vi.advanceTimersByTimeAsync(4000) // Third retry
423+
424+
const result = await resultPromise
425+
426+
expect(result).toBe(false)
427+
expect(fetch).toHaveBeenCalledTimes(4) // Initial + 3 retries
428+
expect(mockLog).toHaveBeenCalledWith(" This appears to be a network connectivity issue.")
429+
430+
vi.useRealTimers()
431+
})
432+
433+
it("should perform network diagnostics on fetch failed error", async () => {
434+
vi.useFakeTimers()
435+
mockAuthService.getSessionToken.mockReturnValue("valid-token")
436+
const fetchError = new Error("fetch failed")
437+
vi.mocked(fetch).mockRejectedValue(fetchError)
438+
439+
// Mock environment variables
440+
process.env.HTTPS_PROXY = "http://proxy.example.com:8080"
441+
442+
// Get the callback function passed to RefreshTimer
443+
const timerCallback = vi.mocked(RefreshTimer).mock.calls[0][0].callback
444+
const resultPromise = timerCallback()
445+
446+
// Advance through all retries
447+
await vi.advanceTimersByTimeAsync(1000) // First retry
448+
await vi.advanceTimersByTimeAsync(2000) // Second retry
449+
await vi.advanceTimersByTimeAsync(4000) // Third retry
450+
451+
const result = await resultPromise
452+
453+
expect(result).toBe(false)
454+
expect(mockLog).toHaveBeenCalledWith("[cloud-settings] Performing network diagnostics...")
455+
expect(mockLog).toHaveBeenCalledWith(" Proxy configuration detected:")
456+
expect(mockLog).toHaveBeenCalledWith(" HTTPS_PROXY: http://proxy.example.com:8080")
457+
expect(mockLog).toHaveBeenCalledWith(expect.stringContaining(" Node.js version:"))
458+
expect(mockLog).toHaveBeenCalledWith(expect.stringContaining(" VSCode version:"))
459+
460+
// Clean up
461+
delete process.env.HTTPS_PROXY
462+
vi.useRealTimers()
362463
})
363464

364465
it("should handle invalid response format", async () => {

0 commit comments

Comments
 (0)