Skip to content

Commit

Permalink
feat(ssr): support for loaderFailureMode configure (#5820)
Browse files Browse the repository at this point in the history
  • Loading branch information
yimingjfe committed Jun 17, 2024
1 parent 15a090c commit 9da873c
Show file tree
Hide file tree
Showing 25 changed files with 112 additions and 60 deletions.
8 changes: 8 additions & 0 deletions .changeset/twenty-pots-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@modern-js/plugin-data-loader': minor
'@modern-js/runtime': minor
'@modern-js/app-tools': minor
---

feat(ssr): support for loaderFailureMode configure
feat(ssr): 支持 loaderFailureMode 配置
20 changes: 17 additions & 3 deletions packages/cli/plugin-data-loader/src/cli/createRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ const handleDeferredResponse = async (res: Response) => {
return res;
};

const isErrorResponse = (res: Response) => {
return res.headers.get('X-Modernjs-Error') != null;
};

const handleErrorResponse = async (res: Response) => {
if (isErrorResponse(res)) {
const data = await res.json();
const error = new Error(data.message);
error.stack = data.stack;
throw error;
}
return res;
};

export const createRequest = (routeId: string, method = 'get') => {
return async ({
params,
Expand All @@ -63,11 +77,11 @@ export const createRequest = (routeId: string, method = 'get') => {
method,
signal: request.signal,
});
if (!res.ok) {
throw res;
}

res = handleRedirectResponse(res);
res = await handleErrorResponse(res);
res = await handleDeferredResponse(res);

return res;
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ When the value type is `Object`, the following properties can be configured:
However, if developers want to reduce one rendering when there is no use of the useLoader API in your project, you can set the configuration `disablePrerender=true`.
- `unsafeHeaders`: `string[] = []`, For safety reasons, Modern.js does not add excessive content to SSR_DATA. Developers can use this configuration to specify the headers that need to be injected.
- `scriptLoading`: `'defer' | 'blocking' | 'module' | 'async'`, The configuration is the same as [html.scriptLoading](/configure/app/html/script-loading), supporting SSR injected script set to `async` loading. The priority is `ssr.scriptLoading` > `html.scriptLoading`.
- `loaderFailureMode`: `'clientRender' | 'errorBoundary'`, The default configuration is `'errorBoundary'`, when an error occurs in [data loader](/en/guides/basic-features/data/data-fetch.html#data-loader-recommended),
it will default to rendering the [`Error`](/en/guides/basic-features/routes.html#errorboundary) component of the route. When configured as `'clientRender'`, if a loader throws an error, it switch to client-side rendering,you can use it with [Client Loader](/en/guides/basic-features/data/data-fetch.html#client-loader).

```ts title="modern.config.ts"
export default defineConfig({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export default defineConfig({
开发者在保证项目中没有使用 useLoader Api 情况下, 可通过配置 `disablePrerender=true`来减少一次渲染。
- `unsafeHeaders`: `string[] = []`, 为了安全考虑,Modern.js 不会往 SSR_DATA 添加过多的内容。开发者可以通过该配置,对需要注入的 headers 进行配置。
- `scriptLoading`: `'defer' | 'blocking' | 'module' | 'async'`, 配置同 [html.scriptLoading](/configure/app/html/script-loading),支持 ssr 注入的 script 设置为 async 加载方式。优先级为 `ssr.scriptLoading` > `html.scriptLoading`
- `loaderFailureMode`: `'clientRender' | 'errorBoundary'`, 默认配置为 `'errorBoundary'`,当 [data loader](/guides/basic-features/data/data-fetch.html#data-loader推荐) 中出错时,默认会渲染路由 [`Error`](/guides/basic-features/routes.html#错误处理) 组件,
配置为 `'clientRender'` 时,有一个 data loader 抛错,就降级到客户端渲染,可以与 [Client Loader](/guides/basic-features/data/data-fetch.html#client-loader) 配合使用。

```ts title="modern.config.ts"
export default defineConfig({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ export const routerPlugin = ({
return next({ context });
}

const { request, mode: ssrMode, nonce } = context.ssrContext!;
const {
request,
mode: ssrMode,
nonce,
loaderFailureMode = 'errorBoundary',
} = context.ssrContext!;
const baseUrl = originalBaseUrl || (request.baseUrl as string);
const _basename =
baseUrl === '/' ? urlJoin(baseUrl, basename) : baseUrl;
Expand Down Expand Up @@ -125,6 +130,15 @@ export const routerPlugin = ({
return routerContext as any;
}

if (
routerContext.statusCode >= 500 &&
routerContext.statusCode < 600 &&
loaderFailureMode === 'clientRender'
) {
routerContext.statusCode = 200;
throw (routerContext.errors as Error[])[0];
}

const router = createStaticRouter(routes, routerContext);
context.remixRouter = router;
context.routerContext = routerContext;
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/plugin-runtime/src/ssr/index.node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const ssr = (config: SSRPluginConfig = {}): Plugin => ({
context.ssrContext!.request = formatServer(request);
context.ssrContext!.mode = config.mode;
context.ssrContext!.tracker = createSSRTracker(context.ssrContext!);
context.ssrContext!.loaderFailureMode = config.loaderFailureMode;

if (!context.ssrContext!.htmlModifiers) {
context.ssrContext!.htmlModifiers = [];
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/plugin-runtime/src/ssr/serverRender/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type RenderResult = {
};
};

type LoaderFailureMode = 'clientRender' | 'errorBoundary';

export type SSRServerContext = BaseSSRServerContext & {
request: BaseSSRServerContext['request'] & {
userAgent: string;
Expand All @@ -29,6 +31,7 @@ export type SSRServerContext = BaseSSRServerContext & {
};
htmlModifiers: BuildHtmlCb[];
tracker: SSRTracker;
loaderFailureMode?: 'clientRender' | 'errorBoundary';
};
export type ModernSSRReactComponent = React.ComponentType<any>;
export { RuntimeContext };
Expand All @@ -39,6 +42,7 @@ export type SSRPluginConfig = {
enableInlineStyles?: boolean | RegExp;
enableInlineScripts?: boolean | RegExp;
disablePrerender?: boolean;
loaderFailureMode?: LoaderFailureMode;
chunkLoadingGlobal?: string;
unsafeHeaders?: string[];
} & Exclude<ServerUserConfig['ssr'], boolean>;
Expand Down
1 change: 1 addition & 0 deletions packages/server/core/src/base/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export enum ServerReportTimings {
}

export const X_RENDER_CACHE = 'x-render-cache';
export const X_MODERNJS_RENDER = 'x-modernjs-render';
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
ErrorDigest,
} from '../../utils';
import type { FallbackReason } from '../../../core/plugin';
import { REPLACE_REG } from '../../../base/constants';
import { REPLACE_REG, X_MODERNJS_RENDER } from '../../constants';
import { Render } from '../../../core/render';
import { dataHandler } from './dataHandler';
import { Params, SSRRenderOptions, ssrRender } from './ssrRender';
Expand Down Expand Up @@ -263,6 +263,7 @@ function csrRender(html: string): Response {
status: 200,
headers: new Headers({
'content-type': 'text/html; charset=UTF-8',
[X_MODERNJS_RENDER]: 'client',
}),
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ServerManifest,
ServerRender,
} from '../../../core/server';
import { X_RENDER_CACHE } from '../../constants';
import { X_MODERNJS_RENDER, X_RENDER_CACHE } from '../../constants';
import type * as streamPolyfills from '../../adapters/node/polyfills/stream';
import type * as ssrCaheModule from './ssrCache';
import { ServerTiming } from './serverTiming';
Expand Down Expand Up @@ -188,6 +188,8 @@ export async function ssrRender(
responseProxy.headers.set(X_RENDER_CACHE, cacheStatus);
}

responseProxy.headers.set(X_MODERNJS_RENDER, 'server');

if (redirection.url) {
const { headers } = responseProxy;
headers.set('Location', redirection.url);
Expand Down
1 change: 1 addition & 0 deletions packages/server/core/src/types/config/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type SSR =
disablePrerender?: boolean;
unsafeHeaders?: string[];
scriptLoading?: 'defer' | 'blocking' | 'module' | 'async';
loaderFailureMode?: 'clientRender' | 'errorBoundary';
};

export type SSRByEntries = Record<string, SSR>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ function applyFilterEntriesBySSRConfig({
entryNames.forEach(name => {
if (
!ssgEntries.includes(name) &&
!name.includes('server-loaders') &&
((ssr && ssrByEntries?.[name] === false) ||
(!ssr && !ssrByEntries?.[name]))
) {
Expand Down
8 changes: 5 additions & 3 deletions tests/integration/routes/modern.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export default defineConfig({
disableTsChecker: true,
},
server: {
ssr: {
mode: 'stream',
},
ssrByEntries: {
one: false,
two: false,
three: {
mode: 'stream',
disablePrerender: true,
loaderFailureMode: 'clientRender',
},
four: false,
},
},
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/routes/src/three/routes/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useRouteError, isRouteErrorResponse } from '@modern-js/runtime/router';

export default function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<div className="response-status">{error.status}</div>
<div className="response-content">{JSON.parse(error.data).message}</div>
</div>
);
} else {
return (
<div className="error-content">
{(error as any).message || (error as any).data}
</div>
);
}
}
6 changes: 5 additions & 1 deletion tests/integration/routes/src/three/routes/error/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default function ErrorBoundary() {
</div>
);
} else {
return <div className="error-content">{(error as any).data}</div>;
return (
<div className="error-content">
{(error as any).message || (error as any).data}
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const loader = async () => {
return 'render by client loader';
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const loader = () => {
throw new Error('loader error');
};

This file was deleted.

12 changes: 4 additions & 8 deletions tests/integration/routes/src/three/routes/error/loader/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { Outlet, useLoaderData } from '@modern-js/runtime/router';
import { useLoaderData } from '@modern-js/runtime/router';

export default function Page() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const data = useLoaderData();
return (
<div>
<Outlet />
</div>
);
const data = useLoaderData() as string;

return <div className="error-loader-page">{data}</div>;
}
1 change: 1 addition & 0 deletions tests/integration/routes/src/three/routes/user/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default function Layout() {
const data = useLoaderData() as {
message: string;
};

return (
<div>
<span className="user-layout">{`${data?.message} layout`}</span>
Expand Down
22 changes: 9 additions & 13 deletions tests/integration/routes/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import puppeteer, { Browser } from 'puppeteer';
import { fs, ROUTE_MANIFEST_FILE } from '@modern-js/utils';
import { ROUTE_MANIFEST } from '@modern-js/utils/universal/constants';

import type {
// Browser,
Page,
} from 'puppeteer';
import type { Page } from 'puppeteer';
import {
launchApp,
killApp,
Expand Down Expand Up @@ -320,11 +317,11 @@ const supportHandleLoaderError = async (
});
await Promise.all([
page.click('.loader-error-btn'),
page.waitForSelector('.error-case'),
page.waitForSelector('.error-loader-page'),
]);
const errorElm = await page.$('.error-case');
const errorElm = await page.$('.error-loader-page');
const text = await page.evaluate(el => el?.textContent, errorElm);
expect(text?.includes('loader error')).toBeTruthy();
expect(text).toBe('render by client loader');
expect(errors.length).toBe(0);
};

Expand Down Expand Up @@ -469,7 +466,7 @@ const supportClientLoader = async (
]);
const clientLoaderLayout = await page.$('.client-loader-layout');
const text = await page.evaluate(el => el?.textContent, clientLoaderLayout);
expect(text?.includes('layout from client loader')).toBeTruthy();
expect(text).toBe('layout from client loader');

const clientLoaderPage = await page.$('.client-loader-page');
const text1 = await page.evaluate(el => el?.textContent, clientLoaderPage);
Expand Down Expand Up @@ -742,7 +739,7 @@ describe('dev', () => {
test('support handle config', async () =>
supportHandleConfig(page, appPort));

test.skip('support handle loader error', async () =>
test('support handle loader error', async () =>
supportHandleLoaderError(page, errors, appPort));
});

Expand Down Expand Up @@ -884,7 +881,7 @@ describe('build', () => {
test('path without layout', async () =>
supportPathWithoutLayout(page, errors, appPort));

test.skip('support handle loader error', async () =>
test('support handle loader error', async () =>
supportHandleLoaderError(page, errors, appPort));
});

Expand Down Expand Up @@ -1028,8 +1025,7 @@ describe('dev with rspack', () => {
test('support handle config', async () =>
supportHandleConfig(page, appPort));

// FIXME: skip the test
test.skip('support handle loader error', async () =>
test('support handle loader error', async () =>
supportHandleLoaderError(page, errors, appPort));
});

Expand Down Expand Up @@ -1175,7 +1171,7 @@ describe('build with rspack', () => {
test('path without layout', async () =>
supportPathWithoutLayout(page, errors, appPort));

test.skip('support handle loader error', async () =>
test('support handle loader error', async () =>
supportHandleLoaderError(page, errors, appPort));
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { isRouteErrorResponse, useRouteError } from '@modern-js/runtime/router';
import { useRouteError } from '@modern-js/runtime/router';

const ErrorBoundary = () => {
const error = useRouteError() as Error;
return (
<div className="error">
<h2>
{isRouteErrorResponse(error)
? JSON.stringify({
status: error.status,
statusText: error.statusText,
data: {
messsage: error.data.message,
},
})
: error.message}
<h2>{error.message}</h2>
</h2>
</div>
);
Expand Down
14 changes: 2 additions & 12 deletions tests/integration/ssr/fixtures/base/src/routes/error/error.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import { isRouteErrorResponse, useRouteError } from '@modern-js/runtime/router';
import { useRouteError } from '@modern-js/runtime/router';

const ErrorBoundary = () => {
const error = useRouteError() as Error;
return (
<div className="error">
<h2>
{isRouteErrorResponse(error)
? JSON.stringify({
status: error.status,
statusText: error.statusText,
data: {
messsage: error.data.message,
},
})
: error.message}
</h2>
<h2>{error.message}</h2>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Traditional SSR in json data error thrown in client navigation 1`] = `"{"status":500,"statusText":"Internal Server Error","data":{"messsage":"error occurs"}}"`;
exports[`Traditional SSR in json data error thrown in client navigation 1`] = `"error occurs"`;
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Traditional SSR error thrown in client navigation 1`] = `"{"status":500,"statusText":"Internal Server Error","data":{"messsage":"error occurs"}}"`;
exports[`Traditional SSR error thrown in client navigation 1`] = `"error occurs"`;

0 comments on commit 9da873c

Please sign in to comment.