diff --git a/src/Client.ts b/src/Client.ts index 0d8b54ac..b064ec2f 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -73,6 +73,13 @@ export const GET_ALL_QUERY_DELAY = 500 */ const DEFUALT_RETRY_AFTER_MS = 1000 +/** + * The maximum number of attemps to retry a query with an invalid ref before + * halting. We allow multiple attemps as each attemp may return a different ref. + * Capping the number of attemps prevents infinite loops. + */ +const MAX_INVALID_REF_RETRY_ATTEMPS = 3 + /** * Extracts one or more Prismic document types that match a given Prismic * document type. If no matches are found, no extraction is performed and the @@ -1696,6 +1703,7 @@ export class Client< */ private async _get( params?: Partial & FetchParams, + attemptCount = 0, ): Promise<{ data: Query; url: string }> { const url = await this.buildQueryURL(params) @@ -1705,7 +1713,10 @@ export class Client< return { data, url } } catch (error) { if ( - !(error instanceof RefNotFoundError || error instanceof RefExpiredError) + !( + error instanceof RefNotFoundError || error instanceof RefExpiredError + ) || + attemptCount >= MAX_INVALID_REF_RETRY_ATTEMPS - 1 ) { throw error } @@ -1722,7 +1733,7 @@ export class Client< `The ref (${badRef}) was ${issue}. Now retrying with the latest master ref (${masterRef}). If you were previewing content, the response will not include draft content.`, ) - return await this._get({ ...params, ref: masterRef }) + return await this._get({ ...params, ref: masterRef }, attemptCount + 1) } } diff --git a/test/__testutils__/testInvalidRefRetry.ts b/test/__testutils__/testInvalidRefRetry.ts index 05c428b2..8ef46c43 100644 --- a/test/__testutils__/testInvalidRefRetry.ts +++ b/test/__testutils__/testInvalidRefRetry.ts @@ -5,7 +5,7 @@ import { rest } from "msw" import { createTestClient } from "./createClient" import { mockPrismicRestAPIV2 } from "./mockPrismicRestAPIV2" -import type * as prismic from "../../src" +import * as prismic from "../../src" type TestInvalidRefRetryArgs = { run: ( @@ -95,4 +95,46 @@ export const testInvalidRefRetry = (args: TestInvalidRefRetryArgs): void => { expect(triedRefs).toStrictEqual([badRef, masterRef]) }, ) + + it.concurrent( + "throws if the maximum number of retries when an invalid ref is used is reached", + async (ctx) => { + const client = createTestClient({ ctx }) + const queryResponse = ctx.mock.api.query({ + documents: [ctx.mock.value.document()], + }) + + const triedRefs: (string | null)[] = [] + + mockPrismicRestAPIV2({ ctx, queryResponse }) + const endpoint = new URL( + "documents/search", + `${client.documentAPIEndpoint}/`, + ).toString() + ctx.server.use( + rest.get(endpoint, (req) => { + triedRefs.push(req.url.searchParams.get("ref")) + }), + rest.get(endpoint, (_req, res, requestCtx) => + res( + requestCtx.json({ + type: "api_notfound_error", + message: `Master ref is: ${ctx.mock.api.ref().ref}`, + }), + requestCtx.status(404), + ), + ), + ) + + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => void 0) + await expect(async () => { + await args.run(client) + }).rejects.toThrow(prismic.RefNotFoundError) + consoleWarnSpy.mockRestore() + + expect(triedRefs.length).toBe(3) + }, + ) }