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:
Roo Code 2025-11-26 18:02:14 +00:00
parent a8a44510d5
commit e3bc189b30
6 changed files with 267 additions and 8 deletions

View file

@ -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 () => {

View file

@ -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

View file

@ -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)}`,

View file

@ -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}`,
)

View 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.",
)
})
})
})

View file

@ -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}`)
}