From ed21e39e8169541b6a3e892637591e978b4a250e Mon Sep 17 00:00:00 2001 From: Nguyen Quang Minh <135627235+n24q02m@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:33:16 +0700 Subject: [PATCH 1/4] feat(api): return structured error code and hint for Cypher query failures Signed-off-by: Nguyen Quang Minh <135627235+n24q02m@users.noreply.github.com> --- .../hugegraph/api/cypher/CypherClient.java | 3 +- .../api/cypher/CypherErrorMapper.java | 67 +++++++++++++++++++ .../hugegraph/api/cypher/CypherModel.java | 27 +++++++- .../apache/hugegraph/api/CypherApiTest.java | 18 +++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java index dc9cf16d00..b8f7d43b42 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherClient.java @@ -92,7 +92,8 @@ public CypherModel submitQuery(String cypherQuery, @Nullable Map } catch (Exception e) { LOG.error(String.format("Failed to submit cypher-query: [ %s ], caused by:", cypherQuery), e); - res = CypherModel.failOf(request.getRequestId().toString(), e.getMessage()); + res = CypherModel.failOf(request.getRequestId().toString(), + e.getMessage(), CypherErrorMapper.map(e)); } finally { try { if (client != null) { diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java new file mode 100644 index 0000000000..f72889718d --- /dev/null +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hugegraph.api.cypher; + +/** + * Classify cypher execution failures into stable error codes and attach + * actionable hints for the most common, cryptic translator messages. + */ +public final class CypherErrorMapper { + + public static final String SYNTAX_ERROR = "HugeGraph.Cypher.SyntaxError"; + public static final String UNSUPPORTED_FEATURE = + "HugeGraph.Cypher.UnsupportedFeature"; + public static final String MISSING_PARAMETER = + "HugeGraph.Cypher.MissingParameter"; + public static final String EXECUTION_ERROR = "HugeGraph.Cypher.ExecutionError"; + + private CypherErrorMapper() { + } + + public static CypherModel.CypherError map(Throwable e) { + String message = e.getMessage() != null ? e.getMessage() : e.toString(); + // strip noisy exception class prefixes, e.g. "...driver.exception.ResponseException: " + message = message.replaceFirst( + "^([a-zA-Z0-9_]+\\.)+[A-Za-z0-9_]+(Exception|Error):\\s*", ""); + String lower = message.toLowerCase(); + + if (lower.contains("not defined") && lower.contains("$")) { + return new CypherModel.CypherError(MISSING_PARAMETER, message, + "Provide the value via the 'parameters' query argument, " + + "e.g. ?parameters=%7B%22city%22%3A%20%22Beijing%22%7D"); + } + if (lower.contains("undefined vertex label") + || lower.contains("undefined edge label") + || lower.contains("undefined property key")) { + return new CypherModel.CypherError(EXECUTION_ERROR, message, + "Create the schema element via the schema API before " + + "running the query"); + } + if (lower.contains("not defined") || lower.contains("undefined")) { + return new CypherModel.CypherError(SYNTAX_ERROR, message, + "Declare the variable in a MATCH, UNWIND or WITH clause " + + "before referencing it"); + } + if (lower.contains("not supported") || lower.contains("unsupported")) { + return new CypherModel.CypherError(UNSUPPORTED_FEATURE, message, + "This construct is not covered by the built-in translator, " + + "see the Cypher compatibility guide for supported syntax"); + } + return new CypherModel.CypherError(EXECUTION_ERROR, message, ""); + } +} diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java index e7c3900605..25f8f7a251 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java @@ -37,6 +37,9 @@ public class CypherModel { @Schema(description = "The query result") public Result result = new Result(); + @Schema(description = "Structured errors, empty on success") + public List errors = Collections.emptyList(); + public static CypherModel dataOf(String requestId, List data) { CypherModel res = new CypherModel(); res.requestId = requestId; @@ -45,11 +48,15 @@ public static CypherModel dataOf(String requestId, List data) { return res; } - public static CypherModel failOf(String requestId, String message) { + public static CypherModel failOf(String requestId, String message, + CypherError error) { CypherModel res = new CypherModel(); res.requestId = requestId; res.status.code = 400; res.status.message = message; + if (error != null) { + res.errors = Collections.singletonList(error); + } return res; } @@ -77,4 +84,22 @@ private static class Result { public Map meta = Collections.EMPTY_MAP; } + public static class CypherError { + + @Schema(description = "Stable error classification code") + public String code; + + @Schema(description = "The error message") + public String message; + + @Schema(description = "Actionable hint, empty when unavailable") + public String hint = ""; + + public CypherError(String code, String message, String hint) { + this.code = code; + this.message = message; + this.hint = hint; + } + } + } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java index 28d253a5cf..415f989218 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java @@ -135,6 +135,24 @@ public void testRelationQuery() { assertColumn(query(cypher), "friend", "peter"); } + @Test + public void testSyntaxErrorHasStructuredErrorWithHint() { + Response r = client().post(PATH, + "MATCH (n:person) RETURN not_defined_var"); + String content = assertResponseStatus(200, r); + assertContains("\"errors\"", content); + assertContains("SyntaxError", content); + assertContains("Declare the variable", content); + } + + @Test + public void testUndefinedLabelHasSchemaHint() { + Response r = client().post(PATH, "MATCH (n:robot) RETURN n"); + String content = assertResponseStatus(200, r); + assertContains("ExecutionError", content); + assertContains("Create the schema element", content); + } + @Test public void testReturnNodeIdAsPrimitiveValue() { String cypher = "MATCH (n:cypher_person) WHERE n.name = 'marko' " + From aabdde7778e65ddcbfa326e5e36c942c735258ea Mon Sep 17 00:00:00 2001 From: Nguyen Quang Minh <135627235+n24q02m@users.noreply.github.com> Date: Tue, 6 Oct 2026 08:34:01 +0700 Subject: [PATCH 2/4] fix(api): keep success cypher responses legacy-shaped and classify parse failures - CypherModel.errors: @JsonInclude(NON_EMPTY) so successful responses keep the exact pre-PR shape (older hugegraph-client RestResult fails on unknown top-level fields) - CypherErrorMapper: detect translator/parser Syntax/Parse exceptions by class name and full message (incl. ANTLR markers) BEFORE the class-prefix strip, so real parse failures map to SyntaxError even when wrapped or carrying generic messages - CypherApiTest: regression test parses a success response with a strict FAIL_ON_UNKNOWN_PROPERTIES mapper reproducing the client 1.8.0 failure mode --- .../api/cypher/CypherErrorMapper.java | 25 ++++++++++++++++--- .../hugegraph/api/cypher/CypherModel.java | 6 ++++- .../apache/hugegraph/api/CypherApiTest.java | 19 ++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java index f72889718d..8728189a31 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java @@ -34,12 +34,29 @@ private CypherErrorMapper() { } public static CypherModel.CypherError map(Throwable e) { - String message = e.getMessage() != null ? e.getMessage() : e.toString(); - // strip noisy exception class prefixes, e.g. "...driver.exception.ResponseException: " - message = message.replaceFirst( + String raw = e.getMessage() != null ? e.getMessage() : e.toString(); + // translator/parser exception types indicate a real parse failure even + // when the message is generic; check class name and FULL message + // before the class-prefix strip below removes the evidence + String className = e.getClass().getName(); + String lowerFull = raw.toLowerCase(); + if (className.contains("SyntaxException") + || className.contains("ParseException") + || lowerFull.contains("syntaxexception") + || lowerFull.contains("parseexception") + || lowerFull.contains("no viable alternative") + || lowerFull.contains("mismatched input") + || lowerFull.contains("extraneous input") + || lowerFull.contains("token recognition error")) { + return new CypherModel.CypherError(SYNTAX_ERROR, + raw.replaceFirst( + "^([a-zA-Z0-9_]+\\.)+[A-Za-z0-9_]+(Exception|Error):\\s*", ""), + "Check the Cypher statement near the position reported " + + "in the message"); + } + String message = raw.replaceFirst( "^([a-zA-Z0-9_]+\\.)+[A-Za-z0-9_]+(Exception|Error):\\s*", ""); String lower = message.toLowerCase(); - if (lower.contains("not defined") && lower.contains("$")) { return new CypherModel.CypherError(MISSING_PARAMETER, message, "Provide the value via the 'parameters' query argument, " + diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java index 25f8f7a251..6a51a90174 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherModel.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonInclude; + import io.swagger.v3.oas.annotations.media.Schema; /** @@ -37,7 +39,9 @@ public class CypherModel { @Schema(description = "The query result") public Result result = new Result(); - @Schema(description = "Structured errors, empty on success") + @Schema(description = "Structured errors, present only on failure so that " + + "older clients keep parsing successful responses") + @JsonInclude(JsonInclude.Include.NON_EMPTY) public List errors = Collections.emptyList(); public static CypherModel dataOf(String requestId, List data) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java index 415f989218..74bbc068ef 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java @@ -34,6 +34,8 @@ import org.junit.Before; import org.junit.Test; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -153,6 +155,23 @@ public void testUndefinedLabelHasSchemaHint() { assertContains("Create the schema element", content); } + @Test + public void testSuccessResponseKeepsLegacyShape() throws Exception { + Response r = client().post(PATH, QUERY); + String content = assertResponseStatus(200, r); + // hugegraph-client RestResult.readObject fails on unknown fields + // (FAIL_ON_UNKNOWN_PROPERTIES defaults to true), so a successful + // response must not carry the new "errors" key + org.junit.Assert.assertFalse("success response must not contain errors", + content.contains("\"errors\"")); + ObjectMapper strict = new ObjectMapper() + .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + @SuppressWarnings("unchecked") + Map legacy = strict.readValue(content, Map.class); + org.junit.Assert.assertTrue(legacy.containsKey("requestId")); + org.junit.Assert.assertTrue(legacy.containsKey("result")); + } + @Test public void testReturnNodeIdAsPrimitiveValue() { String cypher = "MATCH (n:cypher_person) WHERE n.name = 'marko' " + From dc076e0c8875ff3a6699be021dc775f665f5d571 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Minh <135627235+n24q02m@users.noreply.github.com> Date: Wed, 14 Oct 2026 09:56:47 +0700 Subject: [PATCH 3/4] fix(api): drop misleading MissingParameter hint and test real Cypher parse errors The Cypher endpoint has no parameter plumbing, so the ?parameters= hint sent users to an argument that does nothing. Remove the MISSING_PARAMETER code and fall through to the generic classification. Add a test with a genuine parse error (unclosed node pattern) asserting the HugeGraph.Cypher.SyntaxError code. --- .../apache/hugegraph/api/cypher/CypherErrorMapper.java | 7 ------- .../main/java/org/apache/hugegraph/api/CypherApiTest.java | 8 ++++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java index 8728189a31..5dbb2c7ae3 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java @@ -26,8 +26,6 @@ public final class CypherErrorMapper { public static final String SYNTAX_ERROR = "HugeGraph.Cypher.SyntaxError"; public static final String UNSUPPORTED_FEATURE = "HugeGraph.Cypher.UnsupportedFeature"; - public static final String MISSING_PARAMETER = - "HugeGraph.Cypher.MissingParameter"; public static final String EXECUTION_ERROR = "HugeGraph.Cypher.ExecutionError"; private CypherErrorMapper() { @@ -57,11 +55,6 @@ public static CypherModel.CypherError map(Throwable e) { String message = raw.replaceFirst( "^([a-zA-Z0-9_]+\\.)+[A-Za-z0-9_]+(Exception|Error):\\s*", ""); String lower = message.toLowerCase(); - if (lower.contains("not defined") && lower.contains("$")) { - return new CypherModel.CypherError(MISSING_PARAMETER, message, - "Provide the value via the 'parameters' query argument, " + - "e.g. ?parameters=%7B%22city%22%3A%20%22Beijing%22%7D"); - } if (lower.contains("undefined vertex label") || lower.contains("undefined edge label") || lower.contains("undefined property key")) { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java index 74bbc068ef..cd2873d32f 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/api/CypherApiTest.java @@ -147,6 +147,14 @@ public void testSyntaxErrorHasStructuredErrorWithHint() { assertContains("Declare the variable", content); } + @Test + public void testParseErrorIsClassifiedAsSyntaxError() { + Response r = client().post(PATH, "MATCH (n:person RETURN n"); + String content = assertResponseStatus(200, r); + assertContains("\"code\":\"HugeGraph.Cypher.SyntaxError\"", content); + assertContains("Check the Cypher statement", content); + } + @Test public void testUndefinedLabelHasSchemaHint() { Response r = client().post(PATH, "MATCH (n:robot) RETURN n"); From 16cd32e81331a4f57348b063ce2d2cac73984476 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Minh <135627235+n24q02m@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:05:49 +0700 Subject: [PATCH 4/4] fix(api): classify openCypher 'Invalid input' parse failures as SyntaxError The openCypher front-end reports a malformed statement as "Invalid input 'R': expected ...", which none of the existing parse-failure markers matched, so the response was labelled ExecutionError with no hint. --- .../java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java index 5dbb2c7ae3..5d1ee2478d 100644 --- a/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java +++ b/hugegraph-server/hugegraph-api/src/main/java/org/apache/hugegraph/api/cypher/CypherErrorMapper.java @@ -43,6 +43,8 @@ public static CypherModel.CypherError map(Throwable e) { || lowerFull.contains("syntaxexception") || lowerFull.contains("parseexception") || lowerFull.contains("no viable alternative") + // openCypher front-end: "Invalid input 'R': expected ..." + || lowerFull.contains("invalid input '") || lowerFull.contains("mismatched input") || lowerFull.contains("extraneous input") || lowerFull.contains("token recognition error")) {