refactor(py): unify error responses on {"detail": ...} envelope

swadhinbiswas committed 3 months ago
c9d2ab9
1 parent 73a8f3a
commit c9d2ab9
2 files changed
+30 -110
▼crates/justapi-py/python/justapi/system.py
+4-4
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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