Browse Source

feat(errors): add 409 Conflict and structured error details

fszontagh 1 tháng trước cách đây
mục cha
commit
5930dbc761
4 tập tin đã thay đổi với 37 bổ sung và 7 xóa
  1. 6 2
      src/errors.cpp
  2. 12 4
      src/errors.hpp
  3. 1 1
      src/server.cpp
  4. 18 0
      tests/test_errors.cpp

+ 6 - 2
src/errors.cpp

@@ -8,6 +8,7 @@ int httpStatus(ErrCode code) {
         case ErrCode::Unauthorized:  return 401;
         case ErrCode::Forbidden:     return 403;
         case ErrCode::NotFound:      return 404;
+        case ErrCode::Conflict:      return 409;
         case ErrCode::Unprocessable:    return 422;
         case ErrCode::TooManyRequests:  return 429;
         case ErrCode::Unavailable:      return 503;
@@ -16,8 +17,11 @@ int httpStatus(ErrCode code) {
     return 500;
 }
 
-nlohmann::json errorBody(const std::string& code, const std::string& message) {
-    return nlohmann::json{{"error", {{"code", code}, {"message", message}}}};
+nlohmann::json errorBody(const std::string& code, const std::string& message,
+                         const nlohmann::json& details) {
+    nlohmann::json err{{"code", code}, {"message", message}};
+    if (!details.is_null() && !details.empty()) err["details"] = details;
+    return nlohmann::json{{"error", err}};
 }
 
 } // namespace svapi

+ 12 - 4
src/errors.hpp

@@ -5,16 +5,24 @@
 
 namespace svapi {
 
-enum class ErrCode { BadRequest, Unauthorized, Forbidden, NotFound, Unprocessable, TooManyRequests, Unavailable, Internal };
+enum class ErrCode { BadRequest, Unauthorized, Forbidden, NotFound, Conflict,
+                     Unprocessable, TooManyRequests, Unavailable, Internal };
 
 int httpStatus(ErrCode code);
-nlohmann::json errorBody(const std::string& code, const std::string& message);
+
+/// `details` is merged into the error object as `error.details` when non-empty.
+/// Used for machine-readable payloads (colliding ids, relation impacts) that
+/// would otherwise be flattened into the message string.
+nlohmann::json errorBody(const std::string& code, const std::string& message,
+                         const nlohmann::json& details = nlohmann::json::object());
 
 struct ApiError : std::runtime_error {
     ErrCode code;
     std::string slug;
-    ApiError(ErrCode c, std::string slug_, const std::string& msg)
-        : std::runtime_error(msg), code(c), slug(std::move(slug_)) {}
+    nlohmann::json details;   // optional structured payload; empty object = omitted
+    ApiError(ErrCode c, std::string slug_, const std::string& msg,
+             nlohmann::json det = nlohmann::json::object())
+        : std::runtime_error(msg), code(c), slug(std::move(slug_)), details(std::move(det)) {}
 };
 
 } // namespace svapi

+ 1 - 1
src/server.cpp

@@ -73,7 +73,7 @@ void ApiServer::registerRoutes() {
         try { std::rethrow_exception(ep); }
         catch (const ApiError& e) {
             if (e.code == ErrCode::TooManyRequests) res.set_header("Retry-After", "60");
-            sendJson(res, httpStatus(e.code), errorBody(e.slug, e.what()));
+            sendJson(res, httpStatus(e.code), errorBody(e.slug, e.what(), e.details));
         }
         catch (const std::exception& e) { sendJson(res, 500, errorBody("internal", e.what())); }
     });

+ 18 - 0
tests/test_errors.cpp

@@ -30,3 +30,21 @@ TEST(Errors, ForbiddenIs403) { EXPECT_EQ(httpStatus(svapi::ErrCode::Forbidden),
 TEST(Errors, TooManyRequestsMapsTo429) {
     EXPECT_EQ(svapi::httpStatus(svapi::ErrCode::TooManyRequests), 429);
 }
+
+TEST(Errors, ConflictMapsTo409) {
+    EXPECT_EQ(svapi::httpStatus(svapi::ErrCode::Conflict), 409);
+}
+
+TEST(Errors, ErrorBodyCarriesDetails) {
+    nlohmann::json det = {{"duplicate_examples", nlohmann::json::array({"a", "b"})}};
+    auto body = svapi::errorBody("duplicate_values", "field holds duplicates", det);
+    EXPECT_EQ(body["error"]["code"], "duplicate_values");
+    EXPECT_EQ(body["error"]["message"], "field holds duplicates");
+    ASSERT_TRUE(body["error"].contains("details"));
+    EXPECT_EQ(body["error"]["details"]["duplicate_examples"].size(), 2u);
+}
+
+TEST(Errors, ErrorBodyOmitsEmptyDetails) {
+    auto body = svapi::errorBody("validation", "bad");
+    EXPECT_FALSE(body["error"].contains("details"));
+}