From 26d5b0f022c61ed8711d99edce572bdbe013d3c9 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Thu, 5 Oct 2023 14:13:33 -1000 Subject: [PATCH] feat: support expired and not-found ref API errors (#327) * feat: support expired and not-found ref API errors * fix: extend `RefNotFoundError` and `RefExpiredError` from `ForbiddenError` for backwards compatibility --- src/createClient.ts | 21 ++++++++++--- src/errors/ForbiddenError.ts | 8 +++-- src/errors/NotFoundError.ts | 4 ++- src/errors/ParsingError.ts | 4 ++- src/errors/RefExpiredError.ts | 14 +++++++++ src/errors/RefNotFoundError.ts | 14 +++++++++ src/index.ts | 2 ++ test/client.test.ts | 56 +++++++++++++++++++++++++++++++++- 8 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 src/errors/RefExpiredError.ts create mode 100644 src/errors/RefNotFoundError.ts diff --git a/src/createClient.ts b/src/createClient.ts index 1929410d..18febe10 100644 --- a/src/createClient.ts +++ b/src/createClient.ts @@ -19,6 +19,8 @@ import { ForbiddenError } from "./errors/ForbiddenError"; import { NotFoundError } from "./errors/NotFoundError"; import { ParsingError } from "./errors/ParsingError"; import { PrismicError } from "./errors/PrismicError"; +import { RefExpiredError } from "./errors/RefExpiredError"; +import { RefNotFoundError } from "./errors/RefNotFoundError"; import { LinkResolverFunction, asLink } from "./helpers/asLink"; @@ -1900,22 +1902,33 @@ export class Client { // - Incorrect access token for query endpoint case 403: { throw new ForbiddenError( - "error" in res.json ? res.json.error : res.json.message, + res.json.error || res.json.message, url, res.json, ); } - // Not Found (this response has an empty body) - // - Incorrect repository name + // Not Found + // - Incorrect repository name (this response has an empty body) + // - Ref does not exist case 404: { + if (res.json && res.json.type === "api_notfound_error") { + throw new RefNotFoundError(res.json.message, url, res.json); + } + throw new NotFoundError( `Prismic repository not found. Check that "${this.endpoint}" is pointing to the correct repository.`, url, - undefined, + undefined, // res.json is empty ); } + // Gone + // - Ref is expired + case 410: { + throw new RefExpiredError(res.json.message, url, res.json); + } + // Too Many Requests // - Exceeded the maximum number of requests per second case 429: { diff --git a/src/errors/ForbiddenError.ts b/src/errors/ForbiddenError.ts index 32e21b02..ccb41c2c 100644 --- a/src/errors/ForbiddenError.ts +++ b/src/errors/ForbiddenError.ts @@ -9,6 +9,8 @@ type ForbiddenErrorQueryAPIResponse = { error: string; }; -export class ForbiddenError extends PrismicError< - ForbiddenErrorRepositoryAPIResponse | ForbiddenErrorQueryAPIResponse -> {} +export class ForbiddenError< + TResponse = + | ForbiddenErrorRepositoryAPIResponse + | ForbiddenErrorQueryAPIResponse, +> extends PrismicError {} diff --git a/src/errors/NotFoundError.ts b/src/errors/NotFoundError.ts index 7dc9ac65..c1442f95 100644 --- a/src/errors/NotFoundError.ts +++ b/src/errors/NotFoundError.ts @@ -1,3 +1,5 @@ import { PrismicError } from "./PrismicError"; -export class NotFoundError extends PrismicError {} +export class NotFoundError< + TResponse = undefined, +> extends PrismicError {} diff --git a/src/errors/ParsingError.ts b/src/errors/ParsingError.ts index 6e3776c2..9e3cf5b4 100644 --- a/src/errors/ParsingError.ts +++ b/src/errors/ParsingError.ts @@ -9,4 +9,6 @@ type ParsingErrorAPIResponse = { location: string; }; -export class ParsingError extends PrismicError {} +export class ParsingError< + TResponse = ParsingErrorAPIResponse, +> extends PrismicError {} diff --git a/src/errors/RefExpiredError.ts b/src/errors/RefExpiredError.ts new file mode 100644 index 00000000..b86f5a2f --- /dev/null +++ b/src/errors/RefExpiredError.ts @@ -0,0 +1,14 @@ +import { ForbiddenError } from "./ForbiddenError"; + +type RefExpiredErrorAPIResponse = { + type: "api_validation_error"; + message: string; +}; + +// This error extends `ForbiddenError` for backwards compatibility. Before the +// API started returning 410 for expired refs, it returnd 403, which threw a +// `ForbiddenError`. +// TODO: Extend this error from `PrismicError` in v8. +export class RefExpiredError< + TResponse = RefExpiredErrorAPIResponse, +> extends ForbiddenError {} diff --git a/src/errors/RefNotFoundError.ts b/src/errors/RefNotFoundError.ts new file mode 100644 index 00000000..2f0cce01 --- /dev/null +++ b/src/errors/RefNotFoundError.ts @@ -0,0 +1,14 @@ +import { ForbiddenError } from "./ForbiddenError"; + +type RefNotFoundErrorAPIResponse = { + type: "api_notfound_error"; + message: string; +}; + +// This error extends `ForbiddenError` for backwards compatibility. Before the +// API started returning 404 for not found refs, it returnd 403, which threw a +// `ForbiddenError`. +// TODO: Extend this error from `PrismicError` in v8. +export class RefNotFoundError< + TResponse = RefNotFoundErrorAPIResponse, +> extends ForbiddenError {} diff --git a/src/index.ts b/src/index.ts index b0caa361..ad63443c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,6 +101,8 @@ export type { HTMLRichTextSerializer } from "./helpers/asHTML"; export { PrismicError } from "./errors/PrismicError"; export { ForbiddenError } from "./errors/ForbiddenError"; export { NotFoundError } from "./errors/NotFoundError"; +export { RefNotFoundError } from "./errors/RefNotFoundError"; +export { RefExpiredError } from "./errors/RefExpiredError"; export { ParsingError } from "./errors/ParsingError"; //============================================================================= diff --git a/test/client.test.ts b/test/client.test.ts index c4bcbe9a..a6dbb975 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -631,7 +631,7 @@ it("throws PrismicError if response is not 200, 400, 401, 403, or 404", async (c const client = createTestClient(); const queryEndpoint = new URL( - "documents/search", + "./documents/search", `${client.endpoint}/`, ).toString(); @@ -684,6 +684,60 @@ it("throws NotFoundError if repository does not exist", async (ctx) => { await expect(() => client.get()).rejects.toThrowError(prismic.NotFoundError); }); +it("throws RefNotFoundError if ref does not exist", async (ctx) => { + const queryResponse = { + type: "api_notfound_error", + message: "message", + }; + + mockPrismicRestAPIV2({ ctx }); + + const client = createTestClient(); + + const queryEndpoint = new URL( + "./documents/search", + `${client.endpoint}/`, + ).toString(); + + ctx.server.use( + msw.rest.get(queryEndpoint, (_req, res, ctx) => { + return res(ctx.status(404), ctx.json(queryResponse)); + }), + ); + + await expect(() => client.get()).rejects.toThrowError(queryResponse.message); + await expect(() => client.get()).rejects.toThrowError( + prismic.RefNotFoundError, + ); +}); + +it("throws RefExpiredError if ref is expired", async (ctx) => { + const queryResponse = { + type: "api_validation_error", + message: "message", + }; + + mockPrismicRestAPIV2({ ctx }); + + const client = createTestClient(); + + const queryEndpoint = new URL( + "./documents/search", + `${client.endpoint}/`, + ).toString(); + + ctx.server.use( + msw.rest.get(queryEndpoint, (_req, res, ctx) => { + return res(ctx.status(410), ctx.json(queryResponse)); + }), + ); + + await expect(() => client.get()).rejects.toThrowError(queryResponse.message); + await expect(() => client.get()).rejects.toThrowError( + prismic.RefExpiredError, + ); +}); + it("retries after `retry-after` milliseconds if response code is 429", async (ctx) => { const retryAfter = 200; // ms /**