Skip to content

Commit

Permalink
refactor: handle Next.js errors internally
Browse files Browse the repository at this point in the history
  • Loading branch information
TheEdoRan committed Dec 13, 2024
1 parent 86f00f4 commit 9cd2a84
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 34 deletions.
6 changes: 2 additions & 4 deletions packages/next-safe-action/src/action-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import type {
StateServerCodeFn,
} from "./index.types";
import {
DEFAULT_SERVER_ERROR_MESSAGE,
isError,
isForbiddenError,
isFrameworkError,
isNotFoundError,
isRedirectError,
isUnauthorizedError,
winningBoolean,
} from "./utils";
} from "./next/errors";
import { DEFAULT_SERVER_ERROR_MESSAGE, isError, winningBoolean } from "./utils";
import {
ActionMetadataValidationError,
ActionOutputDataValidationError,
Expand Down
22 changes: 22 additions & 0 deletions packages/next-safe-action/src/next/errors/bailout-to-csr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Comes from https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/lazy-dynamic/bailout-to-csr.ts

// This has to be a shared module which is shared between client component error boundary and dynamic component
const BAILOUT_TO_CSR = "BAILOUT_TO_CLIENT_SIDE_RENDERING";

/** An error that should be thrown when we want to bail out to client-side rendering. */
class BailoutToCSRError extends Error {
public readonly digest = BAILOUT_TO_CSR;

constructor(public readonly reason: string) {
super(`Bail out to client-side rendering: ${reason}`);
}
}

/** Checks if a passed argument is an error that is thrown if we want to bail out to client-side rendering. */
export function isBailoutToCSRError(err: unknown): err is BailoutToCSRError {
if (typeof err !== "object" || err === null || !("digest" in err)) {
return false;
}

return err.digest === BAILOUT_TO_CSR;
}
45 changes: 45 additions & 0 deletions packages/next-safe-action/src/next/errors/dynamic-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Comes from https://github.com/vercel/next.js/blob/canary/packages/next/src/export/helpers/is-dynamic-usage-error.ts

import { isBailoutToCSRError } from "./bailout-to-csr";
import { isNextRouterError } from "./router";

const DYNAMIC_ERROR_CODE = "DYNAMIC_SERVER_USAGE";

class DynamicServerError extends Error {
digest: typeof DYNAMIC_ERROR_CODE = DYNAMIC_ERROR_CODE;

constructor(public readonly description: string) {
super(`Dynamic server usage: ${description}`);
}
}

function isDynamicServerError(err: unknown): err is DynamicServerError {
if (typeof err !== "object" || err === null || !("digest" in err) || typeof err.digest !== "string") {
return false;
}

return err.digest === DYNAMIC_ERROR_CODE;
}

function isDynamicPostponeReason(reason: string) {
return (
reason.includes("needs to bail out of prerendering at this point because it used") &&
reason.includes("Learn more: https://nextjs.org/docs/messages/ppr-caught-error")
);
}

function isDynamicPostpone(err: unknown) {
if (
typeof err === "object" &&
err !== null &&
// eslint-disable-next-line
typeof (err as any).message === "string"
) {
// eslint-disable-next-line
return isDynamicPostponeReason((err as any).message);
}
return false;
}

export const isDynamicUsageError = (err: unknown) =>
isDynamicServerError(err) || isBailoutToCSRError(err) || isNextRouterError(err) || isDynamicPostpone(err);
36 changes: 36 additions & 0 deletions packages/next-safe-action/src/next/errors/http-access-fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Comes from https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/http-access-fallback/http-access-fallback.ts

const HTTPAccessErrorStatus = {
NOT_FOUND: 404,
FORBIDDEN: 403,
UNAUTHORIZED: 401,
};

const ALLOWED_CODES = new Set(Object.values(HTTPAccessErrorStatus));

const HTTP_ERROR_FALLBACK_ERROR_CODE = "NEXT_HTTP_ERROR_FALLBACK";

export type HTTPAccessFallbackError = Error & {
digest: `${typeof HTTP_ERROR_FALLBACK_ERROR_CODE};${string}`;
};

/**
* Checks an error to determine if it's an error generated by
* the HTTP navigation APIs `notFound()`, `forbidden()` or `unauthorized()`.
*
* @param error the error that may reference a HTTP access error
* @returns true if the error is a HTTP access error
*/
export function isHTTPAccessFallbackError(error: unknown): error is HTTPAccessFallbackError {
if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") {
return false;
}
const [prefix, httpStatus] = error.digest.split(";");

return prefix === HTTP_ERROR_FALLBACK_ERROR_CODE && ALLOWED_CODES.has(Number(httpStatus));
}

export function getAccessFallbackHTTPStatus(error: HTTPAccessFallbackError): number {
const httpStatus = error.digest.split(";")[1];
return Number(httpStatus);
}
28 changes: 28 additions & 0 deletions packages/next-safe-action/src/next/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { isBailoutToCSRError } from "./bailout-to-csr";
import { isDynamicUsageError } from "./dynamic-usage";
import {
getAccessFallbackHTTPStatus,
isHTTPAccessFallbackError,
type HTTPAccessFallbackError,
} from "./http-access-fallback";
import { isPostpone } from "./postpone";
import { isNextRouterError } from "./router";

export function isNotFoundError(error: unknown): error is HTTPAccessFallbackError {
return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 404;
}

export function isForbiddenError(error: unknown): error is HTTPAccessFallbackError {
return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 403;
}

export function isUnauthorizedError(error: unknown): error is HTTPAccessFallbackError {
return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 401;
}

// Next.js error handling
export function isFrameworkError(error: unknown): error is Error {
return isNextRouterError(error) || isBailoutToCSRError(error) || isDynamicUsageError(error) || isPostpone(error);
}

export { isRedirectError } from "./redirect";
12 changes: 12 additions & 0 deletions packages/next-safe-action/src/next/errors/postpone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Comes from https://github.com/vercel/next.js/blob/canary/packages/next/src/server/lib/router-utils/is-postpone.ts

const REACT_POSTPONE_TYPE: symbol = Symbol.for("react.postpone");

export function isPostpone(error: any): boolean {
return (
typeof error === "object" &&
error !== null &&
// eslint-disable-next-line
error.$$typeof === REACT_POSTPONE_TYPE
);
}
46 changes: 46 additions & 0 deletions packages/next-safe-action/src/next/errors/redirect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Comes from: https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect-error.ts

enum RedirectStatusCode {
SeeOther = 303,
TemporaryRedirect = 307,
PermanentRedirect = 308,
}

const REDIRECT_ERROR_CODE = "NEXT_REDIRECT";

enum RedirectType {
push = "push",
replace = "replace",
}

export type RedirectError = Error & {
digest: `${typeof REDIRECT_ERROR_CODE};${RedirectType};${string};${RedirectStatusCode};`;
};

/**
* Checks an error to determine if it's an error generated by the
* `redirect(url)` helper.
*
* @param error the error that may reference a redirect error
* @returns true if the error is a redirect error
*/
export function isRedirectError(error: unknown): error is RedirectError {
if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") {
return false;
}

const digest = error.digest.split(";");
const [errorCode, type] = digest;
const destination = digest.slice(2, -2).join(";");
const status = digest.at(-2);

const statusCode = Number(status);

return (
errorCode === REDIRECT_ERROR_CODE &&
(type === "replace" || type === "push") &&
typeof destination === "string" &&
!isNaN(statusCode) &&
statusCode in RedirectStatusCode
);
}
13 changes: 13 additions & 0 deletions packages/next-safe-action/src/next/errors/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Comes from https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/is-next-router-error.ts

import { isHTTPAccessFallbackError, type HTTPAccessFallbackError } from "./http-access-fallback";
import { isRedirectError, type RedirectError } from "./redirect";

/**
* Returns true if the error is a navigation signal error. These errors are
* thrown by user code to perform navigation operations and interrupt the React
* render.
*/
export function isNextRouterError(error: unknown): error is RedirectError | HTTPAccessFallbackError {
return isRedirectError(error) || isHTTPAccessFallbackError(error);
}
30 changes: 0 additions & 30 deletions packages/next-safe-action/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import type { HTTPAccessFallbackError } from "next/dist/client/components/http-access-fallback/http-access-fallback.js";
import {
getAccessFallbackHTTPStatus,
isHTTPAccessFallbackError,
} from "next/dist/client/components/http-access-fallback/http-access-fallback.js";
import { isNextRouterError } from "next/dist/client/components/is-next-router-error.js";
import { isRedirectError } from "next/dist/client/components/redirect-error.js";
import { isDynamicUsageError } from "next/dist/export/helpers/is-dynamic-usage-error.js";
import { isPostpone } from "next/dist/server/lib/router-utils/is-postpone.js";
import { isBailoutToCSRError } from "next/dist/shared/lib/lazy-dynamic/bailout-to-csr.js";

export const DEFAULT_SERVER_ERROR_MESSAGE = "Something went wrong while executing the operation.";

/**
Expand All @@ -23,22 +12,3 @@ export const isError = (error: unknown): error is Error => error instanceof Erro
export const winningBoolean = (...args: (boolean | undefined | null)[]) => {
return args.reduce((acc, v) => (typeof v === "boolean" ? v : acc), false) as boolean;
};

// Next.js error handling
export function isFrameworkError(error: unknown): error is Error {
return isNextRouterError(error) || isBailoutToCSRError(error) || isDynamicUsageError(error) || isPostpone(error);
}

export function isNotFoundError(error: unknown): error is HTTPAccessFallbackError {
return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 404;
}

export function isForbiddenError(error: unknown): error is HTTPAccessFallbackError {
return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 403;
}

export function isUnauthorizedError(error: unknown): error is HTTPAccessFallbackError {
return isHTTPAccessFallbackError(error) && getAccessFallbackHTTPStatus(error) === 401;
}

export { isRedirectError };

0 comments on commit 9cd2a84

Please sign in to comment.