Skip to main content
Glama
aws-powertools

Powertools MCP Search Server

fetchWithCache.ts4.78 kB
import { isNull } from '@aws-lambda-powertools/commons/typeutils'; import { get as getFromCache, put as writeToCache } from 'cacache'; import { CACHE_BASE_PATH, FETCH_TIMEOUT_MS } from '../../constants.ts'; import { logger } from '../../logger.ts'; type FetchProps<T extends 'GET' | 'HEAD' = 'GET' | 'HEAD'> = { url: URL; contentType: string; method: T; }; type FetchResult<T extends 'GET' | 'HEAD'> = T extends 'GET' ? { content: string; eTag: string | null } : { content: undefined; eTag: string | null }; /** * Get a remote resource from the documentation. * * @param url - The URL of the remote documentation resource to fetch */ const fetchFromRemote = async <T extends 'GET' | 'HEAD'>( props: FetchProps<T> ): Promise<FetchResult<T>> => { const { url, contentType, method } = props; try { const response = await fetch(url, { method, headers: { Accept: contentType, }, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), }); if (!response.ok) { throw new Error( `Request to fetch ${url} failed: ${response.status} ${response.statusText}` ); } const eTag = response.headers.get('etag'); if (!eTag) { logger.warn('No ETag found in response; this may affect caching.'); } const cleanETag = eTag?.replace(/^"(.*)"$/, '$1') || null; if (method === 'GET') { return { content: await response.text(), eTag: cleanETag, } as FetchResult<T>; } return { content: undefined, eTag: cleanETag, } as FetchResult<T>; } catch (error) { logger.error('Failed to fetch remote resource', { error }); throw new Error('Failed to fetch remote resource', { cause: error, }); } }; /** * Error thrown when cache operations fail or when a cache miss occurs. */ class CacheError extends Error { constructor(message: string) { super(message); this.name = 'CacheError'; } } /** * Fetch a resource from remote or local cache. * * When using this function, we first check if the resource is available in the local cache * by using the page url as the cache key prefix. * * If none is found, we fetch the page from the remote server and cache it locally, * then return its content. * * If the local cache includes a potential match, we make a `HEAD` request to the remote server * and compare the ETag with the one stored in the local cache. * * If the ETag matches, we return the cached content. If it doesn't match, * we fetch the entire page using a `GET` request and update the local cache with the new content. * * @param props - options for fetching a resource * @param props.url - the URL of the resource to fetch */ const fetchWithCache = async ( props: Omit<FetchProps<'GET'>, 'method'> ): Promise<string> => { const cachePath = CACHE_BASE_PATH; const cacheKey = props.url.pathname; logger.debug('Generated cache key', { cacheKey }); try { const [cachedETagPromise, remoteETagPromise] = await Promise.allSettled([ getFromCache(cachePath, `${cacheKey}-etag`), fetchFromRemote({ ...props, method: 'HEAD', }), ]); const cachedETag = cachedETagPromise.status === 'fulfilled' ? cachedETagPromise.value.data.toString() : null; const remoteETag = remoteETagPromise.status === 'fulfilled' ? remoteETagPromise.value.eTag : null; if (isNull(cachedETag) && isNull(remoteETag)) { throw new CacheError( 'No cached ETag and remote ETag found, fetching remote resource' ); } if (cachedETag === remoteETag) { logger.debug('cached eTag matches, returning cached resource'); try { const cachedResource = await getFromCache(cachePath, cacheKey); return cachedResource.data.toString(); } catch (error) { logger.error('Failed to retrieve cached resource', { error, }); throw new CacheError( 'Cached resource not found even though ETag matches; cache may be corrupted' ); } } throw new CacheError( `ETag mismatch: local ${cachedETag} vs remote ${remoteETag}; fetching remote resource` ); } catch (error) { if (error instanceof CacheError) { logger.debug(error.message, { cacheKey }); } try { const { content, eTag: newEtag } = await fetchFromRemote({ ...props, method: 'GET', }); await writeToCache(cachePath, `${cacheKey}-etag`, newEtag); await writeToCache(cachePath, cacheKey, content); return content; } catch (fetchError) { logger.error('Failed to fetch remote resource', { error: fetchError, }); throw fetchError; } } }; export { fetchWithCache };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aws-powertools/powertools-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server