From efa7ecc911f6bd63af5addc545842a1434a6d24e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 2 Apr 2024 15:03:20 +0900 Subject: [PATCH 1/3] refactor: add ReactServerErrorContext.headers --- .../react-server/src/entry/react-server.tsx | 8 ++------ packages/react-server/src/entry/server.tsx | 20 +++++++++---------- .../src/features/router/client.tsx | 19 ++++++++++-------- .../src/lib/client/error-boundary.tsx | 7 ++++--- packages/react-server/src/lib/error.tsx | 20 ++++++++++++------- 5 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/react-server/src/entry/react-server.tsx b/packages/react-server/src/entry/react-server.tsx index 90c0e54fb..ceeb3735a 100644 --- a/packages/react-server/src/entry/react-server.tsx +++ b/packages/react-server/src/entry/react-server.tsx @@ -55,7 +55,7 @@ export const handler: ReactServerHandler = async (ctx) => { const errorCtx = getErrorContext(e) ?? DEFAULT_ERROR_CONTEXT; if (rscOnly) { // returns empty layout to keep current layout and - // let browser initiate clie-side navigation for redirection error + // let browser initiate cliet-side navigation for redirection error const data: ServerRouterData = { action: { error: errorCtx }, layout: {}, @@ -70,11 +70,7 @@ export const handler: ReactServerHandler = async (ctx) => { // TODO: general action error handling? return new Response(null, { status: errorCtx.status, - headers: errorCtx.redirectLocation - ? { - location: errorCtx.redirectLocation, - } - : {}, + headers: errorCtx.headers, }); } } diff --git a/packages/react-server/src/entry/server.tsx b/packages/react-server/src/entry/server.tsx index 0f78648c9..b8a05e461 100644 --- a/packages/react-server/src/entry/server.tsx +++ b/packages/react-server/src/entry/server.tsx @@ -9,7 +9,12 @@ import { ssrImportPromiseCache, } from "../features/use-client/server"; import { Router, RouterContext } from "../lib/client/router"; -import { getErrorContext, getStatusText } from "../lib/error"; +import { + DEFAULT_ERROR_CONTEXT, + getErrorContext, + getStatusText, + isRedirectError, +} from "../lib/error"; import { __global } from "../lib/global"; import { ENTRY_REACT_SERVER_WRAPPER, @@ -121,16 +126,11 @@ export async function renderHtml( }, }); } catch (e) { - const ctx = getErrorContext(e); - status = ctx?.status ?? 500; - if (ctx?.redirectLocation) { - return new Response(null, { - status, - headers: { - location: ctx.redirectLocation, - }, - }); + const ctx = getErrorContext(e) ?? DEFAULT_ERROR_CONTEXT; + if (isRedirectError(ctx)) { + return new Response(null, { status: ctx.status, headers: ctx.headers }); } + status = ctx.status; // render empty as error fallback and // let browser render full CSR instead of hydration // which will replay client error boudnary from RSC error diff --git a/packages/react-server/src/features/router/client.tsx b/packages/react-server/src/features/router/client.tsx index cbf9cb474..b351d2f4c 100644 --- a/packages/react-server/src/features/router/client.tsx +++ b/packages/react-server/src/features/router/client.tsx @@ -1,5 +1,6 @@ import React from "react"; import { RedirectHandler } from "../../lib/client/error-boundary"; +import { isRedirectError } from "../../lib/error"; import { __global } from "../../lib/global"; import { LAYOUT_ROOT_NAME, type ServerRouterData } from "./utils"; @@ -25,14 +26,16 @@ export function ServerActionRedirectHandler() { const ctx = React.useContext(LayoutStateContext); const data = React.use(ctx.data); - if (data.action?.error?.redirectLocation) { - return ( - - ); + if (data.action?.error) { + const redirect = isRedirectError(data.action.error); + if (redirect) { + return ( + + ); + } } - return null; } diff --git a/packages/react-server/src/lib/client/error-boundary.tsx b/packages/react-server/src/lib/client/error-boundary.tsx index f47cb0459..29615748d 100644 --- a/packages/react-server/src/lib/client/error-boundary.tsx +++ b/packages/react-server/src/lib/client/error-boundary.tsx @@ -1,6 +1,6 @@ import { tinyassert } from "@hiogawa/utils"; import React from "react"; -import { getErrorContext, getStatusText } from "../error"; +import { getErrorContext, getStatusText, isRedirectError } from "../error"; import type { ErrorPageProps } from "../router"; import { useRouter } from "./router"; @@ -89,8 +89,9 @@ export class RedirectBoundary extends React.Component { static getDerivedStateFromError(error: Error) { if (!import.meta.env.SSR) { const ctx = getErrorContext(error); - if (ctx?.redirectLocation) { - return { redirectLocation: ctx.redirectLocation }; + const redirect = ctx && isRedirectError(ctx); + if (redirect) { + return { redirectLocation: redirect.location }; } } throw error; diff --git a/packages/react-server/src/lib/error.tsx b/packages/react-server/src/lib/error.tsx index 6b602a8b6..e8feb77b8 100644 --- a/packages/react-server/src/lib/error.tsx +++ b/packages/react-server/src/lib/error.tsx @@ -1,12 +1,7 @@ // TODO: custom (de)serialization? export interface ReactServerErrorContext { status: number; - // TODO: hide from public typing? - redirectLocation?: string; -} - -export interface ReactServerRedirectErrorContext { - redirectLocation: string; + headers?: Record; } export class ReactServerDigestError extends Error { @@ -21,7 +16,18 @@ export function createError(ctx: ReactServerErrorContext) { } export function redirect(location: string, status: number = 302) { - return createError({ status, redirectLocation: location }); + return createError({ + status, + headers: { location }, + }); +} + +export function isRedirectError(ctx: ReactServerErrorContext) { + const location = ctx.headers?.["location"]; + if (300 <= ctx.status && ctx.status <= 399 && typeof location === "string") { + return { location }; + } + return false; } export function getErrorContext( From c436682f12fb22e089d65579628c58174b1bc355 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 2 Apr 2024 15:12:09 +0900 Subject: [PATCH 2/3] chore: headers in action error stream --- packages/react-server/src/entry/react-server.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-server/src/entry/react-server.tsx b/packages/react-server/src/entry/react-server.tsx index ceeb3735a..257e89a21 100644 --- a/packages/react-server/src/entry/react-server.tsx +++ b/packages/react-server/src/entry/react-server.tsx @@ -63,6 +63,7 @@ export const handler: ReactServerHandler = async (ctx) => { const stream = reactServerDomServer.renderToReadableStream(data, {}); return new Response(stream, { headers: { + ...errorCtx.headers, "content-type": "text/x-component; charset=utf-8", }, }); From 8f222ebb2c9e285f7d53b86024d6cee4130734bc Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 2 Apr 2024 15:12:45 +0900 Subject: [PATCH 3/3] chore: typo --- packages/react-server/src/entry/react-server.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-server/src/entry/react-server.tsx b/packages/react-server/src/entry/react-server.tsx index 257e89a21..efb9e073f 100644 --- a/packages/react-server/src/entry/react-server.tsx +++ b/packages/react-server/src/entry/react-server.tsx @@ -55,7 +55,7 @@ export const handler: ReactServerHandler = async (ctx) => { const errorCtx = getErrorContext(e) ?? DEFAULT_ERROR_CONTEXT; if (rscOnly) { // returns empty layout to keep current layout and - // let browser initiate cliet-side navigation for redirection error + // let browser initiate client-side navigation for redirection error const data: ServerRouterData = { action: { error: errorCtx }, layout: {},