mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: improve error messages for VPN-required connection failures
- Enhanced error handler to detect and provide context-aware messages for network errors - Added specific guidance for DNS failures (ENOTFOUND) suggesting VPN connection - Added clear messages for connection refused (ECONNREFUSED) errors - Added timeout error handling (ETIMEDOUT) with VPN stability guidance - Added network unreachable (ENETUNREACH) and connection reset (ECONNRESET) handling - Added SSL/TLS certificate error detection for internal services - Updated native Ollama provider with improved error messages - Updated Ollama fetcher with better error logging - Added comprehensive tests for new error handling - Updated existing tests to match new error messages Fixes #9617
This commit is contained in:
parent
a8a44510d5
commit
e3bc189b30
6 changed files with 267 additions and 8 deletions
|
|
@ -232,7 +232,7 @@ describe("NativeOllamaHandler", () => {
|
|||
for await (const _ of stream) {
|
||||
// consume stream
|
||||
}
|
||||
}).rejects.toThrow("Ollama service is not running")
|
||||
}).rejects.toThrow("Ollama service refused connection")
|
||||
})
|
||||
|
||||
it("should handle model not found errors", async () => {
|
||||
|
|
|
|||
|
|
@ -146,7 +146,9 @@ describe("Ollama Fetcher", () => {
|
|||
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
|
||||
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
|
||||
expect(mockedAxios.post).not.toHaveBeenCalled()
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(`Failed connecting to Ollama at ${baseUrl}`)
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
`Ollama service refused connection at ${baseUrl}. Please verify Ollama is running.`,
|
||||
)
|
||||
expect(result).toEqual({})
|
||||
|
||||
consoleInfoSpy.mockRestore() // Restore original console.info
|
||||
|
|
|
|||
|
|
@ -98,9 +98,25 @@ export async function getOllamaModels(
|
|||
} else {
|
||||
console.error(`Error parsing Ollama models response: ${JSON.stringify(parsedResponse.error, null, 2)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === "ECONNREFUSED") {
|
||||
console.warn(`Failed connecting to Ollama at ${baseUrl}`)
|
||||
} catch (error: any) {
|
||||
const errorCode = error.code || ""
|
||||
const errorMessage = error.message || "Unknown error"
|
||||
|
||||
// DNS resolution failures - typically VPN-related for internal endpoints
|
||||
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
|
||||
console.warn(
|
||||
`Cannot resolve hostname for Ollama at ${baseUrl}. If this is an internal service, please connect to your corporate VPN.`,
|
||||
)
|
||||
}
|
||||
// Connection refused - service is reachable but not accepting connections
|
||||
else if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
|
||||
console.warn(`Ollama service refused connection at ${baseUrl}. Please verify Ollama is running.`)
|
||||
}
|
||||
// Connection timeout - often indicates VPN or network stability issues
|
||||
else if (errorCode === "ETIMEDOUT" || errorMessage.includes("ETIMEDOUT")) {
|
||||
console.warn(
|
||||
`Request to Ollama timed out at ${baseUrl}. If using an internal service, verify your VPN connection is stable.`,
|
||||
)
|
||||
} else {
|
||||
console.error(
|
||||
`Error fetching Ollama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
|
||||
|
|
|
|||
|
|
@ -251,12 +251,32 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
|
|||
// Enhance error reporting
|
||||
const statusCode = error.status || error.statusCode
|
||||
const errorMessage = error.message || "Unknown error"
|
||||
const errorCode = error.code || ""
|
||||
const baseUrl = this.options.ollamaBaseUrl || "http://localhost:11434"
|
||||
|
||||
if (error.code === "ECONNREFUSED") {
|
||||
// DNS resolution failures - typically VPN-related for internal endpoints
|
||||
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
|
||||
throw new Error(
|
||||
`Ollama service is not running at ${this.options.ollamaBaseUrl || "http://localhost:11434"}. Please start Ollama first.`,
|
||||
`Cannot resolve hostname for Ollama at ${baseUrl}. If this is an internal service, please connect to your corporate VPN.`,
|
||||
)
|
||||
} else if (statusCode === 404) {
|
||||
}
|
||||
|
||||
// Connection refused - service is reachable but not accepting connections
|
||||
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
|
||||
throw new Error(
|
||||
`Ollama service refused connection at ${baseUrl}. Please verify Ollama is running. Start it with: ollama serve`,
|
||||
)
|
||||
}
|
||||
|
||||
// Connection timeout - often indicates VPN or network stability issues
|
||||
if (errorCode === "ETIMEDOUT" || errorMessage.includes("ETIMEDOUT")) {
|
||||
throw new Error(
|
||||
`Request to Ollama timed out at ${baseUrl}. If using an internal service, verify your VPN connection is stable.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Model not found
|
||||
if (statusCode === 404) {
|
||||
throw new Error(
|
||||
`Model ${this.getModel().id} not found in Ollama. Please pull the model first with: ollama pull ${this.getModel().id}`,
|
||||
)
|
||||
|
|
|
|||
163
src/api/providers/utils/__tests__/openai-error-handler.spec.ts
Normal file
163
src/api/providers/utils/__tests__/openai-error-handler.spec.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { handleOpenAIError } from "../openai-error-handler"
|
||||
|
||||
// Mock the i18n module
|
||||
vi.mock("../../../../i18n/setup", () => ({
|
||||
default: {
|
||||
t: (key: string) => key,
|
||||
},
|
||||
}))
|
||||
|
||||
describe("handleOpenAIError", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
})
|
||||
|
||||
describe("VPN-related error messages", () => {
|
||||
it("should handle ENOTFOUND errors with VPN guidance", () => {
|
||||
const error = new Error("getaddrinfo ENOTFOUND api.internal.company.com")
|
||||
;(error as any).code = "ENOTFOUND"
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Cannot resolve hostname. If this is an internal service, please connect to your corporate VPN.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle ECONNREFUSED errors with service verification guidance", () => {
|
||||
const error = new Error("connect ECONNREFUSED 127.0.0.1:11434")
|
||||
;(error as any).code = "ECONNREFUSED"
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Service refused connection. The API endpoint is reachable but not accepting connections. Please verify the service is running.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle ETIMEDOUT errors with VPN stability guidance", () => {
|
||||
const error = new Error("connect ETIMEDOUT")
|
||||
;(error as any).code = "ETIMEDOUT"
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Request timed out. If using an internal service, verify your VPN connection is stable.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle ENETUNREACH errors with network/VPN guidance", () => {
|
||||
const error = new Error("connect ENETUNREACH")
|
||||
;(error as any).code = "ENETUNREACH"
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Network unreachable. Please check your network connection and VPN status if accessing internal services.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle ECONNRESET errors with connection stability guidance", () => {
|
||||
const error = new Error("socket hang up")
|
||||
;(error as any).code = "ECONNRESET"
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Connection was reset. This may indicate network instability or VPN disconnection.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle certificate errors with VPN/cert guidance", () => {
|
||||
const error = new Error("self signed certificate in certificate chain")
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: SSL/TLS certificate error. This often occurs with internal services. Please verify your VPN connection and certificate configuration.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle fetch failed errors with network/VPN guidance", () => {
|
||||
const error = new Error("fetch failed")
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Network request failed. Please check your internet connection and VPN status if accessing internal services.",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Existing error handling", () => {
|
||||
it("should handle ByteString conversion errors", () => {
|
||||
const error = new Error(
|
||||
"Cannot convert argument to a ByteString because the character at index 5 has value 65533",
|
||||
)
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe("common:errors.api.invalidKeyInvalidChars")
|
||||
})
|
||||
|
||||
it("should handle generic errors with provider prefix", () => {
|
||||
const error = new Error("Some other API error")
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe("TestProvider completion error: Some other API error")
|
||||
})
|
||||
|
||||
it("should handle non-Error objects", () => {
|
||||
const error = "String error"
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result).toBeInstanceOf(Error)
|
||||
expect(result.message).toBe("TestProvider completion error: String error")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Error detection from message content", () => {
|
||||
it("should detect ENOTFOUND in error message without code", () => {
|
||||
const error = new Error("Error: getaddrinfo ENOTFOUND internal.api.com")
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Cannot resolve hostname. If this is an internal service, please connect to your corporate VPN.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should detect ECONNREFUSED in error message without code", () => {
|
||||
const error = new Error("Error: connect ECONNREFUSED 10.0.0.1:8080")
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Service refused connection. The API endpoint is reachable but not accepting connections. Please verify the service is running.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should detect ETIMEDOUT in error message without code", () => {
|
||||
const error = new Error("Request failed: ETIMEDOUT")
|
||||
|
||||
const result = handleOpenAIError(error, "TestProvider")
|
||||
|
||||
expect(result.message).toBe(
|
||||
"TestProvider: Request timed out. If using an internal service, verify your VPN connection is stable.",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -5,6 +5,57 @@
|
|||
|
||||
import i18n from "../../../i18n/setup"
|
||||
|
||||
/**
|
||||
* Analyzes error patterns to provide context-aware error messages
|
||||
* @param error - The error to analyze
|
||||
* @returns A user-friendly error message with actionable guidance
|
||||
*/
|
||||
function getContextAwareErrorMessage(error: unknown): string | null {
|
||||
if (!(error instanceof Error)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const msg = error.message || ""
|
||||
const errorCode = (error as any).code || ""
|
||||
|
||||
// DNS resolution failures - typically VPN-related for internal endpoints
|
||||
if (errorCode === "ENOTFOUND" || msg.includes("ENOTFOUND") || msg.includes("getaddrinfo ENOTFOUND")) {
|
||||
return "Cannot resolve hostname. If this is an internal service, please connect to your corporate VPN."
|
||||
}
|
||||
|
||||
// Connection refused - service is reachable but not accepting connections
|
||||
if (errorCode === "ECONNREFUSED" || msg.includes("ECONNREFUSED") || msg.includes("connect ECONNREFUSED")) {
|
||||
return "Service refused connection. The API endpoint is reachable but not accepting connections. Please verify the service is running."
|
||||
}
|
||||
|
||||
// Connection timeout - often indicates VPN or network stability issues
|
||||
if (errorCode === "ETIMEDOUT" || msg.includes("ETIMEDOUT") || msg.includes("connect ETIMEDOUT")) {
|
||||
return "Request timed out. If using an internal service, verify your VPN connection is stable."
|
||||
}
|
||||
|
||||
// Network unreachable
|
||||
if (errorCode === "ENETUNREACH" || msg.includes("ENETUNREACH")) {
|
||||
return "Network unreachable. Please check your network connection and VPN status if accessing internal services."
|
||||
}
|
||||
|
||||
// Socket hang up - connection was terminated unexpectedly
|
||||
if (errorCode === "ECONNRESET" || msg.includes("ECONNRESET") || msg.includes("socket hang up")) {
|
||||
return "Connection was reset. This may indicate network instability or VPN disconnection."
|
||||
}
|
||||
|
||||
// Certificate errors - often occur with internal/corporate endpoints
|
||||
if (msg.includes("CERT_") || msg.includes("certificate") || msg.includes("self signed")) {
|
||||
return "SSL/TLS certificate error. This often occurs with internal services. Please verify your VPN connection and certificate configuration."
|
||||
}
|
||||
|
||||
// Fetch failed - generic network error
|
||||
if (msg.includes("fetch failed") || msg.includes("Failed to fetch")) {
|
||||
return "Network request failed. Please check your internet connection and VPN status if accessing internal services."
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles OpenAI client errors and transforms them into user-friendly messages
|
||||
* @param error - The error to handle
|
||||
|
|
@ -19,6 +70,7 @@ export function handleOpenAIError(error: unknown, providerName: string): Error {
|
|||
console.error(`[${providerName}] API error:`, {
|
||||
message: msg,
|
||||
name: error.name,
|
||||
code: (error as any).code,
|
||||
stack: error.stack,
|
||||
})
|
||||
|
||||
|
|
@ -27,6 +79,12 @@ export function handleOpenAIError(error: unknown, providerName: string): Error {
|
|||
return new Error(i18n.t("common:errors.api.invalidKeyInvalidChars"))
|
||||
}
|
||||
|
||||
// Check for context-aware error messages
|
||||
const contextAwareMessage = getContextAwareErrorMessage(error)
|
||||
if (contextAwareMessage) {
|
||||
return new Error(`${providerName}: ${contextAwareMessage}`)
|
||||
}
|
||||
|
||||
// For other Error instances, wrap with provider-specific prefix
|
||||
return new Error(`${providerName} completion error: ${msg}`)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue