From be6fbc4dcc8fff7e7419cf3fa9b05a6b13e3edba Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Thu, 7 Dec 2023 13:14:35 -0500 Subject: [PATCH] [Fleet] Fix 500 in Fleet API when request to product versions endpoint throws ECONNREFUSED (#172850) ## Summary Network-level errors will cause `fetch` to `throw` rather than resolving with a status code. This PR updates our logic to handle this case for airgapped environments where `ECONNREFUSED` style errors squash HTTP requests at the DNS level. --- .../server/services/agents/versions.test.ts | 25 +++++++++++++ .../fleet/server/services/agents/versions.ts | 37 ++++++++++++------- 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/versions.test.ts b/x-pack/plugins/fleet/server/services/agents/versions.test.ts index 513fba910705de..15fa38d4949bc4 100644 --- a/x-pack/plugins/fleet/server/services/agents/versions.test.ts +++ b/x-pack/plugins/fleet/server/services/agents/versions.test.ts @@ -179,4 +179,29 @@ describe('getAvailableVersions', () => { expect(mockedFetch).toBeCalledTimes(1); expect(res2).not.toContain('300.0.0'); }); + + it('should gracefully handle 400 errors when fetching from product versions API', async () => { + mockKibanaVersion = '300.0.0'; + mockedReadFile.mockResolvedValue(`["8.1.0", "8.0.0", "7.17.0", "7.16.0"]`); + mockedFetch.mockResolvedValue({ + status: 400, + text: 'Bad request', + } as any); + + const res = await getAvailableVersions({ ignoreCache: true }); + + // Should sort, uniquify and filter out versions < 7.17 + expect(res).toEqual(['8.1.0', '8.0.0', '7.17.0']); + }); + + it('should gracefully handle network errors when fetching from product versions API', async () => { + mockKibanaVersion = '300.0.0'; + mockedReadFile.mockResolvedValue(`["8.1.0", "8.0.0", "7.17.0", "7.16.0"]`); + mockedFetch.mockRejectedValue('ECONNREFUSED'); + + const res = await getAvailableVersions({ ignoreCache: true }); + + // Should sort, uniquify and filter out versions < 7.17 + expect(res).toEqual(['8.1.0', '8.0.0', '7.17.0']); + }); }); diff --git a/x-pack/plugins/fleet/server/services/agents/versions.ts b/x-pack/plugins/fleet/server/services/agents/versions.ts index 8f31d3f12b3443..b5a09bc99e2cbc 100644 --- a/x-pack/plugins/fleet/server/services/agents/versions.ts +++ b/x-pack/plugins/fleet/server/services/agents/versions.ts @@ -24,6 +24,7 @@ const AGENT_VERSION_BUILD_FILE = 'x-pack/plugins/fleet/target/agent_versions_lis // Endpoint maintained by the web-team and hosted on the elastic website const PRODUCT_VERSIONS_URL = 'https://www.elastic.co/api/product_versions'; +const MAX_REQUEST_TIMEOUT = 60 * 1000; // Only attempt to fetch product versions for one minute total // Cache available versions in memory for 1 hour const CACHE_DURATION = 1000 * 60 * 60; @@ -118,21 +119,29 @@ async function fetchAgentVersionsFromApi() { }, }; - const response = await pRetry(() => fetch(PRODUCT_VERSIONS_URL, options), { retries: 1 }); - const rawBody = await response.text(); - - // We need to handle non-200 responses gracefully here to support airgapped environments where - // Kibana doesn't have internet access to query this API - if (response.status >= 400) { - logger.debug(`Status code ${response.status} received from versions API: ${rawBody}`); - return []; - } + try { + const response = await pRetry(() => fetch(PRODUCT_VERSIONS_URL, options), { + retries: 1, + maxRetryTime: MAX_REQUEST_TIMEOUT, + }); + const rawBody = await response.text(); + + // We need to handle non-200 responses gracefully here to support airgapped environments where + // Kibana doesn't have internet access to query this API + if (response.status >= 400) { + logger.debug(`Status code ${response.status} received from versions API: ${rawBody}`); + return []; + } - const jsonBody = JSON.parse(rawBody); + const jsonBody = JSON.parse(rawBody); - const versions: string[] = (jsonBody.length ? jsonBody[0] : []) - .filter((item: any) => item?.title?.includes('Elastic Agent')) - .map((item: any) => item?.version_number); + const versions: string[] = (jsonBody.length ? jsonBody[0] : []) + .filter((item: any) => item?.title?.includes('Elastic Agent')) + .map((item: any) => item?.version_number); - return versions; + return versions; + } catch (error) { + logger.debug(`Error fetching available versions from API: ${error.message}`); + return []; + } }