异常处理
异常处理中间件的职责很单一:包住后续管道,把未处理异常统一转成 HTTP 响应。
参考实现的核心有三个关键点:
next(context)外层统一try/catchKnowException这类业务异常直接返回业务消息- 其他异常统一返回 500;开发环境附带
stackTrace,同时记录错误日志
最小示例
cangjie
package web
import soulsoft_web_http.*
import soulsoft_web_hosting.*
import soulsoft_web_routing.*
import soulsoft_extensions_injection.*
import soulsoft_extensions_logging.*
import stdx.encoding.json.*
class KnowException <: Exception {
public init(message: String) {
super(message)
}
}
class ExceptionHandlerMiddleware <: IMiddleware {
private let logger: ILogger
public ExceptionHandlerMiddleware(
let environment: IWebHostEnvironment,
let loggerFactory: ILoggerFactory
) {
logger = loggerFactory.createLogger<ExceptionHandlerMiddleware>()
}
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
try {
next(context)
} catch (ex: Exception) {
let obj = JsonObject()
obj.put("status", JsonInt(500))
if (ex is KnowException) {
obj.put("description", JsonString(ex.message))
} else {
obj.put("description", JsonString("服务器错误"))
if (environment.isDevelopment()) {
obj.put("stackTrace", JsonString(ex.toString()))
}
logger.error(ex, ex.message)
}
context.response.statusCode = StatusCodes.InternalServerError
context.response.writeAsJson(obj)
}
}
}
extend ApplicationBuilder {
public func useExceptionHandler() {
this.use<ExceptionHandlerMiddleware>([])
}
}
main(args: Array<String>): Int64 {
let builder = WebHost.createBuilder(args)
builder.services.addRouting()
let app = builder.build()
app.useExceptionHandler()
app.useRouting()
app.mapGet("/known") {
_ => throw KnowException("这是一个可预期的业务错误")
}
app.mapGet("/unknown") {
_ => throw Exception("这是一个未处理异常")
}
app.mapGet("/ok") {
ctx => ctx.response.write("ok")
}
app.run()
return 0
}这个版本刻意只保留最小依赖:
IWebHostEnvironment:判断是不是开发环境ILoggerFactory:给未知异常记日志stdx.encoding.json.*:构造 JSON 响应
管道顺序
异常处理中间件必须放在最外层,否则它后面的中间件抛异常时,没人接。
cangjie
app.useExceptionHandler() // ① 最外层兜底
app.useRouting() // ② 匹配 URL,挂 Endpoint
app.useAuthentication() // ③ 可选:认证
app.useAuthorization() // ④ 可选:授权
app.mapGet(...) // ⑤ 注册终结点如果还要挂静态文件、健康检查,一般也是先异常处理,再其他中间件:
cangjie
app.useExceptionHandler()
app.useHealthChecks("/health")
app.useFileServer()
app.useRouting()响应行为
已知业务异常
访问 /known:
json
{"status":500,"description":"这是一个可预期的业务错误"}这里和参考实现保持一致:虽然是业务异常,HTTP 状态码仍然统一写 500,只是把错误描述换成可直接返回的业务消息。
未知异常
访问 /unknown:
生产环境:
json
{"status":500,"description":"服务器错误"}开发环境:
json
{"status":500,"description":"服务器错误","stackTrace":"..."}开发环境判断来自 environment.isDevelopment()。可以用下面任一方式启动:
powershell
$env:cangjie_environment = "Development"
cjpm run或者:
powershell
cjpm run -- --environment=Development为什么要区分 KnowException
参考实现里,KnowException 本质上是“可以安全返回给调用方”的业务异常,比如:
- 用户名或密码错误
- 参数缺失
- 当前资源状态不允许该操作
这类错误如果统一返回“服务器错误”,前端没法给出明确提示;但如果把所有异常原文都暴露出去,又会把内部实现细节泄漏给外部。
所以常见做法就是分两层:
- 业务可预期异常:返回明确提示
- 非预期异常:返回通用消息,日志里保留细节
一个推荐写法
如果项目里会反复用到这套逻辑,推荐把中间件单独放文件,再扩展 ApplicationBuilder 暴露一个稳定入口:
cangjie
extend ApplicationBuilder {
public func useExceptionHandler() {
this.use<ExceptionHandlerMiddleware>([])
}
}这样有两个直接好处:
- 调用方只写
app.useExceptionHandler(),不会暴露中间件构造细节 - 构造函数依赖仍然由 DI 自动解析,后续补错误码映射、审计日志、统一响应模型也只改这一处