Skip to content

Commit e37ab59

Browse files
authored
feat: server config support onError (#7608)
1 parent be75e1b commit e37ab59

File tree

12 files changed

+660
-200
lines changed

12 files changed

+660
-200
lines changed

.changeset/wide-humans-tease.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@modern-js/prod-server': patch
3+
'@modern-js/plugin-bff': patch
4+
'@modern-js/server-core': patch
5+
---
6+
7+
feat: server config support onError
8+
feat: 自定义 server 支持错误处理

packages/cli/plugin-bff/src/runtime/hono/adapter.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
} from '@modern-js/server-core';
99
import { Hono } from '@modern-js/server-core';
1010

11-
import { isProd } from '@modern-js/utils';
11+
import { isProd, logger } from '@modern-js/utils';
1212
import createHonoRoutes from '../../utils/createHonoRoutes';
1313

1414
const before = ['custom-server-hook', 'custom-server-middleware', 'render'];
@@ -53,6 +53,28 @@ export class HonoAdapter {
5353
const handlers = this.wrapInArray(handler);
5454
this.apiServer?.[method](path, ...handlers);
5555
});
56+
57+
this.apiServer.onError(async (err, c) => {
58+
try {
59+
const serverConfig = this.api.getServerConfig();
60+
const onErrorHandler = serverConfig?.onError;
61+
62+
if (onErrorHandler) {
63+
const result = await onErrorHandler(err, c);
64+
if (result instanceof Response) {
65+
return result;
66+
}
67+
}
68+
} catch (configError) {
69+
logger.error(`Error in serverConfig.onError handler: ${configError}`);
70+
}
71+
return c.json(
72+
{
73+
message: (err as any)?.message || '[BFF] Internal Server Error',
74+
},
75+
(err as any)?.status || 500,
76+
);
77+
});
5678
};
5779

5880
registerMiddleware = async (options: MiddlewareOptions) => {

packages/cli/plugin-bff/src/utils/createHonoRoutes.ts

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -54,61 +54,53 @@ const handleResponseMeta = (c: Context, handler: Handler) => {
5454
};
5555

5656
export const createHonoHandler = (handler: Handler) => {
57-
return async (c: Context, next: Next) => {
58-
try {
59-
const input = await getHonoInput(c);
57+
return async (c: Context) => {
58+
const input = await getHonoInput(c);
6059

61-
if (isWithMetaHandler(handler)) {
62-
try {
63-
const response = handleResponseMeta(c, handler);
64-
if (response) {
65-
return response;
66-
}
67-
if (c.finalized) return;
60+
if (isWithMetaHandler(handler)) {
61+
try {
62+
const response = handleResponseMeta(c, handler);
63+
if (response) {
64+
return response;
65+
}
66+
if (c.finalized) return;
6867

69-
const result = await handler(input);
70-
if (result instanceof Response) {
71-
return result;
72-
}
73-
return result && typeof result === 'object'
74-
? c.json(result)
75-
: c.body(result);
76-
} catch (error) {
77-
if (error instanceof ValidationError) {
78-
c.status((error as any).status);
68+
const result = await handler(input);
69+
if (result instanceof Response) {
70+
return result;
71+
}
72+
return result && typeof result === 'object'
73+
? c.json(result)
74+
: c.body(result);
75+
} catch (error) {
76+
if (error instanceof ValidationError) {
77+
c.status((error as any).status);
7978

80-
return c.json({
81-
message: error.message,
82-
});
83-
}
84-
throw error;
79+
return c.json({
80+
message: error.message,
81+
});
8582
}
86-
} else {
87-
const routePath = c.req.routePath;
88-
const paramNames = routePath.match(/:\w+/g)?.map(s => s.slice(1)) || [];
89-
const params = Object.fromEntries(
90-
paramNames.map(name => [name, input.params[name]]),
91-
);
92-
const args = Object.values(params).concat(input);
83+
throw error;
84+
}
85+
} else {
86+
const routePath = c.req.routePath;
87+
const paramNames = routePath.match(/:\w+/g)?.map(s => s.slice(1)) || [];
88+
const params = Object.fromEntries(
89+
paramNames.map(name => [name, input.params[name]]),
90+
);
91+
const args = Object.values(params).concat(input);
9392

94-
try {
95-
const body = await handler(...args);
96-
if (c.finalized) {
97-
return await Promise.resolve();
98-
}
93+
const body = await handler(...args);
94+
if (c.finalized) {
95+
return await Promise.resolve();
96+
}
9997

100-
if (typeof body !== 'undefined') {
101-
if (body instanceof Response) {
102-
return body;
103-
}
104-
return c.json(body);
105-
}
106-
} catch {
107-
return next();
98+
if (typeof body !== 'undefined') {
99+
if (body instanceof Response) {
100+
return body;
108101
}
102+
return c.json(body);
109103
}
110-
} catch (error) {
111-
next();
112104
}
113105
};
114106
};

packages/document/main-doc/docs/en/guides/advanced-features/web-server.mdx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default defineServerConfig({
4747
middlewares: [],
4848
renderMiddlewares: [],
4949
plugins: [],
50+
onError: () => {},
5051
});
5152
```
5253

@@ -70,6 +71,7 @@ type ServerConfig = {
7071
middlewares?: MiddlewareObj[];
7172
renderMiddlewares?: MiddlewareObj[];
7273
plugins?: ServerPlugin[];
74+
onError?: (err: Error, c: Context) => Promise<any> | any;
7375
};
7476
```
7577

@@ -225,6 +227,35 @@ export default () => {
225227
};
226228
```
227229

230+
### onError
231+
232+
`onError` is a global error handling function used to capture and handle all uncaught errors in the Modern.js server. By customizing the `onError` function, developers can uniformly handle different types of errors, return custom error responses, and implement features such as error logging and error classification.
233+
234+
Below is a basic example of an `onError` configuration:
235+
236+
```ts title="server/modern.server.ts"
237+
import { defineServerConfig } from '@modern-js/server-runtime';
238+
239+
export default defineServerConfig({
240+
onError: (err, c) => {
241+
// Log the error
242+
console.error('Server error:', err);
243+
244+
// Return different responses based on the error type
245+
if (err instanceof SyntaxError) {
246+
return c.json({ error: 'Invalid JSON' }, 400);
247+
}
248+
249+
// Customize BFF error response based on request path
250+
if (c.req.path.includes('/api')) {
251+
return c.json({ message: 'API error occurred' }, 500);
252+
}
253+
254+
return c.text('Internal Server Error', 500);
255+
},
256+
});
257+
```
258+
228259
## Legacy API (Deprecated)
229260

230261
:::warning
@@ -254,10 +285,6 @@ const time: UnstableMiddleware = async (c: UnstableMiddlewareContext, next) => {
254285
export const unstableMiddleware: UnstableMiddleware[] = [time];
255286
```
256287

257-
:::info
258-
For detailed API and more usage, see [UnstableMiddleware](/apis/app/runtime/web-server/unstable_middleware).
259-
:::
260-
261288
### Hooks
262289

263290
:::warning
@@ -289,10 +316,6 @@ Best practices when using Hooks:
289316
2. Handle Rewrite and Redirect in afterMatch.
290317
3. Inject HTML content in afterRender.
291318

292-
:::info
293-
For detailed API and more usage, see [Hook](/apis/app/runtime/web-server/hook).
294-
:::
295-
296319
## Migrate to the New Version of Custom Web Server
297320

298321
### Migration Background

packages/document/main-doc/docs/zh/guides/advanced-features/web-server.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export default defineServerConfig({
4747
middlewares: [], // 中间件
4848
renderMiddlewares: [], // 渲染中间件
4949
plugins: [], // 插件
50+
onError: () => {}, // 错误处理
5051
});
5152
```
5253

@@ -67,6 +68,7 @@ type ServerConfig = {
6768
middlewares?: MiddlewareObj[];
6869
renderMiddlewares?: MiddlewareObj[];
6970
plugins?: ServerPlugin[];
71+
onError?: (err: Error, c: Context) => Promise<any> | any;
7072
};
7173
```
7274

@@ -222,6 +224,35 @@ export default () => {
222224
};
223225
```
224226

227+
### onError
228+
229+
`onError` 是一个全局错误处理函数,用于捕获和处理 Modern.js server 中的所有未捕获错误。通过自定义 `onError` 函数,开发者可以统一处理不同类型的错误,返回自定义的错误响应,实现错误日志记录、错误分类处理等功能。
230+
231+
以下是一个基本的 `onError` 配置示例:
232+
233+
```ts title="server/modern.server.ts"
234+
import { defineServerConfig } from '@modern-js/server-runtime';
235+
236+
export default defineServerConfig({
237+
onError: (err, c) => {
238+
// 记录错误日志
239+
console.error('Server error:', err);
240+
241+
// 根据不同的错误类型返回不同的响应
242+
if (err instanceof SyntaxError) {
243+
return c.json({ error: 'Invalid JSON' }, 400);
244+
}
245+
246+
// 根据请求路径定制 bff 异常响应
247+
if (c.req.path.includes('/api')) {
248+
return c.json({ message: 'API error occurred' }, 500);
249+
}
250+
251+
return c.text('Internal Server Error', 500);
252+
},
253+
});
254+
```
255+
225256
## 旧版 API(废弃)
226257

227258
:::warning

packages/server/core/src/types/plugins/base.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
Reporter,
99
ServerRoute,
1010
} from '@modern-js/types';
11-
import type { MiddlewareHandler } from 'hono';
11+
import type { Context, MiddlewareHandler } from 'hono';
1212
import type { UserConfig } from '../config';
1313
import type { Render } from '../render';
1414
import type { ServerPlugin } from './plugin';
@@ -85,9 +85,12 @@ export type CacheConfig = {
8585
container?: Container;
8686
};
8787

88+
export type ServerErrorHandler = (err: Error, c: Context) => Promise<any> | any;
89+
8890
export type ServerConfig = {
8991
// TODO: Middleware need more env
9092
renderMiddlewares?: MiddlewareObj[];
9193
middlewares?: MiddlewareObj[];
9294
plugins?: ServerPlugin[];
95+
onError?: ServerErrorHandler;
9396
} & UserConfig;

packages/server/prod-server/src/apply.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
loadCacheConfig,
1818
serverStaticPlugin,
1919
} from '@modern-js/server-core/node';
20-
import { createLogger, isProd } from '@modern-js/utils';
20+
import { createLogger, isProd, logger } from '@modern-js/utils';
2121
import type { ProdServerOptions } from './types';
2222

2323
// Now we not use logger options, it can be implemented in the future
@@ -40,6 +40,7 @@ export async function applyPlugins(
4040
) {
4141
const { pwd, appContext, config, logger: optLogger } = options;
4242

43+
const serverErrorHandler = options.serverConfig?.onError;
4344
const loadCachePwd = isProd() ? pwd : appContext.appDirectory || pwd;
4445
const cacheConfig = await loadCacheConfig(loadCachePwd);
4546

@@ -49,10 +50,33 @@ export async function applyPlugins(
4950
return c.html(createErrorHtml(404), 404);
5051
});
5152

52-
serverBase.onError((err, c) => {
53+
serverBase.onError(async (err, c) => {
5354
const monitors = c.get('monitors');
5455
onError(ErrorDigest.EINTER, err, monitors, c.req.raw);
55-
return c.html(createErrorHtml(500), 500);
56+
57+
if (serverErrorHandler) {
58+
try {
59+
const result = await serverErrorHandler(err, c);
60+
if (result instanceof Response) {
61+
return result;
62+
}
63+
} catch (configError) {
64+
logger.error(`Error in serverConfig.onError handler: ${configError}`);
65+
}
66+
}
67+
const bffPrefix = config.bff?.prefix || '/api';
68+
const isApiPath = c.req.path.startsWith(bffPrefix);
69+
70+
if (isApiPath) {
71+
return c.json(
72+
{
73+
message: (err as any)?.message || '[BFF] Internal Server Error',
74+
},
75+
(err as any)?.status || 500,
76+
);
77+
} else {
78+
return c.html(createErrorHtml(500), 500);
79+
}
5680
});
5781

5882
const loggerOptions = config.server.logger;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Api, Get } from '@modern-js/plugin-bff/hono';
2+
import { HTTPException } from 'hono/http-exception';
3+
4+
export default async () => {
5+
throw new Error('Intentional error in get');
6+
};
7+
8+
export const exception = Api(Get('/exception'), async () => {
9+
throw new HTTPException(401, { message: 'exception with 401' });
10+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Api, Get } from '@modern-js/plugin-bff/hono';
2+
import { HTTPException } from 'hono/http-exception';
3+
4+
export default async () => {
5+
throw new Error('Intentional error in get');
6+
};
7+
8+
export const exceptionManaged = Api(Get('/managed/exception'), async () => {
9+
throw new HTTPException(401, { message: 'exception with 401' });
10+
});

tests/integration/bff-hono/server/modern.server.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ export default defineServerConfig({
4242
handler: timing,
4343
},
4444
],
45+
onError: (err, c) => {
46+
if (c.req.path.toLowerCase().includes('managed')) {
47+
return c.json({ error: 'customize Respons in config serverConfig' }, 501);
48+
}
49+
},
4550
});

0 commit comments

Comments
 (0)