mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-03 20:20:35 +00:00
Prefix cache tags with ppr: for PPR route rendering to ensure cache entries are partitioned from static ones
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Prefix all cache tags with `ppr:` when rendering under the PPR route, so PPR cache entries are partitioned from the static ones.
|
||||
@@ -43,7 +43,8 @@ export type PPRRouteParams = PPRRouteLayoutParams & {
|
||||
*/
|
||||
export async function getStaticSiteContext(
|
||||
params: RouteLayoutParams,
|
||||
getClaims = getVisitorAuthClaimsFromToken
|
||||
getClaims = getVisitorAuthClaimsFromToken,
|
||||
options?: { ppr?: true }
|
||||
) {
|
||||
const siteURL = getSiteURLFromParams(params);
|
||||
const siteURLData = getSiteURLDataFromParams(params);
|
||||
@@ -60,6 +61,7 @@ export async function getStaticSiteContext(
|
||||
siteURL,
|
||||
siteURLData,
|
||||
urlMode: getModeFromParams(params.mode),
|
||||
...(options?.ppr ? { ppr: options.ppr } : {}),
|
||||
}),
|
||||
siteURLData
|
||||
);
|
||||
@@ -224,7 +226,7 @@ export function getPPRTableOfContentsRouteParams(params: PPRRouteLayoutParams):
|
||||
}
|
||||
|
||||
export async function getPPRStaticSiteContext(params: RouteLayoutParams) {
|
||||
return getStaticSiteContext(params, getPPRVisitorAuthClaimsFromToken);
|
||||
return getStaticSiteContext(params, getPPRVisitorAuthClaimsFromToken, { ppr: true });
|
||||
}
|
||||
|
||||
function getPPRRouteParam(encodedParam: string, name: string): string {
|
||||
|
||||
@@ -3,9 +3,9 @@ import {
|
||||
type RouteParams,
|
||||
getPPRStaticSiteContext,
|
||||
getPagePathFromParams,
|
||||
getStaticSiteContext,
|
||||
} from '@/app/utils';
|
||||
import { SpaceHeader, SpaceTableOfContents } from '@/components/SpaceLayout';
|
||||
import { prefixCacheTags } from '@/lib/cache-tags';
|
||||
|
||||
import { getCacheTag } from '@gitbook/cache-tags';
|
||||
|
||||
@@ -23,14 +23,19 @@ export async function PPRHeader(props: { params: RouteLayoutParams }) {
|
||||
|
||||
console.log('PPRHeader props.params', props.params);
|
||||
|
||||
const { context } = await getStaticSiteContext(props.params);
|
||||
const { context } = await getPPRStaticSiteContext(props.params);
|
||||
|
||||
// We only need the site cache tag for the header, as the header is not dependent on the page or space content.
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
})
|
||||
...prefixCacheTags(
|
||||
[
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
}),
|
||||
],
|
||||
true
|
||||
)
|
||||
); // Tag the cache entry for the header so it can be invalidated when the site changes
|
||||
|
||||
return <SpaceHeader context={context} />;
|
||||
@@ -80,18 +85,22 @@ export async function cachedGenerateSitePageViewport(routeParams: RouteParams):
|
||||
const { context } = await getPPRStaticSiteContext(routeParams);
|
||||
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
})
|
||||
); // Tag the cache entry for the metadata so it can be invalidated when the site changes
|
||||
|
||||
cacheTag(
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: context.space.id,
|
||||
})
|
||||
); // Tag the cache entry for the metadata so it can be invalidated when the space changes
|
||||
...prefixCacheTags(
|
||||
[
|
||||
// Tag the cache entry for the metadata so it can be invalidated when the site changes
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: context.site.id,
|
||||
}),
|
||||
// ...or when the space changes
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: context.space.id,
|
||||
}),
|
||||
],
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
return generateSitePageViewport(context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Cache tags emitted while rendering the PPR route are prefixed, so that PPR cache entries
|
||||
* live in their own namespace and are invalidated independently from the static ones.
|
||||
*/
|
||||
export const PPR_CACHE_TAG_PREFIX = 'ppr:';
|
||||
|
||||
/**
|
||||
* Prefix cache tags when rendering under the PPR route, leave them untouched otherwise.
|
||||
*/
|
||||
export function prefixCacheTags(tags: string[], ppr: boolean | undefined): string[] {
|
||||
return ppr ? tags.map((tag) => `${PPR_CACHE_TAG_PREFIX}${tag}`) : tags;
|
||||
}
|
||||
@@ -228,12 +228,15 @@ export function getBaseContext(input: {
|
||||
siteURL: URL | string;
|
||||
siteURLData: SiteURLData;
|
||||
urlMode: 'url' | 'url-host';
|
||||
/** Set when rendering under the PPR route, to prefix the cache tags emitted by the fetcher. */
|
||||
ppr?: true;
|
||||
}) {
|
||||
const { urlMode, siteURLData } = input;
|
||||
const siteURL = typeof input.siteURL === 'string' ? new URL(input.siteURL) : input.siteURL;
|
||||
|
||||
const dataFetcher = createDataFetcher({
|
||||
apiToken: siteURLData.apiToken ?? null,
|
||||
...(input.ppr ? { ppr: input.ppr } : {}),
|
||||
});
|
||||
|
||||
const gitbookURL = GITBOOK_URL ? new URL(GITBOOK_URL) : undefined;
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-t
|
||||
import { cache } from '../cache';
|
||||
import { DataFetcherError, wrapDataFetcherError } from './errors';
|
||||
import type { GitBookDataFetcher } from './types';
|
||||
import { prefixCacheTags } from '@/lib/cache-tags';
|
||||
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@/lib/env';
|
||||
import { trace } from '@/lib/tracing';
|
||||
|
||||
@@ -21,6 +22,12 @@ interface DataFetcherInput {
|
||||
* API token.
|
||||
*/
|
||||
apiToken: string | null;
|
||||
|
||||
/**
|
||||
* Set when rendering under the PPR route. It is part of the cache key, so PPR cache entries
|
||||
* are never shared with the static ones and can carry their own `ppr:` prefixed cache tags.
|
||||
*/
|
||||
ppr?: true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,15 +44,24 @@ export const noCacheFetchOptions: Partial<RequestInit> = {
|
||||
* The data are being cached by Next.js built-in cache.
|
||||
*/
|
||||
export function createDataFetcher(
|
||||
input: DataFetcherInput = { apiToken: null }
|
||||
rawInput: DataFetcherInput = { apiToken: null }
|
||||
): GitBookDataFetcher {
|
||||
// The input is part of the cache key of every cached fetcher below, so we normalize its shape
|
||||
// here: `ppr` is omitted entirely when unset, keeping non-PPR cache keys unchanged.
|
||||
const input: DataFetcherInput = rawInput.ppr
|
||||
? { apiToken: rawInput.apiToken, ppr: true }
|
||||
: { apiToken: rawInput.apiToken };
|
||||
|
||||
return {
|
||||
ppr: input.ppr,
|
||||
|
||||
async api() {
|
||||
return apiClient(input);
|
||||
},
|
||||
|
||||
withToken({ apiToken }) {
|
||||
return createDataFetcher({
|
||||
...input,
|
||||
apiToken,
|
||||
});
|
||||
},
|
||||
@@ -246,7 +262,7 @@ const getUserById = cache(async (input: DataFetcherInput, params: { userId: stri
|
||||
const res = await api.users.getUserById(params.userId, {
|
||||
...noCacheFetchOptions,
|
||||
});
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('days');
|
||||
return res.data;
|
||||
});
|
||||
@@ -256,12 +272,12 @@ const getUserById = cache(async (input: DataFetcherInput, params: { userId: stri
|
||||
const getSpace = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; shareKey: string | undefined }) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: params.spaceId,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(`getSpace(${params.spaceId}, ${params.shareKey})`, async () => {
|
||||
@@ -275,7 +291,7 @@ const getSpace = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('days');
|
||||
return res.data;
|
||||
});
|
||||
@@ -286,13 +302,13 @@ const getSpace = cache(
|
||||
const getChangeRequest = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; changeRequestId: string }) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'change-request',
|
||||
space: params.spaceId,
|
||||
changeRequest: params.changeRequestId,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
@@ -306,7 +322,7 @@ const getChangeRequest = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('minutes');
|
||||
return res.data;
|
||||
}
|
||||
@@ -332,7 +348,7 @@ const getRevision = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('max');
|
||||
return res.data;
|
||||
});
|
||||
@@ -346,13 +362,13 @@ const getChangeRequestChanges = cache(
|
||||
params: { spaceId: string; changeRequestId: string; limit?: number }
|
||||
) => {
|
||||
'use cache: remote';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'change-request',
|
||||
space: params.spaceId,
|
||||
changeRequest: params.changeRequestId,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
@@ -369,7 +385,7 @@ const getChangeRequestChanges = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('minutes');
|
||||
return res.data;
|
||||
}
|
||||
@@ -401,7 +417,7 @@ const getRevisionSemanticChanges = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('max');
|
||||
return res.data;
|
||||
}
|
||||
@@ -438,7 +454,7 @@ const getRevisionPageMarkdown = cache(
|
||||
}
|
||||
);
|
||||
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('max');
|
||||
|
||||
if (!('markdown' in res.data)) {
|
||||
@@ -480,7 +496,7 @@ const getRevisionPageDocument = cache(
|
||||
}
|
||||
);
|
||||
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLifeFromResponse(res, 'max');
|
||||
|
||||
return res.data;
|
||||
@@ -513,7 +529,7 @@ const getRevisionReusableContentDocument = cache(
|
||||
}
|
||||
);
|
||||
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLifeFromResponse(res, 'max');
|
||||
|
||||
return res.data;
|
||||
@@ -546,7 +562,7 @@ const getRevisionPageByPath = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('max');
|
||||
return res.data;
|
||||
}
|
||||
@@ -569,7 +585,7 @@ const getDocument = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLifeFromResponse(res, 'max');
|
||||
return res.data;
|
||||
});
|
||||
@@ -588,15 +604,15 @@ const getComputedDocument = cache(
|
||||
}
|
||||
) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
...getComputedContentSourceCacheTags(
|
||||
{
|
||||
spaceId: params.spaceId,
|
||||
organizationId: params.organizationId,
|
||||
},
|
||||
params.source
|
||||
)
|
||||
);
|
||||
),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
@@ -614,7 +630,7 @@ const getComputedDocument = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLifeFromResponse(res, 'max');
|
||||
return res.data;
|
||||
}
|
||||
@@ -627,13 +643,13 @@ const getComputedDocument = cache(
|
||||
const getLatestOpenAPISpecVersionContent = cache(
|
||||
async (input: DataFetcherInput, params: { organizationId: string; slug: string }) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'openapi',
|
||||
organization: params.organizationId,
|
||||
openAPISpec: params.slug,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
@@ -647,7 +663,7 @@ const getLatestOpenAPISpecVersionContent = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('max');
|
||||
return res.data;
|
||||
}
|
||||
@@ -663,12 +679,12 @@ const getPublishedContentSite = cache(
|
||||
_apiVersion: string
|
||||
) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
@@ -685,7 +701,7 @@ const getPublishedContentSite = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('days');
|
||||
return res.data;
|
||||
}
|
||||
@@ -705,12 +721,12 @@ const getSiteRedirectBySource = cache(
|
||||
}
|
||||
) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
@@ -728,7 +744,7 @@ const getSiteRedirectBySource = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('days');
|
||||
return res.data;
|
||||
}
|
||||
@@ -740,12 +756,12 @@ const getSiteRedirectBySource = cache(
|
||||
const getEmbedByUrl = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; url: string }) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'space',
|
||||
space: params.spaceId,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(`getEmbedByUrl(${params.spaceId}, ${params.url})`, async () => {
|
||||
@@ -759,7 +775,7 @@ const getEmbedByUrl = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('weeks');
|
||||
return res.data;
|
||||
});
|
||||
@@ -774,12 +790,12 @@ const searchSiteContent = cache(
|
||||
params: Parameters<GitBookDataFetcher['searchSiteContent']>[0]
|
||||
) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'site',
|
||||
site: params.siteId,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(
|
||||
@@ -809,7 +825,7 @@ const searchSiteContent = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('hours');
|
||||
return res.data.items;
|
||||
}
|
||||
@@ -824,12 +840,12 @@ const renderIntegrationUi = cache(
|
||||
params: { integrationName: string; request: RenderIntegrationUI }
|
||||
) => {
|
||||
'use cache';
|
||||
cacheTag(
|
||||
cacheTagsFor(input, [
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
integration: params.integrationName,
|
||||
})
|
||||
);
|
||||
}),
|
||||
]);
|
||||
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(`renderIntegrationUi(${params.integrationName})`, async () => {
|
||||
@@ -841,7 +857,7 @@ const renderIntegrationUi = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('days');
|
||||
return res.data;
|
||||
});
|
||||
@@ -871,7 +887,7 @@ const listRevisionPageMetaLinks = cache(
|
||||
...noCacheFetchOptions,
|
||||
}
|
||||
);
|
||||
cacheTag(...getCacheTagsFromResponse(res));
|
||||
cacheTagsFor(input, getCacheTagsFromResponse(res));
|
||||
cacheLife('days');
|
||||
return res.data;
|
||||
}
|
||||
@@ -903,3 +919,11 @@ function getCacheTagsFromResponse(response: HttpResponse<unknown, unknown>) {
|
||||
const tags = !cacheTagHeader ? [] : cacheTagHeader.split(',');
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag the current cache entry. Always tag through this helper, never `cacheTag` directly,
|
||||
* so that a PPR render never emits an un-prefixed tag.
|
||||
*/
|
||||
function cacheTagsFor(input: DataFetcherInput, tags: string[]) {
|
||||
cacheTag(...prefixCacheTags(tags, input.ppr));
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ export type DataFetcherResponse<T> =
|
||||
* It is used between v1 and v2.
|
||||
*/
|
||||
export interface GitBookDataFetcher {
|
||||
/**
|
||||
* Set when rendering under the PPR route: the cache entries are partitioned from the static
|
||||
* ones and their cache tags are prefixed.
|
||||
*/
|
||||
readonly ppr?: true;
|
||||
|
||||
/**
|
||||
* Get an API client for the current context.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
ResolveOpenAPIBlockArgs,
|
||||
} from './types';
|
||||
import type { FetchOpenAPIFilesystemResult } from './types';
|
||||
import { prefixCacheTags } from '@/lib/cache-tags';
|
||||
import { DataFetcherError, noCacheFetchOptions } from '@/lib/data';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
|
||||
@@ -44,7 +45,7 @@ export async function fetchOpenAPIFilesystem(
|
||||
return resolved.openapi.filesystem;
|
||||
}
|
||||
// For legacy blocks ("swagger"), we need to fetch the file system.
|
||||
return fetchFilesystem(resolved.href, context.space.id);
|
||||
return fetchFilesystem(resolved.href, context.space.id, context.dataFetcher.ppr);
|
||||
})();
|
||||
|
||||
if ('error' in result) {
|
||||
@@ -74,7 +75,9 @@ export async function fetchOpenAPIFilesystem(
|
||||
*/
|
||||
async function fetchFilesystem(
|
||||
url: string,
|
||||
spaceId: string
|
||||
spaceId: string,
|
||||
// Part of the cache key, so PPR entries are not shared with the static ones.
|
||||
ppr: true | undefined
|
||||
): Promise<
|
||||
| Filesystem
|
||||
| {
|
||||
@@ -86,7 +89,7 @@ async function fetchFilesystem(
|
||||
> {
|
||||
'use cache';
|
||||
try {
|
||||
cacheTag(getCacheTag({ tag: 'space', space: spaceId }));
|
||||
cacheTag(...prefixCacheTags([getCacheTag({ tag: 'space', space: spaceId })], ppr));
|
||||
return await fetchFilesystemNoCache(url);
|
||||
} catch (error) {
|
||||
// To avoid hammering the file with requests, we cache the error for around a minute.
|
||||
|
||||
Reference in New Issue
Block a user