Egg 框架的 eggjs/onerror 插件统一异常处理与内容协商机制深度解析【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/eggeggjs/onerror是 Egg 框架默认内置的错误处理插件负责在应用层统一接管ctx.onerror并为 HTML、文本、JSON、JSONPjs以及兜底all五种响应类型提供可配置的响应协商与处理机制。本文基于当前仓库的源码与测试完整梳理该插件的配置项、执行流程、环境差异策略与自定义扩展方案帮助读者在生产环境中精准掌控异常响应行为。插件定位Egg 默认的错误处理层onerror插件在 Egg 中默认启用其核心职责有两层安装ctx.onerror让每个请求上下文在被 Koa 内核捕获到异常时走统一的错误响应通道按需协商响应类型根据客户端可接受的响应类型html / text / json / js选择对应的错误处理器输出不同的响应体。插件的入口定义在 plugins/onerror/src/index.ts它通过definePluginFactory注册export default definePluginFactory({ name: onerror, enable: true, path: import.meta.dirname, optionalDependencies: [jsonp], }) as EggPluginFactory;注意optionalDependencies: [jsonp]——只有当应用同时启用了jsonp插件时JSONP 错误响应才会生效。同时插件通过 plugins/onerror/src/types.ts 的模块声明扩展了EggAppConfig使config.onerror具备完整的 TypeScript 类型提示。onerror对应用app与代理进程agent都有覆盖应用侧由 plugins/onerror/src/app.ts 的Boot类在didLoad生命周期安装错误处理逻辑代理侧由 plugins/onerror/src/agent.ts 负责监听error事件并写入coreLogger避免 agent 进程的异常被静默吞掉。自持实现不再依赖 koa-onerror与早期版本不同当前仓库中的 onerror 插件不再从koa-onerror包导入实现而是在 plugins/onerror/src/lib/onerror.ts 中内置了一份 Koa 风格的onerror()实现。这一改动的直接收益是响应行为完全由插件本地掌控当插件 app 启动钩子boot hook被静态打包工具引入时不再需要读取koa-onerror包内的模板资源这对静态打包static bundling场景至关重要——插件不再对第三方包的文件读取产生隐性依赖。从仓库结构看lib/onerror.ts导出的onerror(app, options)接收应用实例与OnerrorOptions将自定义的app.context.onerror挂载到应用上下文中返回值仍然是原应用实例方便链式调用。OnerrorOptions的完整类型定义如下摘自 plugins/onerror/src/lib/onerror.tsexport interface OnerrorOptions { text?: OnerrorHandler; json?: OnerrorHandler; html?: OnerrorHandler; all?: OnerrorHandler; js?: OnerrorHandler; redirect?: string | null; accepts?: (...args: string[]) string; }默认处理器内置了text、json、html三种defaultOptionsall、js、redirect等则按需启用。配置项全解从 errorPageUrl 到 appErrorFilter插件的默认配置定义在 plugins/onerror/src/config/config.default.tsexport default { onerror: { errorPageUrl: , appErrorFilter: undefined, templatePath: , } as OnerrorConfig, };errorPageUrl类型string | ((err, ctx) string)默认作用生产环境下当用户请求 HTML 页面且发生意外错误时跳转到该地址。支持函数形式动态决定跳转目标。官方 README 的示例plugins/onerror/README.md// config/config.default.ts import { defineConfig } from egg; export default defineConfig({ onerror: { // errorPageUrl support function errorPageUrl: (err, ctx) ctx.errorPageUrl || /500, }, });从 plugins/onerror/src/app.ts 的实现可以看到errorPageUrl在跳转时还会追加real_status${status}查询参数自动根据 URL 中是否已有?选择或?这样错误页仍能感知真实状态码if (errorPageUrl) { const statusQuery (errorPageUrl.indexOf(?) 0 ? : ?) real_status${status}; return ctx.redirect(errorPageUrl statusQuery); }测试夹具 plugins/onerror/test/fixtures/onerror-custom-500/app/router.js 演示了配套用法在路由中定义/500自定义错误页并通过ctx.errorPageUrl /specialerror在特定请求里临时指定跳转目标。accepts类型Function默认见下作用检测客户端期望的响应类型返回html | text | json | js。默认的accepts实现在 plugins/onerror/src/lib/utils.tsexport function accepts(ctx: Context): json | js | html { if (ctx.acceptJSON) return json; if (ctx.acceptJSONP) return js; return html; }即客户端期望 JSON 时返回json期望 JSONP 时返回js否则一律按 HTML 处理。README 提供了自定义示例——把带x-requested-with: XMLHttpRequest头的请求一律识别为 JSON// an accept detect function that mark all request with x-requested-withXMLHttpRequest header accepts json. function accepts(ctx) { if (ctx.get(x-requested-with) XMLHttpRequest) return json; return html; }all / html / text / json / js类型Function作用分别自定义对应响应类型的错误处理器其中all一旦提供将忽略内容协商结果所有错误统一走该处理器。在 plugins/onerror/src/lib/onerror.ts 的核心流程中if (options.all) { options.all.call(this, err, this); } else if (options.redirect type ! json) { this.redirect(options.redirect); } else { const handler getHandler(options, type); handler?.call(this, err, this); this.type type; }优先级链为all全局兜底→redirect非 JSON 响应时重定向→ 按协商类型分发到具体 handler。appErrorFilter类型(err, ctx) boolean默认undefined作用拦截app上派发的error事件。当函数返回false时插件不再记录该错误日志你可以在appErrorFilter内部自行记录日志并返回false从而覆盖默认的错误日志行为。该逻辑位于 plugins/onerror/src/app.ts 的app.on(error)监听器中app.on(error, (err, ctx) { if (!ctx) { ctx app.currentContext || app.createAnonymousContext(); } if (config.appErrorFilter !config.appErrorFilter(err, ctx)) return; const status detectStatus(err); // 5xx if (status 500) { try { ctx.logger.error(err); } catch (ex) { app.logger.error(err); app.logger.error(ex); } return; } // 4xx try { ctx.logger.warn(err); } catch (ex) { app.logger.warn(err); app.logger.error(ex); } });这里也体现了日志分级策略5xx 用error级别、4xx 用warn级别。当ctx缺失时会尝试使用app.currentContext或匿名上下文兜底保证日志始终有落点。templatePath类型string默认作用自定义开发环境 HTML 错误页的 Mustache 模板路径。为空时使用内置模板ONERROR_PAGE_TEMPLATE定义在 plugins/onerror/src/lib/onerror_page.ts。加载逻辑在 plugins/onerror/src/app.ts 的didLoad中const viewTemplate config.templatePath ? fs.readFileSync(config.templatePath, utf8) : (await import(./lib/onerror_page.ts)).ONERROR_PAGE_TEMPLATE;仓库提供了完整的自定义模板参考plugins/onerror/test/fixtures/onerror-custom-template/template.mustache它展示了模板可用的全部数据变量包括status、name、message、request.url、request.method、request.httpVersion、request.headers、request.cookies、appInfo.baseDir、appInfo.config以及调用栈frames等。核心执行流程ctx.onerror 的完整生命周期在 plugins/onerror/src/lib/onerror.ts 中app.context.onerror的执行顺序可以拆解为六个阶段空值短路err null直接返回排空请求流如果this.req.resume可用先resume()排空请求 body避免连接挂起非 Error 包装若抛出的不是Error实例例如抛了普通对象、字符串会构造new Error(non-error thrown: ...)并尽量保留原值的name、message、stack、status、headers属性对象序列化优先用JSON.stringify遇到循环引用则退回inspect对应测试formats circular non-error throws when JSON.stringify fails状态码规整ENOENT错误归一为 404status不是合法 HTTP 状态码时统一为 500对应测试wraps non-error throws and normalizes invalid status to 500status: 1会被规整为 500响应头清理与重建调用clearResponseHeaders清空已设置的响应头但保留set-cookie随后重新设置err.headers协商与输出调用accepts(html, text, json, js)得到响应类型走all/redirect/ 具体 handler 分发最后this.res.end(this.body)结束响应。其中响应头清理逻辑值得注意function clearResponseHeaders(ctx: any): void { const headers ctx.response?.header ?? ctx.response?.headers ?? ctx.res.getHeaders?.() ?? {}; for (const name of Object.keys(headers)) { if (name.toLowerCase() set-cookie) continue; ctx.res.removeHeader(name); } }即使响应头来源ctx.response.header/ctx.response.headers/ctx.res.getHeaders()在不同 Koa 版本中不同也能正确取到同时刻意跳过set-cookie避免错误处理时把已设置的 Cookie 一并抹掉。测试does not pass undefined headers into ctx.set验证了当响应头里只有set-cookie时不会误传给ctx.set。在 plugins/onerror/src/app.ts 的didLoad中应用层会把这些能力组装进errorOptions默认accepts、默认html/json/js处理器都定义在这里随后通过onerror(app, errorOptions)完成安装。同时config.onerror中用户自定义的all/html/json/text/js会覆盖对应默认处理器const keys: (keyof OnerrorConfig)[] [all, html, json, text, js]; for (const type of keys) { if (config[type]) { Reflect.set(errorOptions, type, config[type]); } }环境差异local / unittest / production 三种响应策略插件的响应策略与app.config.env强相关isProd的定义plugins/onerror/src/lib/utils.ts为export function isProd(app: Application): boolean { return app.config.env ! local app.config.env ! unittest; }开发环境localHTML 请求走ErrorView可视化错误页见下一节展示完整调用栈、请求详情与脱敏后的配置信息JSON 请求则返回完整错误对象包含stack、name以及错误对象上的其他属性便于本地调试。测试环境unittestHTML 请求返回简单文本格式方便断言if (app.config.env unittest) { ctx.status status; ctx.body ${err.name}: ${err.message}\n${err.stack}; return; }生产环境production遵循不泄露内部细节原则响应体只包含通用信息5xxHTML 请求在配置了errorPageUrl时跳转并附加real_status未配置时返回固定文案Internal Server Error, real status: ${status}JSON 请求只返回{ code, message }其中message是http.STATUS_CODES[status]的通用描述而非真实错误信息4xx可以安全返回具体信息。HTML 返回${status} ${http.STATUS_CODES[status]}JSON 返回{ code, message, errors }其中errors用于携带字段校验等结构化错误明细。同样的环境策略也体现在lib/onerror.ts的默认text/json/html处理器中isDev() || err.expose才输出err.message否则使用通用状态文本。如果某个错误确实可以对外暴露比如用户输入错误可以通过设置err.expose true在开发与生产环境都输出真实消息。相关测试uses generic status text for non-exposed production errors验证了在NODE_ENVproduction下503错误只会返回Service Unavailable而非内部细节。ErrorView开发环境的可视化错误诊断页当env为local时HTML 错误由ErrorView类渲染plugins/onerror/src/lib/error_view.ts。它借鉴了 youch 的思路将一次异常渲染成结构化的诊断页面核心能力包括调用栈解析使用stack-trace解析错误区分 Node 原生帧isNode与应用帧isApp并为每个非原生帧读取源码上下文前后各 5 行codeContext 5渲染时原生帧默认折叠可通过页面上的 Show all frames 开关展开请求信息序列化展示 URI、方法、HTTP 版本、连接信息、请求头与 Cookie默认过滤cookie与connection两个敏感头_filterHeaders配置脱敏应用配置通过redactConfig递归脱敏默认忽略pass、pwd、password、keys、masterKey、accessKey及匹配/secret/i的键统一替换为Redacted防止本地调试页泄露敏感配置。若应用配置了dump.ignore则优先使用该列表getConfigIgnoreList循环引用保护使用WeakSet追踪祖先对象遇到循环引用输出[Circular]Mustache 渲染toHTML()将序列化数据与模板内置或自定义合并渲染。内置模板ONERROR_PAGE_TEMPLATEplugins/onerror/src/lib/onerror_page.ts自带 Prism 代码高亮、行号与帧切换交互开发者可以零成本获得开箱即用的排错体验需要品牌化或定制布局时则通过上文提到的onerror.templatePath替换。输出行为细节状态、头与 JSON 序列化lib/onerror.ts中有若干容易被忽视但已被测试锁定的细节JSON 响应不再双重序列化type json时仅当this.body不是字符串才执行JSON.stringify自定义 json handler 若直接返回字符串字面量不会被再次转义测试does not double stringify custom json string bodiesHTML 转义默认 HTML 处理器对状态码与消息做escapeHtml转义 防止错误消息注入 HTML测试escapes default html responses验证被转义为amp;lt;gt;quot;#39;响应头已发送场景当headerSent为真响应已开始输出只触发app.emit(error)记录日志标记err.headerSent true后立即返回不再尝试写响应体测试only emits when headers were already sentredirect 兜底OnerrorOptions.redirect可在非 JSON 响应时统一重定向到指定地址测试redirects non-json responses when configured。这些行为均有对应的单元测试plugins/onerror/test/onerror_lib.test.ts覆盖测试通过构造TestApp/TestContext直接驱动app.context.onerror验证请求流排空、错误事件派发、响应头清理重建、状态码规整与各类型 handler 的分发结果。常见问题排查建议JSON 请求拿到了 HTML 错误页检查是否覆盖了默认accepts或确认请求头中是否带了Accept: application/json默认实现依据ctx.acceptJSON/ctx.acceptJSONP判断而不是裸的Accept头生产环境看不到真实错误消息这是安全设计——只有err.expose true的错误才会在生产环境暴露具体message5xx 默认只返回通用状态文本自定义 JSON 结构不生效确认config.onerror.json是否被正确配置且返回值会被JSON.stringify字符串除外错误页配置脱敏未按预期生效检查应用是否配置了dump.ignore该配置会覆盖插件内置的默认忽略列表。小结eggjs/onerror用一套默认启用、完全可配置的设计覆盖了 Egg 应用从开发调试到生产兜底的全部异常响应场景开发环境提供带源码上下文的可视化错误页生产环境自动切换为不泄露细节的通用响应并通过errorPageUrl、accepts、all/html/text/json/js、appErrorFilter、templatePath等配置项满足不同业务的自定义诉求。理解其自持实现、协商流程与环境差异能帮助你在排查线上问题时迅速定位响应行为背后的真实原因。继续阅读插件完整说明与配置示例plugins/onerror/README.md核心实现plugins/onerror/src/lib/onerror.ts、plugins/onerror/src/app.ts错误页渲染plugins/onerror/src/lib/error_view.ts、内置模板 plugins/onerror/src/lib/onerror_page.ts默认配置与类型plugins/onerror/src/config/config.default.ts、plugins/onerror/src/types.ts单元测试与集成夹具plugins/onerror/test/onerror_lib.test.ts、plugins/onerror/test/fixtures/onerror-custom-template/template.mustache【免费下载链接】egg Born to build better enterprise frameworks and apps with Node.js Koa. https://307.run/eggcode项目地址: https://gitcode.com/gh_mirrors/eg/egg创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
