refactor(py): unify error responses on {"detail": ...} envelope
swadhinbiswas committed 3 months ago
c9d2ab9
1 parent 73a8f3a
commit c9d2ab9
2 files changed
+30 -110
Showing 2 changed files+30-110
▼crates/justapi-py/python/justapi/system.py
+4-4
| @@ -310,7 +310,7 @@ def _help_handler_factory(app): | ||
310 | 310 | if (name and d.get("name") == name) or (path and d.get("path") == path): |
311 | 311 | return JSONResponse(d) |
312 | 312 | return JSONResponse( |
313 | - {"error": f"route name={name!r} path={path!r} not found"}, | |
313 | + {"detail": f"route name={name!r} path={path!r} not found"}, | |
314 | 314 | status_code=404, |
315 | 315 | ) |
316 | 316 | return JSONResponse(build_help(app)) |
| @@ -322,7 +322,7 @@ def _help_handler_factory(app): | ||
322 | 322 | for d in collect_routes(app): |
323 | 323 | if d.get("name") == name or d.get("path") == name: |
324 | 324 | return JSONResponse(d) |
325 | - return JSONResponse({"error": f"route {name!r} not found"}, status_code=404) | |
325 | + return JSONResponse({"detail": f"route {name!r} not found"}, status_code=404) | |
326 | 326 | |
327 | 327 | async def tools_handler(request=None): |
328 | 328 | return JSONResponse({"tools": app.list_tools(), "count": len(app.list_tools())}) |
| @@ -332,11 +332,11 @@ def _help_handler_factory(app): | ||
332 | 332 | name = body.get("name") |
333 | 333 | arguments = body.get("arguments", {}) |
334 | 334 | if not name: |
335 | - return JSONResponse({"error": "missing 'name'"}, status_code=400) | |
335 | + return JSONResponse({"detail": "missing 'name'"}, status_code=400) | |
336 | 336 | try: |
337 | 337 | result = app.call_tool(name, arguments) |
338 | 338 | except (KeyError, Exception) as e: # noqa: B014 - call_tool raises KeyError for unknown |
339 | - return JSONResponse({"error": str(e)}, status_code=404) | |
339 | + return JSONResponse({"detail": str(e)}, status_code=404) | |
340 | 340 | if inspect.iscoroutine(result): |
341 | 341 | result = await result |
342 | 342 | return JSONResponse( |
343 | 343 | |
▼crates/justapi-py/src/native/handlers.rs
+26-106
| @@ -82,21 +82,12 @@ pub(crate) fn call_python_handler( | ||
82 | 82 | if let Ok(error_list) = errors.extract::<Vec<String>>() { |
83 | 83 | if !error_list.is_empty() { |
84 | 84 | let error_body = serde_json::json!({ |
85 | - "type": "https://justapi.dev/errors/validation", | |
86 | - "title": "Validation Error", | |
87 | - "status": 422, | |
88 | - "detail": error_list.join("; "), | |
89 | - "errors": error_list.iter().map(|e| { | |
90 | - serde_json::json!({"message": e}) | |
91 | - }).collect::<Vec<_>>(), | |
85 | + "detail": error_list.join("; ") | |
92 | 86 | }) |
93 | 87 | .to_string(); |
94 | 88 | return NativeResponse { |
95 | 89 | status: 422, |
96 | - headers: vec![( | |
97 | - b"content-type".to_vec(), | |
98 | - b"application/problem+json".to_vec(), | |
99 | - )], | |
90 | + headers: vec![(b"content-type".to_vec(), b"application/json".to_vec())], | |
100 | 91 | body: NativeBody::Bytes(error_body.into_bytes()), |
101 | 92 | }; |
102 | 93 | } |
| @@ -243,7 +234,7 @@ pub(crate) fn call_python_handler( | ||
243 | 234 | NativeResponse { |
244 | 235 | status: 500, |
245 | 236 | headers: vec![(b"content-type".to_vec(), b"application/json".to_vec())], |
246 | - body: NativeBody::Bytes(b"{\"error\":\"internal server error\"}".to_vec()), | |
237 | + body: NativeBody::Bytes(b"{\"detail\":\"Internal Server Error\"}".to_vec()), | |
247 | 238 | } |
248 | 239 | } |
249 | 240 | } |
| @@ -479,22 +470,10 @@ pub(crate) fn try_native_fast_path( | ||
479 | 470 | body: NativeBody::Bytes(body.to_vec()), |
480 | 471 | }), |
481 | 472 | Err(verr) => { |
482 | - let error_body = serde_json::json!({ | |
483 | - "type": "https://justapi.dev/errors/validation", | |
484 | - "title": "Validation Error", | |
485 | - "status": 422, | |
486 | - "detail": verr.to_string(), | |
487 | - "errors": verr.errors.iter().map(|e| { | |
488 | - serde_json::json!({ | |
489 | - "field": e.field, | |
490 | - "message": e.message, | |
491 | - }) | |
492 | - }).collect::<Vec<_>>(), | |
493 | - }) | |
494 | - .to_string(); | |
473 | + let error_body = serde_json::json!({ "detail": verr.to_string() }).to_string(); | |
495 | 474 | Some(NativeResponse { |
496 | 475 | status: 422, |
497 | - headers: vec![(b"content-type".to_vec(), b"application/problem+json".to_vec())], | |
476 | + headers: vec![(b"content-type".to_vec(), b"application/json".to_vec())], | |
498 | 477 | body: NativeBody::Bytes(error_body.into_bytes()), |
499 | 478 | }) |
500 | 479 | } |
| @@ -526,10 +505,7 @@ pub(crate) fn try_native_fast_path_query( | ||
526 | 505 | let pairs: Vec<(String, String)> = match serde_urlencoded::from_bytes(query) { |
527 | 506 | Ok(p) => p, |
528 | 507 | Err(_) => { |
529 | - return Some(validation_error_response( | |
530 | - "Query string is not valid form encoding", | |
531 | - Vec::new(), | |
532 | - )) | |
508 | + return Some(validation_error_response("Query string is not valid form encoding")) | |
533 | 509 | } |
534 | 510 | }; |
535 | 511 | let mut obj = serde_json::Map::new(); |
| @@ -548,29 +524,17 @@ pub(crate) fn try_native_fast_path_query( | ||
548 | 524 | headers: vec![(b"content-type".to_vec(), b"application/json".to_vec())], |
549 | 525 | body: NativeBody::Bytes(query_json), |
550 | 526 | }), |
551 | - Err(verr) => Some(validation_error_response( | |
552 | - &verr.to_string(), | |
553 | - verr.errors.iter().map(|e| (e.field.clone(), e.message.clone())).collect(), | |
554 | - )), | |
527 | + Err(verr) => Some(validation_error_response(&verr.to_string())), | |
555 | 528 | } |
556 | 529 | } |
557 | 530 | |
558 | -/// Build a 422 `application/problem+json` response for a validation failure, | |
559 | -/// mirroring the shape returned by the body fast path. | |
560 | -fn validation_error_response(detail: &str, errors: Vec<(String, String)>) -> NativeResponse { | |
561 | - let error_body = serde_json::json!({ | |
562 | - "type": "https://justapi.dev/errors/validation", | |
563 | - "title": "Validation Error", | |
564 | - "status": 422, | |
565 | - "detail": detail, | |
566 | - "errors": errors.into_iter().map(|(field, message)| { | |
567 | - serde_json::json!({ "field": field, "message": message }) | |
568 | - }).collect::<Vec<_>>(), | |
569 | - }) | |
570 | - .to_string(); | |
531 | +/// Build a 422 response for a validation failure using the canonical | |
532 | +/// `{"detail": ...}` envelope. | |
533 | +fn validation_error_response(detail: &str) -> NativeResponse { | |
534 | + let error_body = serde_json::json!({ "detail": detail }).to_string(); | |
571 | 535 | NativeResponse { |
572 | 536 | status: 422, |
573 | - headers: vec![(b"content-type".to_vec(), b"application/problem+json".to_vec())], | |
537 | + headers: vec![(b"content-type".to_vec(), b"application/json".to_vec())], | |
574 | 538 | body: NativeBody::Bytes(error_body.into_bytes()), |
575 | 539 | } |
576 | 540 | } |
| @@ -675,7 +639,7 @@ where | ||
675 | 639 | { |
676 | 640 | return Ok(json_response( |
677 | 641 | StatusCode::BAD_REQUEST, |
678 | - r#"{"error":"QUERY requires a Content-Type header"}"#, | |
642 | + r#"{"detail":"QUERY requires a Content-Type header"}"#, | |
679 | 643 | )); |
680 | 644 | } |
681 | 645 | |
| @@ -701,12 +665,12 @@ where | ||
701 | 665 | if msg.contains("exceeds maximum size") { |
702 | 666 | return Ok(json_response( |
703 | 667 | StatusCode::PAYLOAD_TOO_LARGE, |
704 | - r#"{"error":"payload too large"}"#, | |
668 | + r#"{"detail":"payload too large"}"#, | |
705 | 669 | )); |
706 | 670 | } |
707 | 671 | return Ok(json_response( |
708 | 672 | StatusCode::BAD_REQUEST, |
709 | - r#"{"error":"invalid multipart body"}"#, | |
673 | + r#"{"detail":"invalid multipart body"}"#, | |
710 | 674 | )); |
711 | 675 | } |
712 | 676 | } |
| @@ -724,11 +688,11 @@ where | ||
724 | 688 | Err(justapi_core::router::RouterError::MethodNotAllowed) => { |
725 | 689 | return Ok(json_response( |
726 | 690 | StatusCode::METHOD_NOT_ALLOWED, |
727 | - r#"{"error":"method not allowed"}"#, | |
691 | + r#"{"detail":"method not allowed"}"#, | |
728 | 692 | )) |
729 | 693 | } |
730 | 694 | Err(justapi_core::router::RouterError::NotFound) => { |
731 | - return Ok(json_response(StatusCode::NOT_FOUND, r#"{"error":"not found"}"#)) | |
695 | + return Ok(json_response(StatusCode::NOT_FOUND, r#"{"detail":"not found"}"#)) | |
732 | 696 | } |
733 | 697 | }; |
734 | 698 | |
| @@ -782,29 +746,7 @@ where | ||
782 | 746 | None => justapi_core::validate::validate_json_schema(&body_bytes, schema_json), |
783 | 747 | }; |
784 | 748 | if let Err(verr) = verr { |
785 | - let error_body = serde_json::json!({ | |
786 | - "type": "https://justapi.dev/errors/validation", | |
787 | - "title": "Validation Error", | |
788 | - "status": 422, | |
789 | - "detail": verr.to_string(), | |
790 | - "errors": verr.errors.iter().map(|e| { | |
791 | - serde_json::json!({ | |
792 | - "field": e.field, | |
793 | - "message": e.message, | |
794 | - }) | |
795 | - }).collect::<Vec<_>>(), | |
796 | - }) | |
797 | - .to_string(); | |
798 | - let mut resp = Response::new(ResponseBody::new( | |
799 | - http_body_util::Full::new(Bytes::from(error_body)) | |
800 | - .map_err(|e: std::convert::Infallible| -> anyhow::Error { match e {} }), | |
801 | - )); | |
802 | - *resp.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; | |
803 | - resp.headers_mut().insert( | |
804 | - http::header::CONTENT_TYPE, | |
805 | - http::HeaderValue::from_static("application/problem+json"), | |
806 | - ); | |
807 | - return Ok(resp); | |
749 | + return Ok(justapi_core::validation_response(&verr.to_string())); | |
808 | 750 | } |
809 | 751 | } |
810 | 752 | |
| @@ -818,7 +760,7 @@ where | ||
818 | 760 | tracing::error!("Failed to begin transaction: {}", e); |
819 | 761 | return Ok(json_response( |
820 | 762 | StatusCode::INTERNAL_SERVER_ERROR, |
821 | - r#"{"error":"database error"}"#, | |
763 | + r#"{"detail":"database error"}"#, | |
822 | 764 | )); |
823 | 765 | } |
824 | 766 | } |
| @@ -955,7 +897,7 @@ where | ||
955 | 897 | { |
956 | 898 | return Ok(json_response( |
957 | 899 | StatusCode::BAD_REQUEST, |
958 | - r#"{"error":"QUERY requires a Content-Type header"}"#, | |
900 | + r#"{"detail":"QUERY requires a Content-Type header"}"#, | |
959 | 901 | )); |
960 | 902 | } |
961 | 903 | |
| @@ -994,12 +936,12 @@ where | ||
994 | 936 | if msg.contains("exceeds maximum size") { |
995 | 937 | return Ok(json_response( |
996 | 938 | StatusCode::PAYLOAD_TOO_LARGE, |
997 | - r#"{"error":"payload too large"}"#, | |
939 | + r#"{"detail":"payload too large"}"#, | |
998 | 940 | )); |
999 | 941 | } |
1000 | 942 | return Ok(json_response( |
1001 | 943 | StatusCode::BAD_REQUEST, |
1002 | - r#"{"error":"invalid multipart body"}"#, | |
944 | + r#"{"detail":"invalid multipart body"}"#, | |
1003 | 945 | )); |
1004 | 946 | } |
1005 | 947 | } |
| @@ -1012,13 +954,13 @@ where | ||
1012 | 954 | Err(justapi_core::router::RouterError::MethodNotAllowed) => { |
1013 | 955 | return Ok(justapi_core::json_response( |
1014 | 956 | hyper::StatusCode::METHOD_NOT_ALLOWED, |
1015 | - r#"{"error":"method not allowed"}"#, | |
957 | + r#"{"detail":"method not allowed"}"#, | |
1016 | 958 | )) |
1017 | 959 | } |
1018 | 960 | Err(justapi_core::router::RouterError::NotFound) => { |
1019 | 961 | return Ok(justapi_core::json_response( |
1020 | 962 | hyper::StatusCode::NOT_FOUND, |
1021 | - r#"{"error":"not found"}"#, | |
963 | + r#"{"detail":"not found"}"#, | |
1022 | 964 | )) |
1023 | 965 | } |
1024 | 966 | }; |
| @@ -1056,29 +998,7 @@ where | ||
1056 | 998 | None => justapi_core::validate::validate_json_schema(&body_bytes, schema_json), |
1057 | 999 | }; |
1058 | 1000 | if let Err(verr) = verr { |
1059 | - let error_body = serde_json::json!({ | |
1060 | - "type": "https://justapi.dev/errors/validation", | |
1061 | - "title": "Validation Error", | |
1062 | - "status": 422, | |
1063 | - "detail": verr.to_string(), | |
1064 | - "errors": verr.errors.iter().map(|e| { | |
1065 | - serde_json::json!({ | |
1066 | - "field": e.field, | |
1067 | - "message": e.message, | |
1068 | - }) | |
1069 | - }).collect::<Vec<_>>(), | |
1070 | - }) | |
1071 | - .to_string(); | |
1072 | - let mut resp = hyper::Response::new(justapi_core::ResponseBody::new( | |
1073 | - http_body_util::Full::new(hyper::body::Bytes::from(error_body)) | |
1074 | - .map_err(|e: std::convert::Infallible| -> anyhow::Error { match e {} }), | |
1075 | - )); | |
1076 | - *resp.status_mut() = hyper::StatusCode::UNPROCESSABLE_ENTITY; | |
1077 | - resp.headers_mut().insert( | |
1078 | - http::header::CONTENT_TYPE, | |
1079 | - http::HeaderValue::from_static("application/problem+json"), | |
1080 | - ); | |
1081 | - return Ok(resp); | |
1001 | + return Ok(justapi_core::validation_response(&verr.to_string())); | |
1082 | 1002 | } |
1083 | 1003 | } |
1084 | 1004 | |
| @@ -1095,7 +1015,7 @@ where | ||
1095 | 1015 | tracing::error!("Failed to begin transaction: {}", e); |
1096 | 1016 | return Ok(justapi_core::json_response( |
1097 | 1017 | hyper::StatusCode::INTERNAL_SERVER_ERROR, |
1098 | - r#"{"error":"database error"}"#, | |
1018 | + r#"{"detail":"database error"}"#, | |
1099 | 1019 | )); |
1100 | 1020 | } |
1101 | 1021 | } |
1102 | 1022 | |