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
This commit is contained in:
Roo Code 2025-07-31 15:49:10 +00:00
parent 74672fafcb
commit 68265db3ce
3 changed files with 214 additions and 16 deletions

View file

@ -1345,19 +1345,44 @@ export class ClineProvider
})
} catch (error) {
console.error("Failed to fetch marketplace data:", error)
const errorMessage = error instanceof Error ? error.message : String(error)
// Send empty data on error to prevent UI from hanging
this.postMessageToWebview({
type: "marketplaceData",
organizationMcps: [],
marketplaceItems: [],
marketplaceInstalledMetadata: { project: {}, global: {} },
errors: [error instanceof Error ? error.message : String(error)],
errors: [errorMessage],
})
// Show user-friendly error notification for network issues
if (error instanceof Error && error.message.includes("timeout")) {
// Show user-friendly error notification for specific network issues
if (errorMessage.includes("socket hang up") || errorMessage.includes("corporate proxy")) {
vscode.window
.showWarningMessage(
"Marketplace data could not be loaded due to network restrictions or corporate proxy settings. " +
"Core functionality remains available. Please check your network configuration if you need marketplace features.",
"Learn More",
)
.then((selection) => {
if (selection === "Learn More") {
vscode.env.openExternal(
vscode.Uri.parse("https://docs.roo-code.com/troubleshooting/network-issues"),
)
}
})
} else if (errorMessage.includes("timeout")) {
vscode.window.showWarningMessage(
"Marketplace data could not be loaded due to network restrictions. Core functionality remains available.",
"Marketplace data could not be loaded due to network timeout. Core functionality remains available.",
)
} else if (errorMessage.includes("ENOTFOUND") || errorMessage.includes("DNS")) {
vscode.window.showWarningMessage(
"Marketplace data could not be loaded due to DNS resolution issues. Please check your internet connection.",
)
} else {
// Generic network error
vscode.window.showWarningMessage(
"Marketplace data could not be loaded due to network issues. Core functionality remains available.",
)
}
}

View file

@ -1,4 +1,4 @@
import axios from "axios"
import axios, { AxiosRequestConfig } from "axios"
import * as yaml from "yaml"
import { z } from "zod"
import { getRooCodeApiUrl } from "@roo-code/cloud"
@ -23,6 +23,47 @@ export class RemoteConfigLoader {
this.apiBaseUrl = getRooCodeApiUrl()
}
private getProxyConfig(): Partial<AxiosRequestConfig> {
// Check for proxy environment variables
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy
const noProxy = process.env.NO_PROXY || process.env.no_proxy
// Check if the API URL should bypass proxy
if (noProxy) {
const noProxyList = noProxy.split(",").map((host) => host.trim())
const apiUrl = new URL(this.apiBaseUrl)
const shouldBypassProxy = noProxyList.some((host) => {
if (host === "*") return true
if (host.startsWith(".")) return apiUrl.hostname.endsWith(host)
return apiUrl.hostname === host || apiUrl.hostname.endsWith("." + host)
})
if (shouldBypassProxy) return {}
}
// Use axios built-in proxy support
const apiUrl = new URL(this.apiBaseUrl)
const proxyUrl = apiUrl.protocol === "https:" ? httpsProxy : httpProxy
if (proxyUrl) {
try {
const proxy = new URL(proxyUrl)
return {
proxy: {
protocol: proxy.protocol.slice(0, -1), // Remove trailing ':'
host: proxy.hostname,
port: parseInt(proxy.port) || (proxy.protocol === "https:" ? 443 : 80),
...(proxy.username && { auth: { username: proxy.username, password: proxy.password || "" } }),
},
}
} catch (error) {
console.warn("Invalid proxy URL format:", proxyUrl)
}
}
return {}
}
async loadAllItems(hideMarketplaceMcps = false): Promise<MarketplaceItem[]> {
const items: MarketplaceItem[] = []
@ -80,25 +121,146 @@ export class RemoteConfigLoader {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await axios.get(url, {
timeout: 10000, // 10 second timeout
const proxyConfig = this.getProxyConfig()
const config: AxiosRequestConfig = {
timeout: 15000, // Increased timeout for corporate networks
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "Roo-Code-Extension/1.0",
},
})
// Add proxy configuration
...proxyConfig,
// Additional network resilience options
maxRedirects: 5,
validateStatus: (status) => status < 500, // Accept 4xx errors but retry 5xx
}
const response = await axios.get(url, config)
// Handle non-2xx responses gracefully
if (response.status >= 400) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.data as T
} catch (error) {
lastError = error as Error
if (i < maxRetries - 1) {
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000
// Enhanced error categorization for better retry logic
const isNetworkError = this.isNetworkError(error)
const isRetryableError = this.isRetryableError(error)
// Don't retry on non-retryable errors (like 404, 401, etc.)
if (!isRetryableError && i === 0) {
// For non-retryable errors, throw immediately with enhanced message
throw this.enhanceError(error as Error)
}
if (i < maxRetries - 1 && (isNetworkError || isRetryableError)) {
// Progressive backoff: 2s, 4s, 8s for network issues
const baseDelay = isNetworkError ? 2000 : 1000
const delay = Math.pow(2, i) * baseDelay
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
throw lastError!
throw this.enhanceError(lastError!)
}
private isNetworkError(error: any): boolean {
if (!error) return false
const networkErrorCodes = [
"ECONNRESET",
"ECONNREFUSED",
"ENOTFOUND",
"ENETUNREACH",
"ETIMEDOUT",
"ECONNABORTED",
"EHOSTUNREACH",
"EPIPE",
]
return (
networkErrorCodes.includes(error.code) ||
error.message?.includes("socket hang up") ||
error.message?.includes("timeout") ||
error.message?.includes("network")
)
}
private isRetryableError(error: any): boolean {
if (!error) return false
// Retry on network errors
if (this.isNetworkError(error)) return true
// Retry on 5xx server errors
if (error.response?.status >= 500) return true
// Retry on specific axios errors
if (error.code === "ECONNABORTED") return true
return false
}
private enhanceError(error: Error): Error {
const originalMessage = error.message || "Unknown error"
// Provide user-friendly error messages for common network issues
if (originalMessage.includes("socket hang up")) {
return new Error(
"Network connection was interrupted while loading marketplace data. " +
"This may be due to corporate proxy settings or network restrictions. " +
"Please check your network configuration or try again later.",
)
}
if (originalMessage.includes("ENOTFOUND") || originalMessage.includes("getaddrinfo")) {
return new Error(
"Unable to resolve marketplace server address. " +
"Please check your internet connection and DNS settings.",
)
}
if (originalMessage.includes("ECONNREFUSED")) {
return new Error(
"Connection to marketplace server was refused. " + "The service may be temporarily unavailable.",
)
}
if (originalMessage.includes("timeout")) {
return new Error(
"Request to marketplace server timed out. " +
"This may be due to slow network conditions or corporate firewall settings.",
)
}
if (originalMessage.includes("ECONNRESET")) {
return new Error(
"Connection to marketplace server was reset. " +
"This often occurs in corporate networks with strict proxy policies.",
)
}
// For HTTP errors, provide more context
if (originalMessage.includes("HTTP 4")) {
return new Error(
"Marketplace server returned a client error. " + "The requested resource may not be available.",
)
}
if (originalMessage.includes("HTTP 5")) {
return new Error("Marketplace server is experiencing issues. " + "Please try again later.")
}
// Return enhanced error with original message for debugging
return new Error(
`Failed to load marketplace data: ${originalMessage}. ` +
"If you are behind a corporate firewall, please ensure proxy settings are configured correctly.",
)
}
async getItem(id: string, type: MarketplaceItemType): Promise<MarketplaceItem | null> {

View file

@ -54,21 +54,27 @@ describe("RemoteConfigLoader", () => {
expect(mockedAxios.get).toHaveBeenCalledWith(
"https://test.api.com/api/marketplace/modes",
expect.objectContaining({
timeout: 10000,
timeout: 15000,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "Roo-Code-Extension/1.0",
},
maxRedirects: 5,
validateStatus: expect.any(Function),
}),
)
expect(mockedAxios.get).toHaveBeenCalledWith(
"https://test.api.com/api/marketplace/mcps",
expect.objectContaining({
timeout: 10000,
timeout: 15000,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "Roo-Code-Extension/1.0",
},
maxRedirects: 5,
validateStatus: expect.any(Function),
}),
)
@ -140,7 +146,10 @@ describe("RemoteConfigLoader", () => {
if (url.includes("/modes")) {
modesCallCount++
if (modesCallCount <= 2) {
return Promise.reject(new Error("Network error"))
// Use a network error that will be retried
const error = new Error("ECONNRESET") as any
error.code = "ECONNRESET"
return Promise.reject(error)
}
return Promise.resolve({ data: mockModesYaml })
}
@ -161,7 +170,9 @@ describe("RemoteConfigLoader", () => {
it("should throw error after max retries", async () => {
mockedAxios.get.mockRejectedValue(new Error("Persistent network error"))
await expect(loader.loadAllItems()).rejects.toThrow("Persistent network error")
await expect(loader.loadAllItems()).rejects.toThrow(
"Failed to load marketplace data: Persistent network error",
)
// Both endpoints will be called with retries since Promise.all starts both promises
// Each endpoint retries 3 times, but due to Promise.all behavior, one might fail faster