From 6c613d0df35a4381e557621982faeeb0f25a5884 Mon Sep 17 00:00:00 2001 From: Taran Vohra Date: Fri, 13 Mar 2026 13:52:09 +0530 Subject: [PATCH] Fix auth redirects with the new preview URL (#4111) --- packages/gitbook/e2e/internal.spec.ts | 38 ++++++++++++++++++ packages/gitbook/src/middleware.ts | 55 +++++++++++++++------------ 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 546ed3a5d..2c4f937bf 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -709,6 +709,26 @@ const testCases: TestsCase[] = [ await expect(page.locator('[data-testid="print-button"]')).toBeVisible(); }, }, + { + name: 'Show error when missing token', + url: async () => { + const data = await getSiteAPIToken( + 'https://gitbook.gitbook.io/test-gitbook-open/' + ); + + // Intentionally not setting the token to test error handling when the token is missing + const searchParams = new URLSearchParams(); + searchParams.set('limit', '10'); + + return `~space/${data.space}/~gitbook/pdf?${searchParams.toString()}`; + }, + screenshot: false, + run: async (page, response) => { + expect(response).not.toBeNull(); + expect(response?.status()).toBe(400); + await expect(page.getByText('Missing API token')).toBeVisible(); + }, + }, ], }, { @@ -887,6 +907,24 @@ const testCases: TestsCase[] = [ ).toBeVisible(); }, }, + { + name: 'Redirect to app for authentication when missing token', + url: async () => { + const data = await getSiteAPIToken('https://gitbook.com/docs'); + + const searchParams = new URLSearchParams(); + // Intentionally not setting the token to test redirection for authentication + + return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`; + }, + screenshot: false, + run: async (page) => { + await page.waitForURL( + (url) => + url.host === 'app.gitbook.com' && url.pathname.includes('/preview/auth') + ); + }, + }, ], }, { diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 17949fe65..11cdc5484 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -473,12 +473,14 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { if (isPreviewRequest(siteRequestURL)) { // Do not track page views for preview requests request.headers.set('x-gitbook-disable-tracking', 'true'); - return serveWithQueryAPIToken( + return serveWithQueryAPIToken({ // We scope the API token to the site ID. - ['preview', getPreviewRequestIdentifier(siteRequestURL)].join('/'), - request, - withAPIToken - ); + scopePath: ['preview', getPreviewRequestIdentifier(siteRequestURL)].join('/'), + // We keep the original request URL when using `url` mode + requestURL: mode === 'url' ? requestURL : siteRequestURL, + requestCookies: request.cookies, + serve: withAPIToken, + }); } return withAPIToken(null); @@ -493,18 +495,22 @@ async function serveSpacePDFRoutes(requestURL: URL, request: NextRequest) { return null; } - return serveWithQueryAPIToken( - pathnameParts.slice(0, 2).join('/'), - request, - async (apiToken) => { + return serveWithQueryAPIToken({ + scopePath: pathnameParts.slice(0, 2).join('/'), + requestURL, + requestCookies: request.cookies, + serve: async (apiToken) => { + if (!apiToken) { + throw new DataFetcherError('Missing API token', 400); + } // Handle the rest with the router default logic return NextResponse.next({ headers: { [MiddlewareHeaders.APIToken]: apiToken, }, }); - } - ); + }, + }); } /** @@ -522,23 +528,25 @@ function serveErrorResponse(error: Error) { } /** - * Server a response with an API token obtained from the query params. + * Serve a response with an API token obtained from the query params. */ -async function serveWithQueryAPIToken( - scopePath: string, - request: NextRequest, - serve: (apiToken: string) => Promise -) { +async function serveWithQueryAPIToken(input: { + scopePath: string; + requestURL: URL; + requestCookies: NextRequest['cookies']; + serve: (apiToken: string | null) => Promise; +}) { + const { scopePath, requestURL, requestCookies, serve } = input; // We store the API token in a cookie that is scoped to the specific route // to avoid errors when multiple previews are opened in different tabs. const cookieName = getPathScopedCookieName('gitbook-api-token', scopePath); // Extract a potential GitBook API token passed in the request // If found, we redirect to the same URL but with the token in the cookie - const queryAPIToken = request.nextUrl.searchParams.get('token'); + const queryAPIToken = requestURL.searchParams.get('token'); if (queryAPIToken) { - request.nextUrl.searchParams.delete('token'); - return writeResponseCookies(NextResponse.redirect(request.nextUrl.toString()), [ + requestURL.searchParams.delete('token'); + return writeResponseCookies(NextResponse.redirect(requestURL.toString()), [ { name: cookieName, value: queryAPIToken, @@ -552,12 +560,9 @@ async function serveWithQueryAPIToken( ]); } - const apiToken = request.cookies.get(cookieName)?.value; - if (!apiToken) { - throw new DataFetcherError('Missing API token', 400); - } + const apiToken = requestCookies.get(cookieName)?.value; - return serve(apiToken); + return serve(apiToken ?? null); } /**