v2: fix error handling in middleware and data fetcher (#2925)

This commit is contained in:
Samy Pessé
2025-03-06 10:53:17 +01:00
committed by GitHub
parent 38e84ffe1a
commit e5366d3a49
22 changed files with 547 additions and 360 deletions
+1 -1
View File
@@ -243,7 +243,7 @@ jobs:
runs-on: ubuntu-latest
name: Visual Testing v2
needs: deploy-v2-vercel
timeout-minutes: 8
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
+11 -11
View File
@@ -137,7 +137,7 @@
"@gitbook/api": "0.96.1",
"@gitbook/cache-tags": "workspace:*",
"@sindresorhus/fnv1a": "^3.1.0",
"next": "^15.2.0",
"next": "^15.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"server-only": "^0.0.1",
@@ -4280,7 +4280,7 @@
"gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"gitbook-v2/next": ["next@15.2.0", "", { "dependencies": { "@next/env": "15.2.0", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0", "@next/swc-darwin-x64": "15.2.0", "@next/swc-linux-arm64-gnu": "15.2.0", "@next/swc-linux-arm64-musl": "15.2.0", "@next/swc-linux-x64-gnu": "15.2.0", "@next/swc-linux-x64-musl": "15.2.0", "@next/swc-win32-arm64-msvc": "15.2.0", "@next/swc-win32-x64-msvc": "15.2.0", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ=="],
"gitbook-v2/next": ["next@15.2.1", "", { "dependencies": { "@next/env": "15.2.1", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.1", "@next/swc-darwin-x64": "15.2.1", "@next/swc-linux-arm64-gnu": "15.2.1", "@next/swc-linux-arm64-musl": "15.2.1", "@next/swc-linux-x64-gnu": "15.2.1", "@next/swc-linux-x64-musl": "15.2.1", "@next/swc-win32-arm64-msvc": "15.2.1", "@next/swc-win32-x64-msvc": "15.2.1", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g=="],
"google-auth-library/jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
@@ -5180,23 +5180,23 @@
"gaxios/https-proxy-agent/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"gitbook-v2/next/@next/env": ["@next/env@15.2.0", "", {}, "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA=="],
"gitbook-v2/next/@next/env": ["@next/env@15.2.1", "", {}, "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q=="],
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA=="],
"gitbook-v2/next/@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ=="],
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA=="],
"gitbook-v2/next/@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ=="],
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ=="],
"gitbook-v2/next/@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ=="],
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw=="],
"gitbook-v2/next/@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg=="],
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q=="],
"gitbook-v2/next/@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww=="],
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ=="],
"gitbook-v2/next/@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg=="],
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw=="],
"gitbook-v2/next/@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw=="],
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw=="],
"gitbook-v2/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g=="],
"gitbook-v2/next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.1.2",
"private": true,
"dependencies": {
"next": "^15.2.0",
"next": "^15.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@gitbook/api": "0.96.1",
@@ -3,8 +3,6 @@ import type { NextRequest } from 'next/server';
import { serveIcon } from '@/routes/icon';
import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils';
export const dynamic = 'force-static';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<RouteLayoutParams> }
+36 -26
View File
@@ -13,7 +13,7 @@ import type {
SiteStructure,
Space,
} from '@gitbook/api';
import { type GitBookDataFetcher, createDataFetcher } from '@v2/lib/data';
import { type GitBookDataFetcher, createDataFetcher, throwIfDataError } from '@v2/lib/data';
import { redirect } from 'next/navigation';
import { assert } from 'ts-essentials';
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_URL } from './env';
@@ -164,11 +164,13 @@ export async function fetchSiteContextByURL(
}
): Promise<GitBookSiteContext> {
const { dataFetcher } = baseContext;
const data = await dataFetcher.getPublishedContentByUrl({
url: input.url,
visitorAuthToken: input.visitorAuthToken,
redirectOnError: input.redirectOnError,
});
const data = await throwIfDataError(
dataFetcher.getPublishedContentByUrl({
url: input.url,
visitorAuthToken: input.visitorAuthToken,
redirectOnError: input.redirectOnError,
})
);
return fetchSiteContextByURLLookup(baseContext, data);
}
@@ -225,11 +227,13 @@ export async function fetchSiteContextByIds(
const [{ site: orgSite, structure: siteStructure, customizations, scripts }, spaceContext] =
await Promise.all([
dataFetcher.getPublishedContentSite({
organizationId: ids.organization,
siteId: ids.site,
siteShareKey: ids.shareKey,
}),
throwIfDataError(
dataFetcher.getPublishedContentSite({
organizationId: ids.organization,
siteId: ids.site,
siteShareKey: ids.shareKey,
})
),
fetchSpaceContextByIds(baseContext, ids),
]);
@@ -302,27 +306,33 @@ export async function fetchSpaceContextByIds(
const { dataFetcher } = baseContext;
const [space, changeRequest] = await Promise.all([
dataFetcher.getSpace({
spaceId: ids.space,
shareKey: ids.shareKey,
}),
throwIfDataError(
dataFetcher.getSpace({
spaceId: ids.space,
shareKey: ids.shareKey,
})
),
ids.changeRequest
? dataFetcher.getChangeRequest({
spaceId: ids.space,
changeRequestId: ids.changeRequest,
})
? throwIfDataError(
dataFetcher.getChangeRequest({
spaceId: ids.space,
changeRequestId: ids.changeRequest,
})
)
: null,
]);
const revisionId = changeRequest?.revision ?? ids.revision ?? space.revision;
const pages = await dataFetcher.getRevisionPages({
spaceId: ids.space,
revisionId,
// We only care about the Git metadata when the Git sync is enabled,
// otherwise we can optimize performance by not fetching it
metadata: !!space.gitSync,
});
const pages = await throwIfDataError(
dataFetcher.getRevisionPages({
spaceId: ids.space,
revisionId,
// We only care about the Git metadata when the Git sync is enabled,
// otherwise we can optimize performance by not fetching it
metadata: !!space.gitSync,
})
);
return {
...baseContext,
+82 -115
View File
@@ -2,6 +2,7 @@ import { type ComputedContentSource, GitBookAPI } from '@gitbook/api';
import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags';
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 { wrapDataFetcherError } from './errors';
import type { GitBookDataFetcher } from './types';
interface DataFetcherInput {
@@ -156,16 +157,10 @@ async function getUserById(input: DataFetcherInput, userId: string) {
cacheLife('days');
try {
return wrapDataFetcherError(async () => {
const res = await getAPI(input).users.getUserById(userId);
return res.data;
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return null;
}
throw error;
}
});
}
async function getSpace(
@@ -185,10 +180,12 @@ async function getSpace(
})
);
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
shareKey: params.shareKey,
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
shareKey: params.shareKey,
});
return res.data;
});
return res.data;
}
async function getChangeRequest(
@@ -202,7 +199,7 @@ async function getChangeRequest(
cacheLife('minutes');
try {
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getChangeRequestById(
params.spaceId,
params.changeRequestId
@@ -215,13 +212,7 @@ async function getChangeRequest(
})
);
return res.data;
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return null;
}
throw error;
}
});
}
async function getRevision(
@@ -236,10 +227,12 @@ async function getRevision(
cacheLife('max');
const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, {
metadata: params.metadata,
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, {
metadata: params.metadata,
});
return res.data;
});
return res.data;
}
async function getRevisionPages(
@@ -254,14 +247,16 @@ async function getRevisionPages(
cacheLife('max');
const res = await getAPI(input).spaces.listPagesInRevisionById(
params.spaceId,
params.revisionId,
{
metadata: params.metadata,
}
);
return res.data.pages;
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.listPagesInRevisionById(
params.spaceId,
params.revisionId,
{
metadata: params.metadata,
}
);
return res.data.pages;
});
}
async function getRevisionFile(
@@ -276,20 +271,14 @@ async function getRevisionFile(
cacheLife('max');
try {
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getFileInRevisionById(
params.spaceId,
params.revisionId,
params.fileId
);
return res.data;
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return null;
}
throw error;
}
});
}
async function getRevisionPageByPath(
@@ -305,7 +294,7 @@ async function getRevisionPageByPath(
cacheLife('max');
const encodedPath = encodeURIComponent(params.path);
try {
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getPageInRevisionByPath(
params.spaceId,
params.revisionId,
@@ -313,13 +302,7 @@ async function getRevisionPageByPath(
);
return res.data;
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return null;
}
throw error;
}
});
}
async function getDocument(
@@ -333,8 +316,10 @@ async function getDocument(
cacheLife('max');
const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId);
return res.data;
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId);
return res.data;
});
}
async function getComputedDocument(
@@ -359,10 +344,12 @@ async function getComputedDocument(
)
);
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
source: params.source,
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
source: params.source,
});
return res.data;
});
return res.data;
}
async function getReusableContent(
@@ -377,20 +364,14 @@ async function getReusableContent(
cacheLife('max');
try {
return wrapDataFetcherError(async () => {
const res = await getAPI(input).spaces.getReusableContentInRevisionById(
params.spaceId,
params.revisionId,
params.reusableContentId
);
return res.data;
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return null;
}
throw error;
}
});
}
async function getLatestOpenAPISpecVersionContent(
@@ -411,19 +392,13 @@ async function getLatestOpenAPISpecVersionContent(
);
cacheLife('days');
try {
return wrapDataFetcherError(async () => {
const res = await getAPI(input).orgs.getLatestOpenApiSpecVersionContent(
params.organizationId,
params.slug
);
return res.data;
} catch (error) {
if (checkHasErrorCode(error, 404)) {
return null;
}
throw error;
}
});
}
async function getPublishedContentByUrl(
@@ -447,22 +422,24 @@ async function getPublishedContentByUrl(
);
cacheLife('days');
const res = await getAPI(input).urls.getPublishedContentByUrl({
url,
visitorAuthToken: visitorAuthToken ?? undefined,
redirectOnError,
return wrapDataFetcherError(async () => {
const res = await getAPI(input).urls.getPublishedContentByUrl({
url,
visitorAuthToken: visitorAuthToken ?? undefined,
redirectOnError,
});
if ('site' in res.data) {
cacheTag(
getCacheTag({
tag: 'site',
site: res.data.site,
})
);
}
return res.data;
});
if ('site' in res.data) {
cacheTag(
getCacheTag({
tag: 'site',
site: res.data.site,
})
);
}
return res.data;
}
async function getPublishedContentSite(
@@ -483,14 +460,16 @@ async function getPublishedContentSite(
);
cacheLife('days');
const res = await getAPI(input).orgs.getPublishedContentSite(
params.organizationId,
params.siteId,
{
shareKey: params.siteShareKey,
}
);
return res.data;
return wrapDataFetcherError(async () => {
const res = await getAPI(input).orgs.getPublishedContentSite(
params.organizationId,
params.siteId,
{
shareKey: params.siteShareKey,
}
);
return res.data;
});
}
async function getSiteRedirectBySource(
@@ -512,7 +491,7 @@ async function getSiteRedirectBySource(
);
cacheLife('days');
try {
return wrapDataFetcherError(async () => {
const res = await getAPI(input).orgs.getSiteRedirectBySource(
params.organizationId,
params.siteId,
@@ -523,19 +502,7 @@ async function getSiteRedirectBySource(
);
return res.data;
} catch (error) {
// 422 is returned when the source is invalid
// we don't want to throw but just return null
if (checkHasErrorCode(error, 422)) {
return null;
}
if (checkHasErrorCode(error, 404)) {
return null;
}
throw error;
}
});
}
async function getEmbedByUrl(
@@ -549,9 +516,11 @@ async function getEmbedByUrl(
cacheLife('weeks');
const api = getAPI(input);
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
return res.data;
return wrapDataFetcherError(async () => {
const api = getAPI(input);
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
return res.data;
});
}
async function searchSiteContent(
@@ -564,11 +533,13 @@ async function searchSiteContent(
cacheLife('days');
const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
query,
...scope,
return wrapDataFetcherError(async () => {
const res = await getAPI(input).orgs.searchSiteContent(organizationId, siteId, {
query,
...scope,
});
return res.data.items;
});
return res.data.items;
}
function getAPI(input: DataFetcherInput) {
@@ -581,7 +552,3 @@ function getAPI(input: DataFetcherInput) {
return api;
}
function checkHasErrorCode(error: unknown, code: number) {
return error instanceof Error && 'code' in error && error.code === code;
}
+112
View File
@@ -0,0 +1,112 @@
import { GitBookAPIError } from '@gitbook/api';
import type { DataFetcherErrorData, DataFetcherResponse } from './types';
export class DataFetcherError extends Error {
constructor(
message: string,
public readonly code: number
) {
super(message);
}
}
/**
* Throw an error if the response contains an error.
*/
export function throwIfDataError<T>(response: DataFetcherResponse<T>): T;
export function throwIfDataError<T>(response: Promise<DataFetcherResponse<T>>): Promise<T>;
export function throwIfDataError<T>(
response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>
): T | Promise<T> {
if (response instanceof Promise) {
return response.then((result) => throwIfDataError(result));
}
if (response.error) {
throw new DataFetcherError(response.error.message, response.error.code);
}
return response.data;
}
/**
* Get the data from the response or null if there is an "Not found" error.
*/
export function getDataOrNull<T>(
response: DataFetcherResponse<T>,
ignoreErrors?: number[]
): T | null;
export function getDataOrNull<T>(
response: Promise<DataFetcherResponse<T>>,
ignoreErrors?: number[]
): Promise<T | null>;
export function getDataOrNull<T>(
response: DataFetcherResponse<T> | Promise<DataFetcherResponse<T>>,
ignoreErrors: number[] = [404]
): T | null | Promise<T | null> {
if (response instanceof Promise) {
return response.then((result) => getDataOrNull(result, ignoreErrors));
}
if (response.error) {
if (ignoreErrors.includes(response.error.code)) return null;
throw new DataFetcherError(response.error.message, response.error.code);
}
return response.data;
}
/**
* Ignore error for an API or data call.
*/
export async function ignoreDataThrownError<T>(promise: Promise<T>): Promise<T | null> {
try {
return await promise;
} catch (error) {
getExposableError(error as Error);
return null;
}
}
/**
* Ignore all errors for an API or data call.
*/
export async function ignoreAllThrownError<T>(promise: Promise<T>): Promise<T | null> {
try {
return await promise;
} catch (error) {
console.warn('Ignoring error', error);
return null;
}
}
/**
* Wrap an async execution to handle errors and return a DataFetcherResponse.
*/
export async function wrapDataFetcherError<T>(
fn: () => Promise<T>
): Promise<DataFetcherResponse<T>> {
try {
return { data: await fn() };
} catch (error) {
return {
error: getExposableError(error as Error),
};
}
}
function getExposableError(error: Error): DataFetcherErrorData {
if (error instanceof GitBookAPIError) {
return {
code: error.code,
message: error.errorMessage,
};
}
if (error instanceof DataFetcherError) {
return {
code: error.code,
message: error.message,
};
}
throw error;
}
+2 -1
View File
@@ -1,4 +1,5 @@
export * from './api';
export * from './types';
export * from './utils';
export * from './pages';
export * from './urls';
export * from './errors';
+29
View File
@@ -0,0 +1,29 @@
import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
import { getDataOrNull } from './errors';
import type { GitBookDataFetcher } from './types';
/**
* Get the document for a page.
*/
export async function getPageDocument(
dataFetcher: GitBookDataFetcher,
space: Space,
page: RevisionPageDocument
): Promise<JSONDocument | null> {
if (page.documentId) {
return getDataOrNull(
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
);
}
if (page.computed) {
return getDataOrNull(
dataFetcher.getComputedDocument({
organizationId: space.organization,
spaceId: space.id,
source: page.computed,
})
);
}
return null;
}
+37 -16
View File
@@ -1,5 +1,20 @@
import type * as api from '@gitbook/api';
export type DataFetcherErrorData = {
code: number;
message: string;
};
export type DataFetcherResponse<T> =
| {
data: T;
error?: undefined;
}
| {
error: DataFetcherErrorData;
data?: undefined;
};
/**
* Generic fetcher for GitBook data.
* It is used between v1 and v2.
@@ -25,7 +40,7 @@ export interface GitBookDataFetcher {
/**
* Get a user by its ID.
*/
getUserById(userId: string): Promise<api.User | null>;
getUserById(userId: string): Promise<DataFetcherResponse<api.User>>;
/**
* Get a published content by its URL.
@@ -34,7 +49,7 @@ export interface GitBookDataFetcher {
url: string;
visitorAuthToken: string | null;
redirectOnError: boolean;
}): Promise<api.PublishedSiteContentLookup>;
}): Promise<DataFetcherResponse<api.PublishedSiteContentLookup>>;
/**
* Get a published content site by its organization ID and site ID.
@@ -43,12 +58,14 @@ export interface GitBookDataFetcher {
organizationId: string;
siteId: string;
siteShareKey: string | undefined;
}): Promise<api.PublishedContentSite>;
}): Promise<DataFetcherResponse<api.PublishedContentSite>>;
/**
* Get a space by its ID.
*/
getSpace(params: { spaceId: string; shareKey: string | undefined }): Promise<api.Space>;
getSpace(params: { spaceId: string; shareKey: string | undefined }): Promise<
DataFetcherResponse<api.Space>
>;
/**
* Get a change request by its space ID and change request ID.
@@ -56,7 +73,7 @@ export interface GitBookDataFetcher {
getChangeRequest(params: {
spaceId: string;
changeRequestId: string;
}): Promise<api.ChangeRequest | null>;
}): Promise<DataFetcherResponse<api.ChangeRequest>>;
/**
* Get the revision by its space ID and revision ID.
@@ -65,7 +82,7 @@ export interface GitBookDataFetcher {
spaceId: string;
revisionId: string;
metadata: boolean;
}): Promise<api.Revision>;
}): Promise<DataFetcherResponse<api.Revision>>;
/**
* Get the revision pages by its space ID and revision ID.
@@ -74,7 +91,7 @@ export interface GitBookDataFetcher {
spaceId: string;
revisionId: string;
metadata: boolean;
}): Promise<api.RevisionPage[]>;
}): Promise<DataFetcherResponse<api.RevisionPage[]>>;
/**
* Get a revision file by its space ID, revision ID and file ID.
@@ -83,7 +100,7 @@ export interface GitBookDataFetcher {
spaceId: string;
revisionId: string;
fileId: string;
}): Promise<api.RevisionFile | null>;
}): Promise<DataFetcherResponse<api.RevisionFile>>;
/**
* Get a revision page by its path.
@@ -92,12 +109,14 @@ export interface GitBookDataFetcher {
spaceId: string;
revisionId: string;
path: string;
}): Promise<api.RevisionPageDocument | api.RevisionPageGroup | null>;
}): Promise<DataFetcherResponse<api.RevisionPageDocument | api.RevisionPageGroup>>;
/**
* Get a document by its space ID and document ID.
*/
getDocument(params: { spaceId: string; documentId: string }): Promise<api.JSONDocument>;
getDocument(params: { spaceId: string; documentId: string }): Promise<
DataFetcherResponse<api.JSONDocument>
>;
/**
* Get a computed document by its space ID and computed source.
@@ -106,7 +125,7 @@ export interface GitBookDataFetcher {
organizationId: string;
spaceId: string;
source: api.ComputedContentSource;
}): Promise<api.JSONDocument>;
}): Promise<DataFetcherResponse<api.JSONDocument>>;
/**
* Get a reusable content by its space ID, revision ID and reusable content ID.
@@ -115,7 +134,7 @@ export interface GitBookDataFetcher {
spaceId: string;
revisionId: string;
reusableContentId: string;
}): Promise<api.RevisionReusableContent | null>;
}): Promise<DataFetcherResponse<api.RevisionReusableContent>>;
/**
* Get the latest OpenAPI spec version content by its organization ID and slug.
@@ -123,7 +142,7 @@ export interface GitBookDataFetcher {
getLatestOpenAPISpecVersionContent(params: {
organizationId: string;
slug: string;
}): Promise<api.OpenAPISpecContent | null>;
}): Promise<DataFetcherResponse<api.OpenAPISpecContent>>;
/**
* Get a site redirect by its source path.
@@ -133,12 +152,14 @@ export interface GitBookDataFetcher {
siteId: string;
siteShareKey: string | undefined;
source: string;
}): Promise<{ redirect: api.SiteRedirect | null; target: string } | null>;
}): Promise<DataFetcherResponse<{ redirect: api.SiteRedirect | null; target: string }>>;
/**
* Get an embed by its URL.
*/
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<api.Embed>;
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<
DataFetcherResponse<api.Embed>
>;
/**
* Search content in a site.
@@ -153,5 +174,5 @@ export interface GitBookDataFetcher {
| { mode: 'specific'; siteSpaceIds: string[] };
/** Cache bust to ensure the search results are fresh when the space is updated. */
cacheBust?: string;
}): Promise<api.SearchSpaceResult[]>;
}): Promise<DataFetcherResponse<api.SearchSpaceResult[]>>;
}
-24
View File
@@ -1,24 +0,0 @@
import type { RevisionPageDocument, Space } from '@gitbook/api';
import type { GitBookDataFetcher } from './types';
/**
* Get the document for a page.
*/
export async function getPageDocument(
dataFetcher: GitBookDataFetcher,
space: Space,
page: RevisionPageDocument
) {
if (page.documentId) {
return dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId });
}
if (page.computed) {
return dataFetcher.getComputedDocument({
organizationId: space.organization,
spaceId: space.id,
source: page.computed,
});
}
return null;
}
+6 -6
View File
@@ -33,12 +33,12 @@ export async function middleware(request: NextRequest) {
* In GitBook v2: image resizing is done at the content level (docs.company.com/section/variant/~gitbook/image)
*/
if (extracted.url.pathname.endsWith('/~gitbook/image')) {
return serveResizedImage(request, {
return await serveResizedImage(request, {
host: extracted.url.host,
});
}
return serveSiteByURL(request, extracted);
return await serveSiteByURL(request, extracted);
}
// Handle the rest with the router default logic
@@ -147,10 +147,10 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
*/
function serveErrorResponse(error: Error) {
if (error instanceof GitBookAPIError) {
return NextResponse.json(
{ error: error.message },
{ status: 500, headers: { 'content-type': 'application/json' } }
);
return new Response(error.errorMessage, {
status: error.code,
headers: { 'content-type': 'text/plain' },
});
}
throw error;
@@ -4,6 +4,7 @@ import { headers } from 'next/headers';
import React from 'react';
import { tcls } from '@/lib/tailwind';
import { throwIfDataError } from '@v2/lib/data';
import { DateRelative } from '../primitives';
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
@@ -107,11 +108,13 @@ async function RevisionToolbar(props: { context: GitBookSiteContext }) {
const { context } = props;
const { space, revisionId } = context;
const revision = await context.dataFetcher.getRevision({
spaceId: space.id,
revisionId,
metadata: true,
});
const revision = await throwIfDataError(
context.dataFetcher.getRevision({
spaceId: space.id,
revisionId,
metadata: true,
})
);
return (
<ToolbarLayout>
@@ -5,6 +5,7 @@ import ReactDOM from 'react-dom';
import { Card } from '@/components/primitives';
import { tcls } from '@/lib/tailwind';
import { getDataOrNull } from '@v2/lib/data';
import type { BlockProps } from './Block';
import { Caption } from './Caption';
import { IntegrationBlock } from './Integration';
@@ -18,10 +19,16 @@ export async function Embed(props: BlockProps<gitbookAPI.DocumentBlockEmbed>) {
ReactDOM.preload('https://cdn.iframe.ly/embed.js', { as: 'script' });
const embed = await context.contentContext.dataFetcher.getEmbedByUrl({
url: block.data.url,
spaceId: context.contentContext.space?.id,
});
const embed = await getDataOrNull(
context.contentContext.dataFetcher.getEmbedByUrl({
url: block.data.url,
spaceId: context.contentContext.space?.id,
})
);
if (!embed) {
return null;
}
return (
<Caption {...props} withBorder>
@@ -6,7 +6,7 @@ import {
type ContentKitServerContext,
} from '@gitbook/react-contentkit';
import { ignoreAPIError, renderIntegrationUi } from '@/lib/api';
import { renderIntegrationUi } from '@/lib/api';
import { parseMarkdown } from '@/lib/markdown';
import { tcls } from '@/lib/tailwind';
import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
@@ -14,6 +14,7 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env';
import type { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import './contentkit.css';
import { ignoreAllThrownError } from '@v2/lib/data';
const outputContext: ContentKitServerContext = {
icons: {
@@ -63,9 +64,8 @@ export async function IntegrationBlock(props: BlockProps<DocumentBlockIntegratio
context: contentKitContext,
};
const initialOutput = await ignoreAPIError(
renderIntegrationUi(block.data.integration, initialInput),
true
const initialOutput = await ignoreAllThrownError(
renderIntegrationUi(block.data.integration, initialInput)
);
if (!initialOutput || initialOutput.type === 'complete') {
return null;
@@ -2,6 +2,7 @@ import type { DocumentBlockReusableContent } from '@gitbook/api';
import { resolveContentRef } from '@/lib/references';
import { getDataOrNull } from '@v2/lib/data';
import type { BlockProps } from './Block';
import { UnwrappedBlocks } from './Blocks';
@@ -17,10 +18,12 @@ export async function ReusableContent(props: BlockProps<DocumentBlockReusableCon
return null;
}
const document = await context.contentContext.dataFetcher.getDocument({
spaceId: context.contentContext.space.id,
documentId: resolved.reusableContent.document,
});
const document = await getDataOrNull(
context.contentContext.dataFetcher.getDocument({
spaceId: context.contentContext.space.id,
documentId: resolved.reusableContent.document,
})
);
if (!document) {
return null;
@@ -20,6 +20,7 @@ import { filterOutNullable } from '@/lib/typescript';
import { getV1BaseContext } from '@/lib/v1';
import { isV2 } from '@/lib/v2';
import { throwIfDataError } from '@v2/lib/data';
import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
import { DocumentView } from '../DocumentView';
@@ -133,11 +134,13 @@ export async function streamAskQuestion({
if (!spacePromises.has(source.space)) {
spacePromises.set(
source.space,
context.dataFetcher.getRevisionPages({
spaceId: source.space,
revisionId: source.revision,
metadata: false,
})
throwIfDataError(
context.dataFetcher.getRevisionPages({
spaceId: source.space,
revisionId: source.revision,
metadata: false,
})
)
);
}
@@ -229,17 +232,21 @@ async function searchSiteContent(
}
const [searchResults, { structure }] = await Promise.all([
dataFetcher.searchSiteContent({
organizationId: siteURLData.organization,
siteId: siteURLData.site,
query,
scope,
}),
dataFetcher.getPublishedContentSite({
organizationId: siteURLData.organization,
siteId: siteURLData.site,
siteShareKey: siteURLData.shareKey,
}),
throwIfDataError(
dataFetcher.searchSiteContent({
organizationId: siteURLData.organization,
siteId: siteURLData.site,
query,
scope,
})
),
throwIfDataError(
dataFetcher.getPublishedContentSite({
organizationId: siteURLData.organization,
siteId: siteURLData.site,
siteShareKey: siteURLData.shareKey,
})
),
]);
return (
@@ -2,6 +2,7 @@ import type { GitBookSiteContext } from '@v2/lib/context';
import { redirect } from 'next/navigation';
import { resolvePageId, resolvePagePath } from '@/lib/pages';
import { getDataOrNull } from '@v2/lib/data';
export interface PagePathParams {
pathname?: string | string[];
@@ -54,22 +55,26 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
// If page can't be found, we try with the API, in case we have a redirect at space level.
// We use the raw pathname to handle special/malformed redirects setup by users in the GitSync.
// The page rendering will take care of redirecting to a normalized pathname.
const resolved = await context.dataFetcher.getRevisionPageByPath({
spaceId: space.id,
revisionId,
path: rawPathname,
});
const resolved = await getDataOrNull(
context.dataFetcher.getRevisionPageByPath({
spaceId: space.id,
revisionId,
path: rawPathname,
})
);
if (resolved) {
return resolvePageId(pages, resolved.id);
}
// If a page still can't be found, we try with the API, in case we have a redirect at site level.
const resolvedSiteRedirect = await context.dataFetcher.getSiteRedirectBySource({
organizationId,
siteId: site.id,
source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`,
siteShareKey: shareKey,
});
const resolvedSiteRedirect = await getDataOrNull(
context.dataFetcher.getSiteRedirectBySource({
organizationId,
siteId: site.id,
source: rawPathname.startsWith('/') ? rawPathname : `/${rawPathname}`,
siteShareKey: shareKey,
})
);
if (resolvedSiteRedirect) {
return redirect(resolvedSiteRedirect.target);
}
-61
View File
@@ -14,7 +14,6 @@ import {
type RevisionFile,
type RevisionReusableContent,
} from '@gitbook/api';
import type { GitBookDataFetcher } from '@v2/lib/data/types';
import { headers } from 'next/headers';
import { batch } from './async';
@@ -865,50 +864,6 @@ export const getPublishedContentSite = cache({
},
});
/**
* Fetch all the content data about a space at once.
* This function executes the requests in parallel and should be used as early as possible
* instead of calling the individual functions.
*
* @deprecated - use 'fetchSpaceContextByIds' from v2
*/
export async function getSpaceContentData(
dataFetcher: GitBookDataFetcher,
pointer: SpaceContentPointer,
shareKey: string | undefined
) {
const [space, changeRequest] = await Promise.all([
dataFetcher.getSpace({
spaceId: pointer.spaceId,
shareKey,
}),
pointer.changeRequestId
? dataFetcher.getChangeRequest({
spaceId: pointer.spaceId,
changeRequestId: pointer.changeRequestId,
})
: null,
]);
const contentTarget: ContentTarget = {
spaceId: pointer.spaceId,
revisionId: changeRequest?.revision ?? pointer.revisionId ?? space.revision,
};
const pages = await dataFetcher.getRevisionPages({
spaceId: pointer.spaceId,
revisionId: contentTarget.revisionId,
// We only care about the Git metadata when the Git sync is enabled,
// otherwise we can optimize performance by not fetching it
metadata: !!space.gitSync,
});
return {
space,
pages,
contentTarget,
};
}
/**
* Search content in a Site or specific SiteSpaces.
*/
@@ -1009,22 +964,6 @@ export function userAgent(): string {
return result;
}
/**
* Ignore error for an API call.
*/
export async function ignoreAPIError<T>(promise: Promise<T>, ignoreAll = false): Promise<T | null> {
try {
return await promise;
} catch (error) {
const code = (error as GitBookAPIError).code;
if (ignoreAll || (code >= 400 && code < 500)) {
return null;
}
throw error;
}
}
/**
* Iterate over a paginated API endpoint and return all the items.
*/
+29 -22
View File
@@ -7,14 +7,13 @@ import type {
} from '@gitbook/api';
import type { Filesystem } from '@gitbook/openapi-parser';
import { type GitBookAnyContext, fetchSpaceContextByIds } from '@v2/lib/context';
import { getPageDocument } from '@v2/lib/data';
import { getDataOrNull, getPageDocument, ignoreDataThrownError } from '@v2/lib/data';
import { createLinker } from '@v2/lib/links';
import assertNever from 'assert-never';
import type React from 'react';
import { PageIcon } from '@/components/PageIcon';
import { ignoreAPIError } from './api';
import { getBlockById, getBlockTitle } from './document';
import { getGitbookAppHref } from './links';
import { resolvePageId } from './pages';
@@ -84,11 +83,13 @@ export async function resolveContentRef(
}
case 'file': {
const file = await dataFetcher.getRevisionFile({
spaceId: space.id,
revisionId,
fileId: contentRef.file,
});
const file = await getDataOrNull(
dataFetcher.getRevisionFile({
spaceId: space.id,
revisionId,
fileId: contentRef.file,
})
);
if (file) {
return {
href: file.downloadURL,
@@ -189,7 +190,7 @@ export async function resolveContentRef(
}
case 'user': {
const user = await dataFetcher.getUserById(contentRef.user);
const user = await getDataOrNull(dataFetcher.getUserById(contentRef.user));
if (user) {
return {
href: `mailto:${user.email}`,
@@ -209,11 +210,13 @@ export async function resolveContentRef(
}
case 'reusable-content': {
const reusableContent = await dataFetcher.getReusableContent({
spaceId: space.id,
revisionId,
reusableContentId: contentRef.reusableContent,
});
const reusableContent = await getDataOrNull(
dataFetcher.getReusableContent({
spaceId: space.id,
revisionId,
reusableContentId: contentRef.reusableContent,
})
);
if (!reusableContent) {
return null;
}
@@ -226,10 +229,12 @@ export async function resolveContentRef(
}
case 'openapi': {
const openAPISpecVersionContent = await dataFetcher.getLatestOpenAPISpecVersionContent({
organizationId: context.organizationId,
slug: contentRef.spec,
});
const openAPISpecVersionContent = await getDataOrNull(
dataFetcher.getLatestOpenAPISpecVersionContent({
organizationId: context.organizationId,
slug: contentRef.spec,
})
);
if (!openAPISpecVersionContent) {
return null;
@@ -258,19 +263,21 @@ async function getBestTargetSpace(
const { dataFetcher } = context;
const [fetchedSpace, publishedContentSite] = await Promise.all([
ignoreAPIError(
getDataOrNull(
dataFetcher.getSpace({
spaceId,
shareKey: context?.shareKey,
})
}),
[404, 403]
),
'site' in context
? ignoreAPIError(
? getDataOrNull(
dataFetcher.getPublishedContentSite({
organizationId: context.organizationId,
siteId: context.site.id,
siteShareKey: context.shareKey,
})
}),
[404, 403]
)
: null,
]);
@@ -294,7 +301,7 @@ async function resolveContentRefInSpace(
contentRef: ContentRef
) {
const [spaceContext, bestTargetSpace] = await Promise.all([
ignoreAPIError(
ignoreDataThrownError(
fetchSpaceContextByIds(context, {
space: spaceId,
shareKey: context?.shareKey,
+124 -25
View File
@@ -8,6 +8,7 @@ import type { GitBookDataFetcher } from '@v2/lib/data/types';
import { createImageResizer } from '@v2/lib/images';
import { createLinker } from '@v2/lib/links';
import { DataFetcherError, wrapDataFetcherError } from '@v2/lib/data';
import {
type SiteContentPointer,
type SpaceContentPointer,
@@ -89,78 +90,176 @@ async function getDataFetcherV1(): Promise<GitBookDataFetcher> {
},
getUserById(userId) {
return getUserById(userId);
return wrapDataFetcherError(async () => {
const user = await getUserById(userId);
if (!user) {
throw new DataFetcherError('User not found', 404);
}
return user;
});
},
// @ts-ignore - types are compatible enough, and this will not be called in v1 this way
getPublishedContentByUrl(params) {
return getPublishedContentByUrl(
params.url,
params.visitorAuthToken ?? undefined,
params.redirectOnError ? true : undefined
);
return wrapDataFetcherError(async () => {
return getPublishedContentByUrl(
params.url,
params.visitorAuthToken ?? undefined,
params.redirectOnError ? true : undefined
);
});
},
getPublishedContentSite(params) {
return getPublishedContentSite(params);
return wrapDataFetcherError(async () => {
return getPublishedContentSite(params);
});
},
getSpace(params) {
return getSpace(params.spaceId, params.shareKey);
return wrapDataFetcherError(async () => {
return getSpace(params.spaceId, params.shareKey);
});
},
getChangeRequest(params) {
return getChangeRequest(params.spaceId, params.changeRequestId);
return wrapDataFetcherError(async () => {
const changeRequest = await getChangeRequest(
params.spaceId,
params.changeRequestId
);
if (!changeRequest) {
throw new DataFetcherError('Change request not found', 404);
}
return changeRequest;
});
},
getRevision(params) {
return getRevision(params.spaceId, params.revisionId, {
metadata: params.metadata,
return wrapDataFetcherError(async () => {
return getRevision(params.spaceId, params.revisionId, {
metadata: params.metadata,
});
});
},
getRevisionFile(params) {
return getRevisionFile(params.spaceId, params.revisionId, params.fileId);
return wrapDataFetcherError(async () => {
const revisionFile = await getRevisionFile(
params.spaceId,
params.revisionId,
params.fileId
);
if (!revisionFile) {
throw new DataFetcherError('Revision file not found', 404);
}
return revisionFile;
});
},
getDocument(params) {
return getDocument(params.spaceId, params.documentId);
return wrapDataFetcherError(async () => {
const document = await getDocument(params.spaceId, params.documentId);
if (!document) {
throw new DataFetcherError('Document not found', 404);
}
return document;
});
},
getComputedDocument(params) {
return getComputedDocument(params.organizationId, params.spaceId, params.source);
return wrapDataFetcherError(() => {
return getComputedDocument(params.organizationId, params.spaceId, params.source);
});
},
getRevisionPages(params) {
return getRevisionPages(params.spaceId, params.revisionId, {
metadata: params.metadata,
return wrapDataFetcherError(async () => {
return getRevisionPages(params.spaceId, params.revisionId, {
metadata: params.metadata,
});
});
},
getRevisionPageByPath(params) {
return getRevisionPageByPath(params.spaceId, params.revisionId, params.path);
return wrapDataFetcherError(async () => {
const revisionPage = await getRevisionPageByPath(
params.spaceId,
params.revisionId,
params.path
);
if (!revisionPage) {
throw new DataFetcherError('Revision page not found', 404);
}
return revisionPage;
});
},
getReusableContent(params) {
return getReusableContent(params.spaceId, params.revisionId, params.reusableContentId);
return wrapDataFetcherError(async () => {
const reusableContent = await getReusableContent(
params.spaceId,
params.revisionId,
params.reusableContentId
);
if (!reusableContent) {
throw new DataFetcherError('Reusable content not found', 404);
}
return reusableContent;
});
},
getLatestOpenAPISpecVersionContent(params) {
return getLatestOpenAPISpecVersionContent(params.organizationId, params.slug);
return wrapDataFetcherError(async () => {
const openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
params.organizationId,
params.slug
);
if (!openAPISpecVersionContent) {
throw new DataFetcherError('OpenAPI spec version content not found', 404);
}
return openAPISpecVersionContent;
});
},
getSiteRedirectBySource(params) {
return getSiteRedirectBySource(params);
},
return wrapDataFetcherError(async () => {
const siteRedirect = await getSiteRedirectBySource(params);
if (!siteRedirect) {
throw new DataFetcherError('Site redirect not found', 404);
}
return siteRedirect;
});
},
getEmbedByUrl(params) {
return getEmbedByUrlInSpace(params.spaceId, params.url);
return wrapDataFetcherError(() => {
return getEmbedByUrlInSpace(params.spaceId, params.url);
});
},
async searchSiteContent(params) {
const { organizationId, siteId, query, cacheBust, scope } = params;
const result = await searchSiteContent(organizationId, siteId, query, scope, cacheBust);
return result.items;
return wrapDataFetcherError(async () => {
const { organizationId, siteId, query, cacheBust, scope } = params;
const result = await searchSiteContent(
organizationId,
siteId,
query,
scope,
cacheBust
);
return result.items;
});
},
};
+8 -5
View File
@@ -8,6 +8,7 @@ import { joinPath } from '@/lib/paths';
import { getIndexablePages } from '@/lib/sitemap';
import { getSiteStructureSections } from '@/lib/sites';
import { type GitBookSiteContext, checkIsRootSiteContext } from '@v2/lib/context';
import { throwIfDataError } from '@v2/lib/data';
/**
* Generate a llms.txt file for the site.
@@ -106,11 +107,13 @@ async function getNodesFromSiteSpaces(
if (!siteSpaceUrl) {
return [];
}
const rootPages = await dataFetcher.getRevisionPages({
spaceId: siteSpace.space.id,
revisionId: siteSpace.space.revision,
metadata: false,
});
const rootPages = await throwIfDataError(
dataFetcher.getRevisionPages({
spaceId: siteSpace.space.id,
revisionId: siteSpace.space.revision,
metadata: false,
})
);
const pages = getIndexablePages(rootPages);
const listChildren = await Promise.all(
pages.map(async ({ page }): Promise<ListItem> => {