Skip to main content

sz_rust_core/
error_handler.rs

1//! 错误处理 — 404/500 标准化 JSON 响应
2//!
3//! 对齐 PHP 异常处理:所有错误响应统一返回 `{code:0, msg, data:{}}` JSON 结构。
4//!
5//! ## 功能
6//!
7//! - [`not_found_handler`]:404 处理器,返回 `{code:0, msg:"Not Found", data:{}}`
8//! - [`internal_error_handler`]:500 处理器,返回 `{code:0, msg:"Internal Error", data:{}}`
9//! - [`method_not_allowed_handler`]:405 处理器
10//! - [`error_router`]:预配置的错误处理 Router(包含 fallback + 500 处理)
11//! - [`HandleError`]:自定义错误类型,可转换为标准 JSON 响应
12//!
13//! ## 用法
14//!
15//! ```ignore
16//! use sz_rust_core::error_handler::error_router;
17//! use axum::Router;
18//!
19//! let app: Router = Router::new()
20//!     .route("/api", axum::routing::get(|| async { "ok" }))
21//!     .merge(error_router());
22//! ```
23
24use crate::response::ApiResponse;
25use axum::http::StatusCode;
26use axum::response::{IntoResponse, Response};
27use axum::Router;
28use serde_json::json;
29
30/// 404 处理器 — 返回 `{code:0, msg:"Not Found", data:{}}`
31///
32/// 用于 `Router::fallback`。
33pub async fn not_found_handler() -> Response {
34    let resp = ApiResponse::error("Not Found");
35    let body = resp.to_json_string();
36    (
37        StatusCode::NOT_FOUND,
38        [(
39            axum::http::header::CONTENT_TYPE,
40            "application/json; charset=utf-8",
41        )],
42        body,
43    )
44        .into_response()
45}
46
47/// 500 处理器 — 返回 `{code:0, msg:"Internal Error", data:{}}`
48pub async fn internal_error_handler() -> Response {
49    let resp = ApiResponse::error("Internal Error");
50    let body = resp.to_json_string();
51    (
52        StatusCode::INTERNAL_SERVER_ERROR,
53        [(
54            axum::http::header::CONTENT_TYPE,
55            "application/json; charset=utf-8",
56        )],
57        body,
58    )
59        .into_response()
60}
61
62/// 405 处理器 — 返回 `{code:0, msg:"Method Not Allowed", data:{}}`
63pub async fn method_not_allowed_handler() -> Response {
64    let resp = ApiResponse::error("Method Not Allowed");
65    let body = resp.to_json_string();
66    (
67        StatusCode::METHOD_NOT_ALLOWED,
68        [(
69            axum::http::header::CONTENT_TYPE,
70            "application/json; charset=utf-8",
71        )],
72        body,
73    )
74        .into_response()
75}
76
77/// 400 处理器 — 返回 `{code:0, msg:"Bad Request", data:{}}`
78pub async fn bad_request_handler() -> Response {
79    let resp = ApiResponse::error("Bad Request");
80    let body = resp.to_json_string();
81    (
82        StatusCode::BAD_REQUEST,
83        [(
84            axum::http::header::CONTENT_TYPE,
85            "application/json; charset=utf-8",
86        )],
87        body,
88    )
89        .into_response()
90}
91
92/// 401 处理器 — 返回 `{code:-1, msg:"Unauthorized", data:{}}`
93///
94/// 对齐 PHP `BaseException` 中的未登录 code=-1。
95pub async fn unauthorized_handler() -> Response {
96    let resp = ApiResponse::error_with_code(-1, "Unauthorized", json!({}));
97    let body = resp.to_json_string();
98    (
99        StatusCode::UNAUTHORIZED,
100        [(
101            axum::http::header::CONTENT_TYPE,
102            "application/json; charset=utf-8",
103        )],
104        body,
105    )
106        .into_response()
107}
108
109/// 403 处理器 — 返回 `{code:0, msg:"Forbidden", data:{}}`
110pub async fn forbidden_handler() -> Response {
111    let resp = ApiResponse::error("Forbidden");
112    let body = resp.to_json_string();
113    (
114        StatusCode::FORBIDDEN,
115        [(
116            axum::http::header::CONTENT_TYPE,
117            "application/json; charset=utf-8",
118        )],
119        body,
120    )
121        .into_response()
122}
123
124/// 422 处理器 — 返回 `{code:0, msg:"Unprocessable Entity", data:{}}`
125pub async fn unprocessable_entity_handler() -> Response {
126    let resp = ApiResponse::error("Unprocessable Entity");
127    let body = resp.to_json_string();
128    (
129        StatusCode::UNPROCESSABLE_ENTITY,
130        [(
131            axum::http::header::CONTENT_TYPE,
132            "application/json; charset=utf-8",
133        )],
134        body,
135    )
136        .into_response()
137}
138
139/// 构建指定状态码的错误响应
140pub fn error_response(status: StatusCode, msg: &str) -> Response {
141    let resp = ApiResponse::error(msg);
142    let body = resp.to_json_string();
143    (
144        status,
145        [(
146            axum::http::header::CONTENT_TYPE,
147            "application/json; charset=utf-8",
148        )],
149        body,
150    )
151        .into_response()
152}
153
154/// 构建带自定义业务 code 的错误响应
155pub fn error_response_with_code(status: StatusCode, code: i32, msg: &str) -> Response {
156    let resp = ApiResponse::error_with_code(code, msg, json!({}));
157    let body = resp.to_json_string();
158    (
159        status,
160        [(
161            axum::http::header::CONTENT_TYPE,
162            "application/json; charset=utf-8",
163        )],
164        body,
165    )
166        .into_response()
167}
168
169/// 自定义错误类型,可转换为标准 JSON 响应
170#[derive(Debug, Clone)]
171pub struct HandleError {
172    /// HTTP 状态码
173    pub status: StatusCode,
174    /// 业务错误码
175    pub code: i32,
176    /// 错误信息
177    pub msg: String,
178}
179
180impl HandleError {
181    /// 创建一个新的错误实例
182    pub fn new(status: StatusCode, code: i32, msg: impl Into<String>) -> Self {
183        Self {
184            status,
185            code,
186            msg: msg.into(),
187        }
188    }
189
190    /// 创建 404 未找到错误
191    pub fn not_found(msg: impl Into<String>) -> Self {
192        Self::new(StatusCode::NOT_FOUND, 0, msg)
193    }
194
195    /// 创建 500 服务器内部错误
196    pub fn internal(msg: impl Into<String>) -> Self {
197        Self::new(StatusCode::INTERNAL_SERVER_ERROR, 0, msg)
198    }
199
200    /// 创建 400 错误请求错误
201    pub fn bad_request(msg: impl Into<String>) -> Self {
202        Self::new(StatusCode::BAD_REQUEST, 0, msg)
203    }
204
205    /// 创建 401 未授权错误
206    pub fn unauthorized(msg: impl Into<String>) -> Self {
207        Self::new(StatusCode::UNAUTHORIZED, -1, msg)
208    }
209
210    /// 创建 403 禁止访问错误
211    pub fn forbidden(msg: impl Into<String>) -> Self {
212        Self::new(StatusCode::FORBIDDEN, 0, msg)
213    }
214}
215
216impl IntoResponse for HandleError {
217    fn into_response(self) -> Response {
218        error_response_with_code(self.status, self.code, &self.msg)
219    }
220}
221
222/// 创建预配置的错误处理 Router
223///
224/// - 设置 404 fallback
225/// - 其他错误码通过 `HandleError` 或单独的处理器处理
226pub fn error_router() -> Router {
227    Router::new().fallback(not_found_handler)
228}
229
230/// 创建 404 fallback Router(用于 `Router::merge`)
231pub fn fallback_router() -> Router {
232    Router::new().fallback(not_found_handler)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use axum::body::Body;
239    use axum::http::{Method, Request, StatusCode};
240    use http_body_util::BodyExt;
241    use serde_json::Value;
242    use tower::ServiceExt;
243
244    async fn fetch_json(resp: Response) -> (StatusCode, Value) {
245        let status = resp.status();
246        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
247        let json: Value = serde_json::from_slice(&bytes).unwrap();
248        (status, json)
249    }
250
251    async fn send_get(router: Router, uri: &str) -> Response {
252        let req = Request::builder()
253            .method(Method::GET)
254            .uri(uri)
255            .body(Body::empty())
256            .unwrap();
257        router.oneshot(req).await.unwrap()
258    }
259
260    // ====================================================================
261    // not_found_handler
262    // ====================================================================
263
264    #[tokio::test]
265    async fn test_not_found_handler_status_code() {
266        let resp = not_found_handler().await;
267        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
268    }
269
270    #[tokio::test]
271    async fn test_not_found_handler_body() {
272        let resp = not_found_handler().await;
273        let (status, json) = fetch_json(resp).await;
274        assert_eq!(status, StatusCode::NOT_FOUND);
275        assert_eq!(json["code"], 0);
276        assert_eq!(json["msg"], "Not Found");
277        assert!(json["data"].is_object());
278    }
279
280    #[tokio::test]
281    async fn test_not_found_handler_content_type() {
282        let resp = not_found_handler().await;
283        assert_eq!(
284            resp.headers().get("content-type").unwrap(),
285            "application/json; charset=utf-8"
286        );
287    }
288
289    // ====================================================================
290    // internal_error_handler
291    // ====================================================================
292
293    #[tokio::test]
294    async fn test_internal_error_handler() {
295        let resp = internal_error_handler().await;
296        let (status, json) = fetch_json(resp).await;
297        assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
298        assert_eq!(json["code"], 0);
299        assert_eq!(json["msg"], "Internal Error");
300    }
301
302    // ====================================================================
303    // method_not_allowed_handler
304    // ====================================================================
305
306    #[tokio::test]
307    async fn test_method_not_allowed_handler() {
308        let resp = method_not_allowed_handler().await;
309        let (status, json) = fetch_json(resp).await;
310        assert_eq!(status, StatusCode::METHOD_NOT_ALLOWED);
311        assert_eq!(json["code"], 0);
312        assert_eq!(json["msg"], "Method Not Allowed");
313    }
314
315    // ====================================================================
316    // bad_request_handler
317    // ====================================================================
318
319    #[tokio::test]
320    async fn test_bad_request_handler() {
321        let resp = bad_request_handler().await;
322        let (status, json) = fetch_json(resp).await;
323        assert_eq!(status, StatusCode::BAD_REQUEST);
324        assert_eq!(json["code"], 0);
325        assert_eq!(json["msg"], "Bad Request");
326    }
327
328    // ====================================================================
329    // unauthorized_handler
330    // ====================================================================
331
332    #[tokio::test]
333    async fn test_unauthorized_handler() {
334        let resp = unauthorized_handler().await;
335        let (status, json) = fetch_json(resp).await;
336        assert_eq!(status, StatusCode::UNAUTHORIZED);
337        // 对齐 PHP BaseException: code=-1
338        assert_eq!(json["code"], -1);
339        assert_eq!(json["msg"], "Unauthorized");
340    }
341
342    // ====================================================================
343    // forbidden_handler
344    // ====================================================================
345
346    #[tokio::test]
347    async fn test_forbidden_handler() {
348        let resp = forbidden_handler().await;
349        let (status, json) = fetch_json(resp).await;
350        assert_eq!(status, StatusCode::FORBIDDEN);
351        assert_eq!(json["code"], 0);
352        assert_eq!(json["msg"], "Forbidden");
353    }
354
355    // ====================================================================
356    // unprocessable_entity_handler
357    // ====================================================================
358
359    #[tokio::test]
360    async fn test_unprocessable_entity_handler() {
361        let resp = unprocessable_entity_handler().await;
362        let (status, json) = fetch_json(resp).await;
363        assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
364        assert_eq!(json["code"], 0);
365        assert_eq!(json["msg"], "Unprocessable Entity");
366    }
367
368    // ====================================================================
369    // error_response / error_response_with_code
370    // ====================================================================
371
372    #[tokio::test]
373    async fn test_error_response_custom() {
374        let resp = error_response(StatusCode::NOT_FOUND, "资源不存在");
375        let (status, json) = fetch_json(resp).await;
376        assert_eq!(status, StatusCode::NOT_FOUND);
377        assert_eq!(json["code"], 0);
378        assert_eq!(json["msg"], "资源不存在");
379    }
380
381    #[tokio::test]
382    async fn test_error_response_with_code_custom() {
383        let resp = error_response_with_code(StatusCode::BAD_REQUEST, 1001, "参数错误");
384        let (status, json) = fetch_json(resp).await;
385        assert_eq!(status, StatusCode::BAD_REQUEST);
386        assert_eq!(json["code"], 1001);
387        assert_eq!(json["msg"], "参数错误");
388    }
389
390    // ====================================================================
391    // HandleError 类型
392    // ====================================================================
393
394    #[tokio::test]
395    async fn test_handle_error_not_found() {
396        let err = HandleError::not_found("用户不存在");
397        let resp = err.into_response();
398        let (status, json) = fetch_json(resp).await;
399        assert_eq!(status, StatusCode::NOT_FOUND);
400        assert_eq!(json["code"], 0);
401        assert_eq!(json["msg"], "用户不存在");
402    }
403
404    #[tokio::test]
405    async fn test_handle_error_internal() {
406        let err = HandleError::internal("数据库错误");
407        let resp = err.into_response();
408        let (status, json) = fetch_json(resp).await;
409        assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
410        assert_eq!(json["code"], 0);
411        assert_eq!(json["msg"], "数据库错误");
412    }
413
414    #[tokio::test]
415    async fn test_handle_error_bad_request() {
416        let err = HandleError::bad_request("参数错误");
417        let resp = err.into_response();
418        let (status, json) = fetch_json(resp).await;
419        assert_eq!(status, StatusCode::BAD_REQUEST);
420        assert_eq!(json["code"], 0);
421        assert_eq!(json["msg"], "参数错误");
422    }
423
424    #[tokio::test]
425    async fn test_handle_error_unauthorized() {
426        let err = HandleError::unauthorized("请先登录");
427        let resp = err.into_response();
428        let (status, json) = fetch_json(resp).await;
429        assert_eq!(status, StatusCode::UNAUTHORIZED);
430        assert_eq!(json["code"], -1);
431        assert_eq!(json["msg"], "请先登录");
432    }
433
434    #[tokio::test]
435    async fn test_handle_error_forbidden() {
436        let err = HandleError::forbidden("无权限");
437        let resp = err.into_response();
438        let (status, json) = fetch_json(resp).await;
439        assert_eq!(status, StatusCode::FORBIDDEN);
440        assert_eq!(json["code"], 0);
441        assert_eq!(json["msg"], "无权限");
442    }
443
444    #[tokio::test]
445    async fn test_handle_error_custom() {
446        let err = HandleError::new(StatusCode::CONFLICT, 4091, "冲突");
447        let resp = err.into_response();
448        let (status, json) = fetch_json(resp).await;
449        assert_eq!(status, StatusCode::CONFLICT);
450        assert_eq!(json["code"], 4091);
451        assert_eq!(json["msg"], "冲突");
452    }
453
454    #[test]
455    fn test_handle_error_clone_debug() {
456        let err = HandleError::not_found("test");
457        let cloned = err.clone();
458        assert_eq!(cloned.msg, "test");
459        let debug = format!("{err:?}");
460        assert!(debug.contains("HandleError"));
461    }
462
463    // ====================================================================
464    // Router 集成
465    // ====================================================================
466
467    #[tokio::test]
468    async fn test_error_router_404_fallback() {
469        let router: Router = Router::new()
470            .route("/api", axum::routing::get(|| async { "ok" }))
471            .merge(error_router());
472
473        // 已注册路由
474        let resp = send_get(router.clone(), "/api").await;
475        assert_eq!(resp.status(), StatusCode::OK);
476
477        // 未注册路由 → 404
478        let resp = send_get(router, "/nonexistent").await;
479        let (status, json) = fetch_json(resp).await;
480        assert_eq!(status, StatusCode::NOT_FOUND);
481        assert_eq!(json["code"], 0);
482        assert_eq!(json["msg"], "Not Found");
483    }
484
485    #[tokio::test]
486    async fn test_fallback_router_404() {
487        let router: Router = Router::new()
488            .route("/api", axum::routing::get(|| async { "ok" }))
489            .merge(fallback_router());
490
491        let resp = send_get(router, "/nonexistent").await;
492        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
493    }
494
495    // ====================================================================
496    // 作为 handler 返回值
497    // ====================================================================
498
499    #[tokio::test]
500    async fn test_handle_error_as_handler_return() {
501        async fn handler() -> Result<String, HandleError> {
502            Err(HandleError::not_found("用户不存在"))
503        }
504
505        let router: Router = Router::new().route("/users/{id}", axum::routing::get(handler));
506
507        let req = Request::builder()
508            .method(Method::GET)
509            .uri("/users/999")
510            .body(Body::empty())
511            .unwrap();
512        let resp = router.oneshot(req).await.unwrap();
513        let (status, json) = fetch_json(resp).await;
514        assert_eq!(status, StatusCode::NOT_FOUND);
515        assert_eq!(json["msg"], "用户不存在");
516    }
517
518    #[tokio::test]
519    async fn test_handle_error_success_path() {
520        async fn handler() -> Result<String, HandleError> {
521            Ok("success".to_string())
522        }
523
524        let router: Router = Router::new().route("/ok", axum::routing::get(handler));
525
526        let resp = send_get(router, "/ok").await;
527        assert_eq!(resp.status(), StatusCode::OK);
528    }
529
530    // ====================================================================
531    // 集成测试:与 ApiResponse 协同
532    // ====================================================================
533
534    #[tokio::test]
535    async fn test_api_response_vs_handle_error_consistency() {
536        // ApiResponse 错误 → HTTP 200(业务错误)
537        let api_resp = ApiResponse::error("业务错误");
538        let api_response: Response = api_resp.into_response();
539        let (api_status, api_json) = fetch_json(api_response).await;
540        assert_eq!(api_status, StatusCode::OK);
541        assert_eq!(api_json["code"], 0);
542
543        // HandleError 错误 → HTTP 4xx/5xx
544        let handle_err = HandleError::bad_request("参数错误");
545        let err_response: Response = handle_err.into_response();
546        let (err_status, err_json) = fetch_json(err_response).await;
547        assert_eq!(err_status, StatusCode::BAD_REQUEST);
548        assert_eq!(err_json["code"], 0);
549
550        // 两者 code 都是 0(业务失败),但 HTTP 状态码不同
551        // - ApiResponse::error:HTTP 200 + code=0(PHP 风格)
552        // - HandleError::bad_request:HTTP 400 + code=0(REST 风格)
553    }
554
555    // ====================================================================
556    // 各种 HTTP 状态码覆盖
557    // ====================================================================
558
559    #[tokio::test]
560    async fn test_all_error_handlers_return_json() {
561        let handlers: Vec<(Response, StatusCode, &str)> = vec![
562            (
563                not_found_handler().await,
564                StatusCode::NOT_FOUND,
565                "Not Found",
566            ),
567            (
568                internal_error_handler().await,
569                StatusCode::INTERNAL_SERVER_ERROR,
570                "Internal Error",
571            ),
572            (
573                method_not_allowed_handler().await,
574                StatusCode::METHOD_NOT_ALLOWED,
575                "Method Not Allowed",
576            ),
577            (
578                bad_request_handler().await,
579                StatusCode::BAD_REQUEST,
580                "Bad Request",
581            ),
582            (
583                forbidden_handler().await,
584                StatusCode::FORBIDDEN,
585                "Forbidden",
586            ),
587            (
588                unprocessable_entity_handler().await,
589                StatusCode::UNPROCESSABLE_ENTITY,
590                "Unprocessable Entity",
591            ),
592        ];
593
594        for (resp, expected_status, expected_msg) in handlers {
595            let (status, json) = fetch_json(resp).await;
596            assert_eq!(status, expected_status);
597            assert_eq!(json["code"], 0);
598            assert_eq!(json["msg"], expected_msg);
599            assert!(json["data"].is_object());
600        }
601    }
602
603    #[tokio::test]
604    async fn test_unauthorized_returns_code_minus_one() {
605        // 对齐 PHP BaseException
606        let resp = unauthorized_handler().await;
607        let (_, json) = fetch_json(resp).await;
608        assert_eq!(json["code"], -1);
609    }
610}