diff --git a/.changeset/real-walls-glow.md b/.changeset/real-walls-glow.md new file mode 100644 index 0000000000..277c39ca9f --- /dev/null +++ b/.changeset/real-walls-glow.md @@ -0,0 +1,5 @@ +--- +"gitbook-v2": patch +--- + +Don't cache unexpected API errors for more than a few minutes. diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index d36c2445f5..8e6535331a 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -9,7 +9,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; import { cache } from '../cache'; -import { DataFetcherError, wrapDataFetcherError } from './errors'; +import { DataFetcherError, wrapCacheDataFetcherError } from './errors'; import type { GitBookDataFetcher } from './types'; interface DataFetcherInput { @@ -206,8 +206,8 @@ export function createDataFetcher( const getUserById = cache(async (input: DataFetcherInput, params: { userId: string }) => { 'use cache'; - return trace(`getUserById(${params.userId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getUserById(${params.userId})`, async () => { const api = apiClient(input); const res = await api.users.getUserById(params.userId, { ...noCacheFetchOptions, @@ -229,8 +229,8 @@ const getSpace = cache( }) ); - return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => { const api = apiClient(input); const res = await api.spaces.getSpaceById( params.spaceId, @@ -260,20 +260,23 @@ const getChangeRequest = cache( }) ); - return trace(`getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, async () => { - return wrapDataFetcherError(async () => { - const api = apiClient(input); - const res = await api.spaces.getChangeRequestById( - params.spaceId, - params.changeRequestId, - { - ...noCacheFetchOptions, - } - ); - cacheTag(...getCacheTagsFromResponse(res)); - cacheLife('minutes'); - return res.data; - }); + return wrapCacheDataFetcherError(async () => { + return trace( + `getChangeRequest(${params.spaceId}, ${params.changeRequestId})`, + async () => { + const api = apiClient(input); + const res = await api.spaces.getChangeRequestById( + params.spaceId, + params.changeRequestId, + { + ...noCacheFetchOptions, + } + ); + cacheTag(...getCacheTagsFromResponse(res)); + cacheLife('minutes'); + return res.data; + } + ); }); } ); @@ -284,8 +287,8 @@ const getRevision = cache( params: { spaceId: string; revisionId: string; metadata: boolean } ) => { 'use cache'; - return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => { const api = apiClient(input); const res = await api.spaces.getRevisionById( params.spaceId, @@ -311,8 +314,8 @@ const getRevisionPages = cache( params: { spaceId: string; revisionId: string; metadata: boolean } ) => { 'use cache'; - return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getRevisionPages(${params.spaceId}, ${params.revisionId})`, async () => { const api = apiClient(input); const res = await api.spaces.listPagesInRevisionById( params.spaceId, @@ -338,10 +341,10 @@ const getRevisionFile = cache( params: { spaceId: string; revisionId: string; fileId: string } ) => { 'use cache'; - return trace( - `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionFile(${params.spaceId}, ${params.revisionId}, ${params.fileId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getFileInRevisionById( params.spaceId, @@ -355,9 +358,9 @@ const getRevisionFile = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -367,10 +370,10 @@ const getRevisionPageMarkdown = cache( params: { spaceId: string; revisionId: string; pageId: string } ) => { 'use cache'; - return trace( - `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionPageMarkdown(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getPageInRevisionById( params.spaceId, @@ -391,9 +394,9 @@ const getRevisionPageMarkdown = cache( throw new DataFetcherError('Page is not a document', 404); } return res.data.markdown; - }); - } - ); + } + ); + }); } ); @@ -403,10 +406,10 @@ const getRevisionPageDocument = cache( params: { spaceId: string; revisionId: string; pageId: string } ) => { 'use cache'; - return trace( - `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionPageDocument(${params.spaceId}, ${params.revisionId}, ${params.pageId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getPageDocumentInRevisionById( params.spaceId, @@ -424,9 +427,9 @@ const getRevisionPageDocument = cache( cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -436,11 +439,11 @@ const getRevisionPageByPath = cache( params: { spaceId: string; revisionId: string; path: string } ) => { 'use cache'; - return trace( - `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, - async () => { - const encodedPath = encodeURIComponent(params.path); - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getRevisionPageByPath(${params.spaceId}, ${params.revisionId}, ${params.path})`, + async () => { + const encodedPath = encodeURIComponent(params.path); const api = apiClient(input); const res = await api.spaces.getPageInRevisionByPath( params.spaceId, @@ -454,17 +457,17 @@ const getRevisionPageByPath = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); const getDocument = cache( async (input: DataFetcherInput, params: { spaceId: string; documentId: string }) => { 'use cache'; - return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getDocument(${params.spaceId}, ${params.documentId})`, async () => { const api = apiClient(input); const res = await api.spaces.getDocumentById( params.spaceId, @@ -503,10 +506,10 @@ const getComputedDocument = cache( ) ); - return trace( - `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getComputedDocument(${params.spaceId}, ${params.organizationId}, ${params.source.type}, ${params.seed})`, + async () => { const api = apiClient(input); const res = await api.spaces.getComputedDocument( params.spaceId, @@ -522,9 +525,9 @@ const getComputedDocument = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -534,10 +537,10 @@ const getReusableContent = cache( params: { spaceId: string; revisionId: string; reusableContentId: string } ) => { 'use cache'; - return trace( - `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getReusableContent(${params.spaceId}, ${params.revisionId}, ${params.reusableContentId})`, + async () => { const api = apiClient(input); const res = await api.spaces.getReusableContentInRevisionById( params.spaceId, @@ -551,9 +554,9 @@ const getReusableContent = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -568,10 +571,10 @@ const getLatestOpenAPISpecVersionContent = cache( }) ); - return trace( - `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getLatestOpenAPISpecVersionContent(${params.organizationId}, ${params.slug})`, + async () => { const api = apiClient(input); const res = await api.orgs.getLatestOpenApiSpecVersionContent( params.organizationId, @@ -583,9 +586,9 @@ const getLatestOpenAPISpecVersionContent = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('max'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -602,10 +605,10 @@ const getPublishedContentSite = cache( }) ); - return trace( - `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getPublishedContentSite(${params.organizationId}, ${params.siteId}, ${params.siteShareKey})`, + async () => { const api = apiClient(input); const res = await api.orgs.getPublishedContentSite( params.organizationId, @@ -620,9 +623,9 @@ const getPublishedContentSite = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('days'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -644,10 +647,10 @@ const getSiteRedirectBySource = cache( }) ); - return trace( - `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `getSiteRedirectBySource(${params.organizationId}, ${params.siteId}, ${params.siteShareKey}, ${params.source})`, + async () => { const api = apiClient(input); const res = await api.orgs.getSiteRedirectBySource( params.organizationId, @@ -663,9 +666,9 @@ const getSiteRedirectBySource = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('days'); return res.data; - }); - } - ); + } + ); + }); } ); @@ -679,8 +682,8 @@ const getEmbedByUrl = cache( }) ); - return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => { const api = apiClient(input); const res = await api.spaces.getEmbedByUrlInSpace( params.spaceId, @@ -712,10 +715,10 @@ const searchSiteContent = cache( }) ); - return trace( - `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, - async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace( + `searchSiteContent(${params.organizationId}, ${params.siteId}, ${params.query})`, + async () => { const { organizationId, siteId, query, scope } = params; const api = apiClient(input); const res = await api.orgs.searchSiteContent( @@ -733,9 +736,9 @@ const searchSiteContent = cache( cacheTag(...getCacheTagsFromResponse(res)); cacheLife('hours'); return res.data.items; - }); - } - ); + } + ); + }); } ); @@ -752,8 +755,8 @@ const renderIntegrationUi = cache( }) ); - return trace(`renderIntegrationUi(${params.integrationName})`, async () => { - return wrapDataFetcherError(async () => { + return wrapCacheDataFetcherError(async () => { + return trace(`renderIntegrationUi(${params.integrationName})`, async () => { const api = apiClient(input); const res = await api.integrations.renderIntegrationUiWithPost( params.integrationName, diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts index 4059fb5c94..c4d983ec2a 100644 --- a/packages/gitbook-v2/src/lib/data/errors.ts +++ b/packages/gitbook-v2/src/lib/data/errors.ts @@ -1,4 +1,5 @@ import { GitBookAPIError } from '@gitbook/api'; +import { unstable_cacheLife as cacheLife } from 'next/cache'; import type { DataFetcherErrorData, DataFetcherResponse } from './types'; export class DataFetcherError extends Error { @@ -89,6 +90,22 @@ export async function wrapDataFetcherError( } } +/** + * Wrap an async execution to handle errors and return a DataFetcherResponse. + * This should be used inside 'use cache' functions. + */ +export async function wrapCacheDataFetcherError( + fn: () => Promise +): Promise> { + const result = await wrapDataFetcherError(fn); + if (result.error && result.error.code >= 500) { + // We don't want to cache errors for too long. + // as the API might + cacheLife('minutes'); + } + return result; +} + /** * Ignore some data fetcher errors. */