diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3918431ba..09d50b80f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/bun.lock b/bun.lock index 9a33daac5..3c4dcd6a8 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 87d494926..a106ef5bb 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -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", diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/icon/route.ts b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/icon/route.ts index 8ae5e5af9..631a8d40b 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/icon/route.ts +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/icon/route.ts @@ -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 } diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index b74fb2786..040ea286a 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -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 { 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, diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index a3a254a28..b8cb1f5db 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -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; -} diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts new file mode 100644 index 000000000..0a5dff5ac --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/errors.ts @@ -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(response: DataFetcherResponse): T; +export function throwIfDataError(response: Promise>): Promise; +export function throwIfDataError( + response: DataFetcherResponse | Promise> +): T | Promise { + 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( + response: DataFetcherResponse, + ignoreErrors?: number[] +): T | null; +export function getDataOrNull( + response: Promise>, + ignoreErrors?: number[] +): Promise; +export function getDataOrNull( + response: DataFetcherResponse | Promise>, + ignoreErrors: number[] = [404] +): T | null | Promise { + 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(promise: Promise): Promise { + 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(promise: Promise): Promise { + 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( + fn: () => Promise +): Promise> { + 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; +} diff --git a/packages/gitbook-v2/src/lib/data/index.ts b/packages/gitbook-v2/src/lib/data/index.ts index aca6e3723..d2caca58b 100644 --- a/packages/gitbook-v2/src/lib/data/index.ts +++ b/packages/gitbook-v2/src/lib/data/index.ts @@ -1,4 +1,5 @@ export * from './api'; export * from './types'; -export * from './utils'; +export * from './pages'; export * from './urls'; +export * from './errors'; diff --git a/packages/gitbook-v2/src/lib/data/pages.ts b/packages/gitbook-v2/src/lib/data/pages.ts new file mode 100644 index 000000000..0b0121b01 --- /dev/null +++ b/packages/gitbook-v2/src/lib/data/pages.ts @@ -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 { + 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; +} diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index b86fb09fc..166909591 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -1,5 +1,20 @@ import type * as api from '@gitbook/api'; +export type DataFetcherErrorData = { + code: number; + message: string; +}; + +export type DataFetcherResponse = + | { + 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; + getUserById(userId: string): Promise>; /** * Get a published content by its URL. @@ -34,7 +49,7 @@ export interface GitBookDataFetcher { url: string; visitorAuthToken: string | null; redirectOnError: boolean; - }): Promise; + }): Promise>; /** * 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; + }): Promise>; /** * Get a space by its ID. */ - getSpace(params: { spaceId: string; shareKey: string | undefined }): Promise; + getSpace(params: { spaceId: string; shareKey: string | undefined }): Promise< + DataFetcherResponse + >; /** * 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; + }): Promise>; /** * Get the revision by its space ID and revision ID. @@ -65,7 +82,7 @@ export interface GitBookDataFetcher { spaceId: string; revisionId: string; metadata: boolean; - }): Promise; + }): Promise>; /** * 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; + }): Promise>; /** * 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; + }): Promise>; /** * Get a revision page by its path. @@ -92,12 +109,14 @@ export interface GitBookDataFetcher { spaceId: string; revisionId: string; path: string; - }): Promise; + }): Promise>; /** * Get a document by its space ID and document ID. */ - getDocument(params: { spaceId: string; documentId: string }): Promise; + getDocument(params: { spaceId: string; documentId: string }): Promise< + DataFetcherResponse + >; /** * 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; + }): Promise>; /** * 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; + }): Promise>; /** * 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; + }): Promise>; /** * 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>; /** * Get an embed by its URL. */ - getEmbedByUrl(params: { url: string; spaceId: string }): Promise; + getEmbedByUrl(params: { url: string; spaceId: string }): Promise< + DataFetcherResponse + >; /** * 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; + }): Promise>; } diff --git a/packages/gitbook-v2/src/lib/data/utils.ts b/packages/gitbook-v2/src/lib/data/utils.ts deleted file mode 100644 index 4e7f4a436..000000000 --- a/packages/gitbook-v2/src/lib/data/utils.ts +++ /dev/null @@ -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; -} diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index bfcc70ce4..5aba59c60 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -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; diff --git a/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx b/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx index 1bab745b8..5ecdf7717 100644 --- a/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx +++ b/packages/gitbook/src/components/AdminToolbar/AdminToolbar.tsx @@ -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 ( diff --git a/packages/gitbook/src/components/DocumentView/Embed.tsx b/packages/gitbook/src/components/DocumentView/Embed.tsx index 5ca643596..796cdaeee 100644 --- a/packages/gitbook/src/components/DocumentView/Embed.tsx +++ b/packages/gitbook/src/components/DocumentView/Embed.tsx @@ -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) { 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 ( diff --git a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx index 831988cc3..28dd4ef6b 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx @@ -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(promise: Promise, ignoreAll = false): Promise { - 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. */ diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index e2f6416ef..caff358e3 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -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, diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts index d4314c896..3ad2f54c1 100644 --- a/packages/gitbook/src/lib/v1.ts +++ b/packages/gitbook/src/lib/v1.ts @@ -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 { }, 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; + }); }, }; diff --git a/packages/gitbook/src/routes/llms.ts b/packages/gitbook/src/routes/llms.ts index 326973a4d..44f9b5d76 100644 --- a/packages/gitbook/src/routes/llms.ts +++ b/packages/gitbook/src/routes/llms.ts @@ -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 => {