Skip to content

Commit 68265db

Browse files
committed
fix: improve marketplace network error handling for corporate proxies
- Enhanced RemoteConfigLoader with better proxy support using axios built-in proxy configuration - Added comprehensive error handling for common network issues (socket hang up, timeouts, DNS issues) - Increased timeout from 10s to 15s for corporate networks - Added User-Agent header and improved retry logic for network errors - Enhanced ClineProvider error messaging with user-friendly notifications - Updated tests to match new configuration parameters Fixes #6488
1 parent 74672fa commit 68265db

File tree

3 files changed

+214
-16
lines changed

3 files changed

+214
-16
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,19 +1345,44 @@ export class ClineProvider
13451345
})
13461346
} catch (error) {
13471347
console.error("Failed to fetch marketplace data:", error)
1348+
const errorMessage = error instanceof Error ? error.message : String(error)
1349+
13481350
// Send empty data on error to prevent UI from hanging
13491351
this.postMessageToWebview({
13501352
type: "marketplaceData",
13511353
organizationMcps: [],
13521354
marketplaceItems: [],
13531355
marketplaceInstalledMetadata: { project: {}, global: {} },
1354-
errors: [error instanceof Error ? error.message : String(error)],
1356+
errors: [errorMessage],
13551357
})
13561358

1357-
// Show user-friendly error notification for network issues
1358-
if (error instanceof Error && error.message.includes("timeout")) {
1359+
// Show user-friendly error notification for specific network issues
1360+
if (errorMessage.includes("socket hang up") || errorMessage.includes("corporate proxy")) {
1361+
vscode.window
1362+
.showWarningMessage(
1363+
"Marketplace data could not be loaded due to network restrictions or corporate proxy settings. " +
1364+
"Core functionality remains available. Please check your network configuration if you need marketplace features.",
1365+
"Learn More",
1366+
)
1367+
.then((selection) => {
1368+
if (selection === "Learn More") {
1369+
vscode.env.openExternal(
1370+
vscode.Uri.parse("https://docs.roo-code.com/troubleshooting/network-issues"),
1371+
)
1372+
}
1373+
})
1374+
} else if (errorMessage.includes("timeout")) {
1375+
vscode.window.showWarningMessage(
1376+
"Marketplace data could not be loaded due to network timeout. Core functionality remains available.",
1377+
)
1378+
} else if (errorMessage.includes("ENOTFOUND") || errorMessage.includes("DNS")) {
1379+
vscode.window.showWarningMessage(
1380+
"Marketplace data could not be loaded due to DNS resolution issues. Please check your internet connection.",
1381+
)
1382+
} else {
1383+
// Generic network error
13591384
vscode.window.showWarningMessage(
1360-
"Marketplace data could not be loaded due to network restrictions. Core functionality remains available.",
1385+
"Marketplace data could not be loaded due to network issues. Core functionality remains available.",
13611386
)
13621387
}
13631388
}

src/services/marketplace/RemoteConfigLoader.ts

Lines changed: 170 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from "axios"
1+
import axios, { AxiosRequestConfig } from "axios"
22
import * as yaml from "yaml"
33
import { z } from "zod"
44
import { getRooCodeApiUrl } from "@roo-code/cloud"
@@ -23,6 +23,47 @@ export class RemoteConfigLoader {
2323
this.apiBaseUrl = getRooCodeApiUrl()
2424
}
2525

26+
private getProxyConfig(): Partial<AxiosRequestConfig> {
27+
// Check for proxy environment variables
28+
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy
29+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy
30+
const noProxy = process.env.NO_PROXY || process.env.no_proxy
31+
32+
// Check if the API URL should bypass proxy
33+
if (noProxy) {
34+
const noProxyList = noProxy.split(",").map((host) => host.trim())
35+
const apiUrl = new URL(this.apiBaseUrl)
36+
const shouldBypassProxy = noProxyList.some((host) => {
37+
if (host === "*") return true
38+
if (host.startsWith(".")) return apiUrl.hostname.endsWith(host)
39+
return apiUrl.hostname === host || apiUrl.hostname.endsWith("." + host)
40+
})
41+
if (shouldBypassProxy) return {}
42+
}
43+
44+
// Use axios built-in proxy support
45+
const apiUrl = new URL(this.apiBaseUrl)
46+
const proxyUrl = apiUrl.protocol === "https:" ? httpsProxy : httpProxy
47+
48+
if (proxyUrl) {
49+
try {
50+
const proxy = new URL(proxyUrl)
51+
return {
52+
proxy: {
53+
protocol: proxy.protocol.slice(0, -1), // Remove trailing ':'
54+
host: proxy.hostname,
55+
port: parseInt(proxy.port) || (proxy.protocol === "https:" ? 443 : 80),
56+
...(proxy.username && { auth: { username: proxy.username, password: proxy.password || "" } }),
57+
},
58+
}
59+
} catch (error) {
60+
console.warn("Invalid proxy URL format:", proxyUrl)
61+
}
62+
}
63+
64+
return {}
65+
}
66+
2667
async loadAllItems(hideMarketplaceMcps = false): Promise<MarketplaceItem[]> {
2768
const items: MarketplaceItem[] = []
2869

@@ -80,25 +121,146 @@ export class RemoteConfigLoader {
80121

81122
for (let i = 0; i < maxRetries; i++) {
82123
try {
83-
const response = await axios.get(url, {
84-
timeout: 10000, // 10 second timeout
124+
const proxyConfig = this.getProxyConfig()
125+
const config: AxiosRequestConfig = {
126+
timeout: 15000, // Increased timeout for corporate networks
85127
headers: {
86128
Accept: "application/json",
87129
"Content-Type": "application/json",
130+
"User-Agent": "Roo-Code-Extension/1.0",
88131
},
89-
})
132+
// Add proxy configuration
133+
...proxyConfig,
134+
// Additional network resilience options
135+
maxRedirects: 5,
136+
validateStatus: (status) => status < 500, // Accept 4xx errors but retry 5xx
137+
}
138+
139+
const response = await axios.get(url, config)
140+
141+
// Handle non-2xx responses gracefully
142+
if (response.status >= 400) {
143+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
144+
}
145+
90146
return response.data as T
91147
} catch (error) {
92148
lastError = error as Error
93-
if (i < maxRetries - 1) {
94-
// Exponential backoff: 1s, 2s, 4s
95-
const delay = Math.pow(2, i) * 1000
149+
150+
// Enhanced error categorization for better retry logic
151+
const isNetworkError = this.isNetworkError(error)
152+
const isRetryableError = this.isRetryableError(error)
153+
154+
// Don't retry on non-retryable errors (like 404, 401, etc.)
155+
if (!isRetryableError && i === 0) {
156+
// For non-retryable errors, throw immediately with enhanced message
157+
throw this.enhanceError(error as Error)
158+
}
159+
160+
if (i < maxRetries - 1 && (isNetworkError || isRetryableError)) {
161+
// Progressive backoff: 2s, 4s, 8s for network issues
162+
const baseDelay = isNetworkError ? 2000 : 1000
163+
const delay = Math.pow(2, i) * baseDelay
96164
await new Promise((resolve) => setTimeout(resolve, delay))
97165
}
98166
}
99167
}
100168

101-
throw lastError!
169+
throw this.enhanceError(lastError!)
170+
}
171+
172+
private isNetworkError(error: any): boolean {
173+
if (!error) return false
174+
175+
const networkErrorCodes = [
176+
"ECONNRESET",
177+
"ECONNREFUSED",
178+
"ENOTFOUND",
179+
"ENETUNREACH",
180+
"ETIMEDOUT",
181+
"ECONNABORTED",
182+
"EHOSTUNREACH",
183+
"EPIPE",
184+
]
185+
186+
return (
187+
networkErrorCodes.includes(error.code) ||
188+
error.message?.includes("socket hang up") ||
189+
error.message?.includes("timeout") ||
190+
error.message?.includes("network")
191+
)
192+
}
193+
194+
private isRetryableError(error: any): boolean {
195+
if (!error) return false
196+
197+
// Retry on network errors
198+
if (this.isNetworkError(error)) return true
199+
200+
// Retry on 5xx server errors
201+
if (error.response?.status >= 500) return true
202+
203+
// Retry on specific axios errors
204+
if (error.code === "ECONNABORTED") return true
205+
206+
return false
207+
}
208+
209+
private enhanceError(error: Error): Error {
210+
const originalMessage = error.message || "Unknown error"
211+
212+
// Provide user-friendly error messages for common network issues
213+
if (originalMessage.includes("socket hang up")) {
214+
return new Error(
215+
"Network connection was interrupted while loading marketplace data. " +
216+
"This may be due to corporate proxy settings or network restrictions. " +
217+
"Please check your network configuration or try again later.",
218+
)
219+
}
220+
221+
if (originalMessage.includes("ENOTFOUND") || originalMessage.includes("getaddrinfo")) {
222+
return new Error(
223+
"Unable to resolve marketplace server address. " +
224+
"Please check your internet connection and DNS settings.",
225+
)
226+
}
227+
228+
if (originalMessage.includes("ECONNREFUSED")) {
229+
return new Error(
230+
"Connection to marketplace server was refused. " + "The service may be temporarily unavailable.",
231+
)
232+
}
233+
234+
if (originalMessage.includes("timeout")) {
235+
return new Error(
236+
"Request to marketplace server timed out. " +
237+
"This may be due to slow network conditions or corporate firewall settings.",
238+
)
239+
}
240+
241+
if (originalMessage.includes("ECONNRESET")) {
242+
return new Error(
243+
"Connection to marketplace server was reset. " +
244+
"This often occurs in corporate networks with strict proxy policies.",
245+
)
246+
}
247+
248+
// For HTTP errors, provide more context
249+
if (originalMessage.includes("HTTP 4")) {
250+
return new Error(
251+
"Marketplace server returned a client error. " + "The requested resource may not be available.",
252+
)
253+
}
254+
255+
if (originalMessage.includes("HTTP 5")) {
256+
return new Error("Marketplace server is experiencing issues. " + "Please try again later.")
257+
}
258+
259+
// Return enhanced error with original message for debugging
260+
return new Error(
261+
`Failed to load marketplace data: ${originalMessage}. ` +
262+
"If you are behind a corporate firewall, please ensure proxy settings are configured correctly.",
263+
)
102264
}
103265

104266
async getItem(id: string, type: MarketplaceItemType): Promise<MarketplaceItem | null> {

src/services/marketplace/__tests__/RemoteConfigLoader.spec.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,27 @@ describe("RemoteConfigLoader", () => {
5454
expect(mockedAxios.get).toHaveBeenCalledWith(
5555
"https://test.api.com/api/marketplace/modes",
5656
expect.objectContaining({
57-
timeout: 10000,
57+
timeout: 15000,
5858
headers: {
5959
Accept: "application/json",
6060
"Content-Type": "application/json",
61+
"User-Agent": "Roo-Code-Extension/1.0",
6162
},
63+
maxRedirects: 5,
64+
validateStatus: expect.any(Function),
6265
}),
6366
)
6467
expect(mockedAxios.get).toHaveBeenCalledWith(
6568
"https://test.api.com/api/marketplace/mcps",
6669
expect.objectContaining({
67-
timeout: 10000,
70+
timeout: 15000,
6871
headers: {
6972
Accept: "application/json",
7073
"Content-Type": "application/json",
74+
"User-Agent": "Roo-Code-Extension/1.0",
7175
},
76+
maxRedirects: 5,
77+
validateStatus: expect.any(Function),
7278
}),
7379
)
7480

@@ -140,7 +146,10 @@ describe("RemoteConfigLoader", () => {
140146
if (url.includes("/modes")) {
141147
modesCallCount++
142148
if (modesCallCount <= 2) {
143-
return Promise.reject(new Error("Network error"))
149+
// Use a network error that will be retried
150+
const error = new Error("ECONNRESET") as any
151+
error.code = "ECONNRESET"
152+
return Promise.reject(error)
144153
}
145154
return Promise.resolve({ data: mockModesYaml })
146155
}
@@ -161,7 +170,9 @@ describe("RemoteConfigLoader", () => {
161170
it("should throw error after max retries", async () => {
162171
mockedAxios.get.mockRejectedValue(new Error("Persistent network error"))
163172

164-
await expect(loader.loadAllItems()).rejects.toThrow("Persistent network error")
173+
await expect(loader.loadAllItems()).rejects.toThrow(
174+
"Failed to load marketplace data: Persistent network error",
175+
)
165176

166177
// Both endpoints will be called with retries since Promise.all starts both promises
167178
// Each endpoint retries 3 times, but due to Promise.all behavior, one might fail faster

0 commit comments

Comments
 (0)