From a3fe162e75bb59513588722a8591a30e49c07a41 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 14 Jul 2025 02:34:42 +0000 Subject: [PATCH] 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 --- .../fetchers/__tests__/ollama.test.ts | 39 ++++++++++++++++--- src/api/providers/fetchers/ollama.ts | 14 +++++-- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/api/providers/fetchers/__tests__/ollama.test.ts b/src/api/providers/fetchers/__tests__/ollama.test.ts index bf1bf3c6b2..0562397dfc 100644 --- a/src/api/providers/fetchers/__tests__/ollama.test.ts +++ b/src/api/providers/fetchers/__tests__/ollama.test.ts @@ -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) diff --git a/src/api/providers/fetchers/ollama.ts b/src/api/providers/fetchers/ollama.ts index 8e1e3f7f07..73db51f5a6 100644 --- a/src/api/providers/fetchers/ollama.ts +++ b/src/api/providers/fetchers/ollama.ts @@ -65,7 +65,7 @@ export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promi return models } - const response = await axios.get(`${baseUrl}/api/tags`) + const response = await axios.get(`${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(`${baseUrl}/api/show`, { - model: ollamaModel.model, - }) + .post( + `${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)}`,