diff --git a/cors/pkg.generated.mbti b/cors/pkg.generated.mbti index 3e5a734..9930930 100644 --- a/cors/pkg.generated.mbti +++ b/cors/pkg.generated.mbti @@ -10,7 +10,7 @@ pub fn append_cors_headers(@mocket.MocketEvent, origin? : String, methods? : Str pub fn append_cors_preflight_headers(@mocket.MocketEvent, origin? : String, methods? : String, allow_headers? : String, credentials? : Bool, max_age? : Int) -> Unit -pub fn handle_cors(origin? : String, methods? : String, allow_headers? : String, expose_headers? : String, credentials? : Bool, max_age? : Int) -> async (@mocket.MocketEvent, async () -> &@mocket.Responder noraise) -> &@mocket.Responder noraise +pub fn handle_cors(origin? : String, methods? : String, allow_headers? : String, expose_headers? : String, credentials? : Bool, max_age? : Int) -> async (@mocket.MocketEvent, async () -> &@mocket.Responder) -> &@mocket.Responder pub fn is_preflight_request(@mocket.MocketEvent) -> Bool diff --git a/dispatch.mbt b/dispatch.mbt index 2fdf5db..e485f84 100644 --- a/dispatch.mbt +++ b/dispatch.mbt @@ -5,7 +5,7 @@ pub async fn dispatch_http( url : String, headers : Map[StringView, StringView], raw_body : Bytes, -) -> HttpResponse noraise { +) -> HttpResponse { let (params, handler) = match mocket.find_route(http_method, url) { Some((h, p)) => (p, h) _ => ({}, handle_not_found()) @@ -15,7 +15,14 @@ pub async fn dispatch_http( res: HttpResponse::new(OK), params, } - let responder = mocket.execute_middlewares(event, handler) + let responder = mocket.execute_middlewares(event, handler) catch { + err => { + if @async.is_cancellation_error(err) { + raise err + } + mocket.handle_request_error(event, err) + } + } responder.options(event.res) let buf = Buffer() responder.output(buf) diff --git a/error.mbt b/error.mbt index 294a516..48278e0 100644 --- a/error.mbt +++ b/error.mbt @@ -6,3 +6,30 @@ pub suberror NetworkError ///| pub suberror ExecError + +///| +// 请求错误处理器:接收事件和未捕获的错误,返回一个响应。 +// 用于记录日志、返回自定义错误响应等。 +pub type ErrorHandler = (MocketEvent, Error) -> &Responder + +///| +// 默认错误处理器:返回 500 Internal Server Error 并附带错误信息。 +fn default_error_handler(_event : MocketEvent, err : Error) -> &Responder { + HttpResponse::new(InternalServerError).body(err.to_string()) +} + +///| +// 将未捕获的请求错误转换为响应,优先使用用户注册的错误处理器。 +fn Mocket::handle_request_error( + self : Mocket, + event : MocketEvent, + err : Error, +) -> &Responder { + (self.error_handler)(event, err) +} + +///| +// 注册请求错误处理器,用于记录日志或返回自定义错误响应。 +pub fn Mocket::on_error(self : Mocket, handler : ErrorHandler) -> Unit { + self.error_handler = handler +} diff --git a/error_wbtest.mbt b/error_wbtest.mbt new file mode 100644 index 0000000..6c0a021 --- /dev/null +++ b/error_wbtest.mbt @@ -0,0 +1,51 @@ +///| +suberror TestRequestError { + TestRequestError(String) +} derive(Debug) + +///| +async test "dispatch converts unhandled handler errors to 500" { + let app = new() + app.get("/boom", _ => raise TestRequestError("boom")) + let response = dispatch_http(app, "GET", "/boom", {}, b"") + inspect(response.status_code.to_int(), content="500") + let body : String = response.read_body() + assert_true(body.length() > 0) +} + +///| +async test "dispatch uses custom error handler" { + let app = new() + let log : Array[String] = [] + app.on_error((event, err) => { + log.push("\{event.req.http_method} \{event.req.url}: \{err}") + HttpResponse::new(Custom(599)).body("custom") + }) + app.post("/custom", _ => raise TestRequestError("custom")) + + let response = dispatch_http(app, "POST", "/custom", {}, b"") + inspect(response.status_code.to_int(), content="599") + let body : String = response.read_body() + @test.assert_eq(body, "custom") + @test.assert_eq(log.length(), 1) + assert_true(log[0].contains("POST /custom")) + assert_true(log[0].contains("custom")) +} + +///| +async test "handlers can still catch errors locally" { + let app = new() + app.get("/local", _ => { + let text = try { + raise TestRequestError("local") + } catch { + TestRequestError(msg) => msg + } + text + }) + + let response = dispatch_http(app, "GET", "/local", {}, b"") + inspect(response.status_code.to_int(), content="200") + let body : String = response.read_body() + @test.assert_eq(body, "local") +} diff --git a/examples/route/pkg.generated.mbti b/examples/route/pkg.generated.mbti index 1013099..edca620 100644 --- a/examples/route/pkg.generated.mbti +++ b/examples/route/pkg.generated.mbti @@ -6,7 +6,7 @@ import { } // Values -pub fn not_found_middleware() -> async (@mocket.MocketEvent, async () -> &@mocket.Responder noraise) -> &@mocket.Responder noraise +pub fn not_found_middleware() -> async (@mocket.MocketEvent, async () -> &@mocket.Responder) -> &@mocket.Responder // Errors diff --git a/handler.mbt b/handler.mbt index 7718114..d0508f0 100644 --- a/handler.mbt +++ b/handler.mbt @@ -1,2 +1,2 @@ ///| -pub type HttpHandler = async (MocketEvent) -> &Responder noraise +pub type HttpHandler = async (MocketEvent) -> &Responder diff --git a/index.mbt b/index.mbt index 9e820d0..0c47709 100644 --- a/index.mbt +++ b/index.mbt @@ -24,6 +24,7 @@ pub(all) struct Mocket { ws_channels : Map[String, Map[String, Unit]] ws_client_port : Map[String, Int] max_body_size : Int + mut error_handler : ErrorHandler } ///| @@ -42,6 +43,7 @@ pub fn new(base_path? : String = "", max_body_size? : Int = 1048576) -> Mocket { ws_channels: {}, ws_client_port: {}, max_body_size, + error_handler: default_error_handler, } } diff --git a/middleware.mbt b/middleware.mbt index d22fcee..ce47668 100644 --- a/middleware.mbt +++ b/middleware.mbt @@ -1,9 +1,9 @@ ///| -pub type MiddlewareNext = async () -> &Responder noraise +pub type MiddlewareNext = async () -> &Responder ///| // 中间件类型:接受 HttpEvent 和 next 函数,返回 HttpBody -pub type Middleware = async (MocketEvent, MiddlewareNext) -> &Responder noraise +pub type Middleware = async (MocketEvent, MiddlewareNext) -> &Responder ///| priv struct MiddlewareTrieEntry { @@ -126,7 +126,7 @@ async fn Mocket::execute_middlewares( self : Mocket, event : MocketEvent, final_handler : HttpHandler, -) -> &Responder noraise { +) -> &Responder { if self.middlewares.is_empty() { return final_handler(event) } @@ -141,7 +141,7 @@ pub async fn execute_middlewares( middlewares : Array[(String, Middleware)], event : MocketEvent, final_handler : HttpHandler, -) -> &Responder noraise { +) -> &Responder { if middlewares.is_empty() { return final_handler(event) } @@ -168,14 +168,14 @@ async fn execute_middleware_chain( index : Int, event : MocketEvent, final_handler : HttpHandler, -) -> &Responder noraise { +) -> &Responder { if index >= middlewares.length() { // 所有中间件都执行完毕,调用最终处理器 final_handler(event) } else { // 执行当前中间件 let current_middleware = middlewares[index] - let next = async fn() noraise { + let next = async fn() { execute_middleware_chain(middlewares, index + 1, event, final_handler) } current_middleware(event, next) diff --git a/mocket.js.mbt b/mocket.js.mbt index d09bb59..126826b 100644 --- a/mocket.js.mbt +++ b/mocket.js.mbt @@ -374,7 +374,9 @@ pub fn listen_ffi(mocket : Mocket, address : String) -> Unit { } // 执行中间件链和处理器 - let responder = mocket.execute_middlewares(event, handler) + let responder = mocket.execute_middlewares(event, handler) catch { + err => mocket.handle_request_error(event, err) + } // let boundary = "----------------moonbit-" + port.to_string() responder.options(event.res) res.write_head( diff --git a/mocket.native.mbt b/mocket.native.mbt index a863180..763b539 100644 --- a/mocket.native.mbt +++ b/mocket.native.mbt @@ -404,7 +404,14 @@ async fn handle_http_request( request_route_path(request.path), string_headers_to_views(request.headers), raw_body, - ) + ) catch { + err => { + if @async.is_cancellation_error(err) { + raise err + } + HttpResponse::new(InternalServerError).body(err.to_string()) + } + } send_native_response(request, conn, response) } diff --git a/native/mongoose/mongoose.mbt b/native/mongoose/mongoose.mbt index bb655ba..85bb326 100644 --- a/native/mongoose/mongoose.mbt +++ b/native/mongoose/mongoose.mbt @@ -184,7 +184,12 @@ fn handle_request( async_run(async fn() noraise { let response = @mocket.dispatch_http( mocket, http_method, url, headers, raw_body, - ) + ) catch { + _ => + @mocket.HttpResponse::new(@mocket.InternalServerError).body( + "Internal Server Error", + ) + } res.status(response.status_code.to_int()) response.headers.each((key, value) => { res.set_header(to_cbytes(key), to_cbytes(value)) diff --git a/performance_wbtest.mbt b/performance_wbtest.mbt index 928947d..93b2551 100644 --- a/performance_wbtest.mbt +++ b/performance_wbtest.mbt @@ -1,6 +1,6 @@ ///| #warnings("-unused_async") -async fn benchmark_route_handler(_event : MocketEvent) -> &Responder noraise { +async fn benchmark_route_handler(_event : MocketEvent) -> &Responder { "ok" } @@ -8,7 +8,7 @@ async fn benchmark_route_handler(_event : MocketEvent) -> &Responder noraise { async fn benchmark_middleware( _event : MocketEvent, next : MiddlewareNext, -) -> &Responder noraise { +) -> &Responder { next() } diff --git a/pkg.generated.mbti b/pkg.generated.mbti index bc31229..5eaf240 100644 --- a/pkg.generated.mbti +++ b/pkg.generated.mbti @@ -11,7 +11,7 @@ pub fn __ws_emit(Bytes, Bytes, Bytes) -> Unit pub fn cookie_to_string(Array[CookieItem]) -> String -pub async fn dispatch_http(Mocket, String, String, Map[StringView, StringView], Bytes) -> HttpResponse noraise +pub async fn dispatch_http(Mocket, String, String, Map[StringView, StringView], Bytes) -> HttpResponse pub fn dispatch_ws_event((WebSocketEvent) -> Unit, WebSocketPeer, String, Bytes) -> Unit @@ -19,11 +19,11 @@ pub fn encode_multipart(Map[String, MultipartFormValue], String) -> String pub fn escape_html(String) -> String -pub async fn execute_middlewares(Array[(String, async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise)], MocketEvent, async (MocketEvent) -> &Responder noraise) -> &Responder noraise +pub async fn execute_middlewares(Array[(String, async (MocketEvent, async () -> &Responder) -> &Responder)], MocketEvent, async (MocketEvent) -> &Responder) -> &Responder pub fn form_encode(Map[String, String]) -> String -pub fn handle_not_found() -> async (MocketEvent) -> &Responder noraise +pub fn handle_not_found() -> async (MocketEvent) -> &Responder pub fn html(&Show) -> &Responder @@ -117,65 +117,67 @@ pub impl Responder for HttpResponse #alias(T) pub(all) struct Mocket { base_path : String - mappings : Map[(String, String), async (MocketEvent) -> &Responder noraise] - middlewares : Array[(String, async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise)] - static_routes : Map[String, Map[String, async (MocketEvent) -> &Responder noraise]] - dynamic_routes : Map[String, Array[(String, async (MocketEvent) -> &Responder noraise)]] + mappings : Map[(String, String), async (MocketEvent) -> &Responder] + middlewares : Array[(String, async (MocketEvent, async () -> &Responder) -> &Responder)] + static_routes : Map[String, Map[String, async (MocketEvent) -> &Responder]] + dynamic_routes : Map[String, Array[(String, async (MocketEvent) -> &Responder)]] ws_static_routes : Map[String, (WebSocketEvent) -> Unit] ws_dynamic_routes : Array[(String, (WebSocketEvent) -> Unit)] ws_clients : Map[String, Unit] ws_channels : Map[String, Map[String, Unit]] ws_client_port : Map[String, Int] max_body_size : Int + mut error_handler : (MocketEvent, Error) -> &Responder // private fields } -pub fn Mocket::acl(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::all(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::bind(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::checkin(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::checkout(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::connect(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::copy(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::delete(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::get(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +pub fn Mocket::acl(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::all(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::bind(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::checkin(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::checkout(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::connect(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::copy(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::delete(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::get(Self, String, async (MocketEvent) -> &Responder) -> Unit pub fn Mocket::group(Self, String, (Self) -> Unit) -> Unit -pub fn Mocket::head(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::label(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::link(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +pub fn Mocket::head(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::label(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::link(Self, String, async (MocketEvent) -> &Responder) -> Unit pub async fn Mocket::listen(Self, String) -> Unit noraise -pub fn Mocket::lock(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::merge(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::mkactivity(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::mkcalendar(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::mkcol(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::mkredirectref(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::mkworkspace(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::move_(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::on(Self, String, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::options(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::orderpatch(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::patch(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::post(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::pri(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::propfind(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::proppatch(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::put(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::query(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::rebind(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::report(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::search(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +pub fn Mocket::lock(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::merge(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::mkactivity(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::mkcalendar(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::mkcol(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::mkredirectref(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::mkworkspace(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::move_(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::on(Self, String, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::on_error(Self, (MocketEvent, Error) -> &Responder) -> Unit +pub fn Mocket::options(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::orderpatch(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::patch(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::post(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::pri(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::propfind(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::proppatch(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::put(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::query(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::rebind(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::report(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::search(Self, String, async (MocketEvent) -> &Responder) -> Unit #deprecated pub async fn Mocket::serve(Self, port~ : Int) -> Unit noraise pub fn Mocket::static_assets(Self, String, &ServeStaticProvider) -> Unit -pub fn Mocket::trace(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::unbind(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::uncheckout(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::unlink(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::unlock(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::update(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::updateredirectref(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit -pub fn Mocket::use_middleware(Self, async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise, base_path? : String) -> Unit -pub fn Mocket::version_control(Self, String, async (MocketEvent) -> &Responder noraise) -> Unit +pub fn Mocket::trace(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::unbind(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::uncheckout(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::unlink(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::unlock(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::update(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::updateredirectref(Self, String, async (MocketEvent) -> &Responder) -> Unit +pub fn Mocket::use_middleware(Self, async (MocketEvent, async () -> &Responder) -> &Responder, base_path? : String) -> Unit +pub fn Mocket::version_control(Self, String, async (MocketEvent) -> &Responder) -> Unit pub fn Mocket::ws(Self, String, (WebSocketEvent) -> Unit) -> Unit pub(all) struct MocketEvent { @@ -301,11 +303,13 @@ pub fn WebSocketPeer::to_string(Self) -> String pub fn WebSocketPeer::unsubscribe(Self, String) -> Unit // Type aliases -pub type HttpHandler = async (MocketEvent) -> &Responder noraise +pub type ErrorHandler = (MocketEvent, Error) -> &Responder -pub type Middleware = async (MocketEvent, async () -> &Responder noraise) -> &Responder noraise +pub type HttpHandler = async (MocketEvent) -> &Responder -pub type MiddlewareNext = async () -> &Responder noraise +pub type Middleware = async (MocketEvent, async () -> &Responder) -> &Responder + +pub type MiddlewareNext = async () -> &Responder pub type WebSocketHandler = (WebSocketEvent) -> Unit diff --git a/static.mbt b/static.mbt index 0343332..f036906 100644 --- a/static.mbt +++ b/static.mbt @@ -62,7 +62,7 @@ pub fn Mocket::static_assets( path : String, provider : &ServeStaticProvider, ) -> Unit { - self.use_middleware(async fn(event, next) noraise { + self.use_middleware(async fn(event, next) { if !(match_path(path, event.req.url) is None) { return next() }