Skip to content

Commit

Permalink
[Fleet] Fix 500 in Fleet API when request to product versions endpoin…
Browse files Browse the repository at this point in the history
…t throws ECONNREFUSED (elastic#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.
  • Loading branch information
kpollich committed Dec 7, 2023
1 parent 4ea262d commit be6fbc4
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 14 deletions.
25 changes: 25 additions & 0 deletions x-pack/plugins/fleet/server/services/agents/versions.test.ts
Expand Up @@ -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']);
});
});
37 changes: 23 additions & 14 deletions x-pack/plugins/fleet/server/services/agents/versions.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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 [];
}
}

0 comments on commit be6fbc4

Please sign in to comment.