fix: add timeout configuration to Ollama fetcher for remote connections

- Add 10-second timeout to axios requests in getOllamaModels()
- Improve error handling for timeout scenarios with specific warning message
- Update tests to verify timeout configuration is passed correctly
- Add new test case for timeout error handling

Fixes #5677: Ollama remote server connections now timeout gracefully instead of hanging indefinitely
This commit is contained in:
Roo Code 2025-07-14 02:34:42 +00:00
parent 62f97b917a
commit a3fe162e75
2 changed files with 43 additions and 10 deletions

View file

@ -109,10 +109,14 @@ describe("Ollama Fetcher", () => {
const result = await getOllamaModels(baseUrl)
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { timeout: 10000 })
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
expect(mockedAxios.post).toHaveBeenCalledWith(
`${baseUrl}/api/show`,
{ model: modelName },
{ timeout: 10000 },
)
expect(typeof result).toBe("object")
expect(result).not.toBeInstanceOf(Array)
@ -131,7 +135,7 @@ describe("Ollama Fetcher", () => {
const result = await getOllamaModels(baseUrl)
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { timeout: 10000 })
expect(mockedAxios.post).not.toHaveBeenCalled()
expect(result).toEqual({})
})
@ -147,7 +151,7 @@ describe("Ollama Fetcher", () => {
const result = await getOllamaModels(baseUrl)
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { timeout: 10000 })
expect(mockedAxios.post).not.toHaveBeenCalled()
expect(consoleInfoSpy).toHaveBeenCalledWith(`Failed connecting to Ollama at ${baseUrl}`)
expect(result).toEqual({})
@ -155,6 +159,25 @@ describe("Ollama Fetcher", () => {
consoleInfoSpy.mockRestore() // Restore original console.info
})
it("should log a warning message and return an empty object on timeout", async () => {
const baseUrl = "http://localhost:11434"
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}) // Spy and suppress output
const timeoutError = new Error("timeout of 10000ms exceeded") as any
timeoutError.code = "ECONNABORTED"
mockedAxios.get.mockRejectedValueOnce(timeoutError)
const result = await getOllamaModels(baseUrl)
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { timeout: 10000 })
expect(mockedAxios.post).not.toHaveBeenCalled()
expect(consoleWarnSpy).toHaveBeenCalledWith(`Connection to Ollama at ${baseUrl} timed out after 10 seconds`)
expect(result).toEqual({})
consoleWarnSpy.mockRestore() // Restore original console.warn
})
it("should handle models with null families field in API response", async () => {
const baseUrl = "http://localhost:11434"
const modelName = "test-model:latest"
@ -205,10 +228,14 @@ describe("Ollama Fetcher", () => {
const result = await getOllamaModels(baseUrl)
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { timeout: 10000 })
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
expect(mockedAxios.post).toHaveBeenCalledWith(
`${baseUrl}/api/show`,
{ model: modelName },
{ timeout: 10000 },
)
expect(typeof result).toBe("object")
expect(result).not.toBeInstanceOf(Array)

View file

@ -65,7 +65,7 @@ export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promi
return models
}
const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`)
const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`, { timeout: 10000 })
const parsedResponse = OllamaModelsResponseSchema.safeParse(response.data)
let modelInfoPromises = []
@ -73,9 +73,13 @@ export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promi
for (const ollamaModel of parsedResponse.data.models) {
modelInfoPromises.push(
axios
.post<OllamaModelInfoResponse>(`${baseUrl}/api/show`, {
model: ollamaModel.model,
})
.post<OllamaModelInfoResponse>(
`${baseUrl}/api/show`,
{
model: ollamaModel.model,
},
{ timeout: 10000 },
)
.then((ollamaModelInfo) => {
models[ollamaModel.name] = parseOllamaModel(ollamaModelInfo.data)
}),
@ -89,6 +93,8 @@ export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promi
} catch (error) {
if (error.code === "ECONNREFUSED") {
console.warn(`Failed connecting to Ollama at ${baseUrl}`)
} else if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) {
console.warn(`Connection to Ollama at ${baseUrl} timed out after 10 seconds`)
} else {
console.error(
`Error fetching Ollama models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,