mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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:
parent
74672fafcb
commit
68265db3ce
3 changed files with 214 additions and 16 deletions
|
|
@ -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.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue