This is an automated email from the ASF dual-hosted git repository.

imbajin pushed a commit to branch goal-scan
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git


The following commit(s) were added to refs/heads/goal-scan by this push:
     new c8878bff fix(review): address PR review comments
c8878bff is described below

commit c8878bff56809ba10ae1515a7b7b9eb3ffa43532
Author: imbajin <[email protected]>
AuthorDate: Wed Jun 3 11:16:31 2026 +0800

    fix(review): address PR review comments
    
    - make RAG request client_config scoped and restored after use
    
    - avoid eager request redaction when debug logging is disabled
    
    - preserve explicit response validation errors and simplify HTTPError flow
    
    - clean admin error control flow and cross-platform path assertion
    
    - expose empty Gremlin payloads instead of silently returning None
---
 .workflow/code-scan/checkpoints/00-setup.md        |   2 +-
 hugegraph-llm/src/hugegraph_llm/api/admin_api.py   |   5 +-
 hugegraph-llm/src/hugegraph_llm/api/rag_api.py     | 224 +++++++++++----------
 hugegraph-llm/src/tests/api/test_admin_api.py      |   3 +-
 hugegraph-llm/src/tests/api/test_rag_api.py        |  21 +-
 .../src/pyhugegraph/api/gremlin.py                 |   3 +-
 .../src/pyhugegraph/utils/huge_requests.py         |  13 +-
 .../src/pyhugegraph/utils/util.py                  |  31 ++-
 .../src/tests/api/test_auth_routing.py             |  31 ++-
 .../src/tests/api/test_gremlin.py                  |   8 +
 .../src/tests/api/test_response_validation.py      |  40 +++-
 11 files changed, 254 insertions(+), 127 deletions(-)

diff --git a/.workflow/code-scan/checkpoints/00-setup.md 
b/.workflow/code-scan/checkpoints/00-setup.md
index 563eda77..520d2505 100644
--- a/.workflow/code-scan/checkpoints/00-setup.md
+++ b/.workflow/code-scan/checkpoints/00-setup.md
@@ -2,7 +2,7 @@
 
 ## Repository State
 
-- CWD: `/Users/imbajin/github/graph/ai`
+- CWD: repository root
 - Branch: `goal-test`
 - Start SHA: `d16db33c5509647afccae588c46c0b0767adbb76`
 - Existing unrelated untracked path observed before edits: 
`.workflow/pr68-review/`
diff --git a/hugegraph-llm/src/hugegraph_llm/api/admin_api.py 
b/hugegraph-llm/src/hugegraph_llm/api/admin_api.py
index 87abc063..294cb8e8 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/admin_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/admin_api.py
@@ -49,7 +49,6 @@ def _resolve_log_path(log_file: str | None) -> str:
     return os.path.join(LOG_DIR, log_file)
 
 
-# FIXME: line 31: E0702: Raising dict while only classes or instances are 
allowed (raising-bad-type)
 def admin_http_api(router: APIRouter, log_stream):
     @router.post("/logs", status_code=status.HTTP_200_OK)
     async def log_stream_api(req: LogStreamRequest):
@@ -59,9 +58,9 @@ def admin_http_api(router: APIRouter, log_stream):
                 detail="Admin token is not configured securely.",
             )
         if admin_settings.admin_token != req.admin_token:
-            raise generate_response(  # pylint: disable=raising-bad-type
+            return generate_response(
                 RAGResponse(
-                    status_code=status.HTTP_403_FORBIDDEN,  # pylint: 
disable=E0702
+                    status_code=status.HTTP_403_FORBIDDEN,
                     message="Invalid admin_token",
                 )
             )
diff --git a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py 
b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
index d57f2812..0e76ca50 100644
--- a/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
+++ b/hugegraph-llm/src/hugegraph_llm/api/rag_api.py
@@ -16,6 +16,8 @@
 # under the License.
 
 import json
+from contextlib import contextmanager
+from threading import RLock
 
 from fastapi import APIRouter, HTTPException, status
 
@@ -33,6 +35,15 @@ from hugegraph_llm.config import huge_settings, 
llm_settings, prompt
 from hugegraph_llm.utils.graph_index_utils import get_vertex_details
 from hugegraph_llm.utils.log import log
 
+_GRAPH_CONFIG_FIELD_MAP = {
+    "url": "graph_url",
+    "graph": "graph_name",
+    "user": "graph_user",
+    "pwd": "graph_pwd",
+    "gs": "graph_space",
+}
+_GRAPH_CONFIG_LOCK = RLock()
+
 
 # pylint: disable=too-many-statements
 def rag_http_api(
@@ -45,69 +56,27 @@ def rag_http_api(
     apply_reranker_conf,
     gremlin_generate_selective_func,
 ):
+    @contextmanager
+    def request_graph_config(req):
+        with _GRAPH_CONFIG_LOCK:
+            original_values = {
+                settings_field: getattr(huge_settings, settings_field)
+                for settings_field in _GRAPH_CONFIG_FIELD_MAP.values()
+            }
+            try:
+                client_config = getattr(req, "client_config", None)
+                if client_config is not None:
+                    for request_field, settings_field in 
_GRAPH_CONFIG_FIELD_MAP.items():
+                        if request_field in client_config.model_fields_set:
+                            setattr(huge_settings, settings_field, 
getattr(client_config, request_field))
+                yield
+            finally:
+                for settings_field, original_value in original_values.items():
+                    setattr(huge_settings, settings_field, original_value)
+
     @router.post("/rag", status_code=status.HTTP_200_OK)
     def rag_answer_api(req: RAGRequest):
-        set_graph_config(req)
-
-        # Basic parameter validation: empty query => 400
-        if not req.query or not str(req.query).strip():
-            raise HTTPException(
-                status_code=status.HTTP_400_BAD_REQUEST,
-                detail="Query must not be empty.",
-            )
-
-        result = rag_answer_func(
-            text=req.query,
-            raw_answer=req.raw_answer,
-            vector_only_answer=req.vector_only,
-            graph_only_answer=req.graph_only,
-            graph_vector_answer=req.graph_vector_answer,
-            graph_ratio=req.graph_ratio,
-            rerank_method=req.rerank_method,
-            near_neighbor_first=req.near_neighbor_first,
-            gremlin_tmpl_num=req.gremlin_tmpl_num,
-            max_graph_items=req.max_graph_items,
-            topk_return_results=req.topk_return_results,
-            vector_dis_threshold=req.vector_dis_threshold,
-            topk_per_keyword=req.topk_per_keyword,
-            # Keep prompt params in the end
-            custom_related_information=req.custom_priority_info,
-            answer_prompt=req.answer_prompt or prompt.answer_prompt,
-            keywords_extract_prompt=req.keywords_extract_prompt or 
prompt.keywords_extract_prompt,
-            gremlin_prompt=req.gremlin_prompt or 
prompt.gremlin_generate_prompt,
-        )
-        # TODO: we need more info in the response for users to understand the 
query logic
-        return {
-            "query": req.query,
-            **{
-                key: value
-                for key, value in zip(
-                    ["raw_answer", "vector_only", "graph_only", 
"graph_vector_answer"],
-                    result,
-                )
-                if getattr(req, key)
-            },
-        }
-
-    def set_graph_config(req):
-        if req.client_config is None:
-            return
-        field_map = {
-            "url": "graph_url",
-            "graph": "graph_name",
-            "user": "graph_user",
-            "pwd": "graph_pwd",
-            "gs": "graph_space",
-        }
-        for request_field, settings_field in field_map.items():
-            if request_field in req.client_config.model_fields_set:
-                setattr(huge_settings, settings_field, 
getattr(req.client_config, request_field))
-
-    @router.post("/rag/graph", status_code=status.HTTP_200_OK)
-    def graph_rag_recall_api(req: GraphRAGRequest):
-        try:
-            set_graph_config(req)
-
+        with request_graph_config(req):
             # Basic parameter validation: empty query => 400
             if not req.query or not str(req.query).strip():
                 raise HTTPException(
@@ -115,39 +84,85 @@ def rag_http_api(
                     detail="Query must not be empty.",
                 )
 
-            result = graph_rag_recall_func(
-                query=req.query,
+            result = rag_answer_func(
+                text=req.query,
+                raw_answer=req.raw_answer,
+                vector_only_answer=req.vector_only,
+                graph_only_answer=req.graph_only,
+                graph_vector_answer=req.graph_vector_answer,
+                graph_ratio=req.graph_ratio,
+                rerank_method=req.rerank_method,
+                near_neighbor_first=req.near_neighbor_first,
+                gremlin_tmpl_num=req.gremlin_tmpl_num,
                 max_graph_items=req.max_graph_items,
                 topk_return_results=req.topk_return_results,
                 vector_dis_threshold=req.vector_dis_threshold,
                 topk_per_keyword=req.topk_per_keyword,
-                gremlin_tmpl_num=req.gremlin_tmpl_num,
-                rerank_method=req.rerank_method,
-                near_neighbor_first=req.near_neighbor_first,
+                # Keep prompt params in the end
                 custom_related_information=req.custom_priority_info,
+                answer_prompt=req.answer_prompt or prompt.answer_prompt,
+                keywords_extract_prompt=req.keywords_extract_prompt or 
prompt.keywords_extract_prompt,
                 gremlin_prompt=req.gremlin_prompt or 
prompt.gremlin_generate_prompt,
-                get_vertex_only=req.get_vertex_only,
             )
+            # TODO: we need more info in the response for users to understand 
the query logic
+            return {
+                "query": req.query,
+                **{
+                    key: value
+                    for key, value in zip(
+                        ["raw_answer", "vector_only", "graph_only", 
"graph_vector_answer"],
+                        result,
+                    )
+                    if getattr(req, key)
+                },
+            }
 
-            if req.get_vertex_only:
-                vertex_details = get_vertex_details(result["match_vids"], 
result)
-                if vertex_details:
-                    result["match_vids"] = vertex_details
-
-            if isinstance(result, dict):
-                params = [
-                    "query",
-                    "keywords",
-                    "match_vids",
-                    "graph_result_flag",
-                    "gremlin",
-                    "graph_result",
-                    "vertex_degree_list",
-                ]
-                user_result = {key: result[key] for key in params if key in 
result}
-                return {"graph_recall": user_result}
-            return {"graph_recall": json.dumps(result)}
+    @router.post("/rag/graph", status_code=status.HTTP_200_OK)
+    def graph_rag_recall_api(req: GraphRAGRequest):
+        try:
+            with request_graph_config(req):
+                # Basic parameter validation: empty query => 400
+                if not req.query or not str(req.query).strip():
+                    raise HTTPException(
+                        status_code=status.HTTP_400_BAD_REQUEST,
+                        detail="Query must not be empty.",
+                    )
+
+                result = graph_rag_recall_func(
+                    query=req.query,
+                    max_graph_items=req.max_graph_items,
+                    topk_return_results=req.topk_return_results,
+                    vector_dis_threshold=req.vector_dis_threshold,
+                    topk_per_keyword=req.topk_per_keyword,
+                    gremlin_tmpl_num=req.gremlin_tmpl_num,
+                    rerank_method=req.rerank_method,
+                    near_neighbor_first=req.near_neighbor_first,
+                    custom_related_information=req.custom_priority_info,
+                    gremlin_prompt=req.gremlin_prompt or 
prompt.gremlin_generate_prompt,
+                    get_vertex_only=req.get_vertex_only,
+                )
 
+                if req.get_vertex_only:
+                    vertex_details = get_vertex_details(result["match_vids"], 
result)
+                    if vertex_details:
+                        result["match_vids"] = vertex_details
+
+                if isinstance(result, dict):
+                    params = [
+                        "query",
+                        "keywords",
+                        "match_vids",
+                        "graph_result_flag",
+                        "gremlin",
+                        "graph_result",
+                        "vertex_degree_list",
+                    ]
+                    user_result = {key: result[key] for key in params if key 
in result}
+                    return {"graph_recall": user_result}
+                return {"graph_recall": json.dumps(result)}
+
+        except HTTPException as e:
+            raise e
         except TypeError as e:
             log.error("TypeError in graph_rag_recall_api: %s", e)
             raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, 
detail=str(e)) from e
@@ -208,27 +223,26 @@ def rag_http_api(
     @router.post("/text2gremlin", status_code=status.HTTP_200_OK)
     def text2gremlin_api(req: GremlinGenerateRequest):
         try:
-            set_graph_config(req)
-
-            # Basic parameter validation: empty query => 400
-            if not req.query or not str(req.query).strip():
-                raise HTTPException(
-                    status_code=status.HTTP_400_BAD_REQUEST,
-                    detail="Query must not be empty.",
+            with request_graph_config(req):
+                # Basic parameter validation: empty query => 400
+                if not req.query or not str(req.query).strip():
+                    raise HTTPException(
+                        status_code=status.HTTP_400_BAD_REQUEST,
+                        detail="Query must not be empty.",
+                    )
+
+                output_types_str_list = None
+                if req.output_types:
+                    output_types_str_list = [ot.value for ot in 
req.output_types]
+
+                response_dict = gremlin_generate_selective_func(
+                    inp=req.query,
+                    example_num=req.example_num,
+                    schema_input=huge_settings.graph_name,
+                    gremlin_prompt_input=req.gremlin_prompt,
+                    requested_outputs=output_types_str_list,
                 )
-
-            output_types_str_list = None
-            if req.output_types:
-                output_types_str_list = [ot.value for ot in req.output_types]
-
-            response_dict = gremlin_generate_selective_func(
-                inp=req.query,
-                example_num=req.example_num,
-                schema_input=huge_settings.graph_name,
-                gremlin_prompt_input=req.gremlin_prompt,
-                requested_outputs=output_types_str_list,
-            )
-            return response_dict
+                return response_dict
         except HTTPException as e:
             raise e
         except Exception as e:
diff --git a/hugegraph-llm/src/tests/api/test_admin_api.py 
b/hugegraph-llm/src/tests/api/test_admin_api.py
index 4032c78c..ee3c0a5f 100644
--- a/hugegraph-llm/src/tests/api/test_admin_api.py
+++ b/hugegraph-llm/src/tests/api/test_admin_api.py
@@ -15,6 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import os
 from unittest.mock import Mock
 
 import pytest
@@ -108,4 +109,4 @@ def 
test_logs_streams_valid_log_file_under_logs_dir(monkeypatch):
     )
 
     assert response.status_code == status.HTTP_200_OK
-    log_stream.assert_called_once_with("logs/llm-server.log")
+    log_stream.assert_called_once_with(os.path.join("logs", "llm-server.log"))
diff --git a/hugegraph-llm/src/tests/api/test_rag_api.py 
b/hugegraph-llm/src/tests/api/test_rag_api.py
index e95bc5ec..7b05d70f 100644
--- a/hugegraph-llm/src/tests/api/test_rag_api.py
+++ b/hugegraph-llm/src/tests/api/test_rag_api.py
@@ -181,7 +181,17 @@ def 
test_rag_client_config_updates_only_explicit_graph_fields(monkeypatch):
     monkeypatch.setattr(huge_settings, "graph_user", "original_user")
     monkeypatch.setattr(huge_settings, "graph_pwd", "original_pwd")
     monkeypatch.setattr(huge_settings, "graph_space", "original_space")
-    client, callbacks = _make_test_client()
+    observed_settings = {}
+
+    def rag_answer_func(**_kwargs):
+        observed_settings["graph_url"] = huge_settings.graph_url
+        observed_settings["graph_name"] = huge_settings.graph_name
+        observed_settings["graph_user"] = huge_settings.graph_user
+        observed_settings["graph_pwd"] = huge_settings.graph_pwd
+        observed_settings["graph_space"] = huge_settings.graph_space
+        return ("raw", "vector", "graph", "graph_vector")
+
+    client, callbacks = 
_make_test_client(rag_answer_func=Mock(side_effect=rag_answer_func))
 
     response = client.post(
         "/rag",
@@ -195,7 +205,14 @@ def 
test_rag_client_config_updates_only_explicit_graph_fields(monkeypatch):
 
     assert response.status_code == status.HTTP_200_OK
     callbacks["rag_answer_func"].assert_called_once()
-    assert huge_settings.graph_url == "http://override:8080";
+    assert observed_settings == {
+        "graph_url": "http://override:8080";,
+        "graph_name": "original_graph",
+        "graph_user": "original_user",
+        "graph_pwd": "original_pwd",
+        "graph_space": "original_space",
+    }
+    assert huge_settings.graph_url == "http://original:8080";
     assert huge_settings.graph_name == "original_graph"
     assert huge_settings.graph_user == "original_user"
     assert huge_settings.graph_pwd == "original_pwd"
diff --git a/hugegraph-python-client/src/pyhugegraph/api/gremlin.py 
b/hugegraph-python-client/src/pyhugegraph/api/gremlin.py
index d738410c..9875b075 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/gremlin.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/gremlin.py
@@ -44,7 +44,8 @@ class GremlinManager(HugeParamsBase):
                 "g": f"__g_{self._sess.cfg.graph_name}",
             }
 
-        if response := self._invoke_request(data=gremlin_data.to_json()):
+        response = self._invoke_request(data=gremlin_data.to_json())
+        if response is not None:
             return ResponseData(response).result
         log.error("Gremlin can't get results: %s", str(response))
         return None
diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py 
b/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py
index 8fb933df..91d086b7 100644
--- a/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py
+++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_requests.py
@@ -16,6 +16,7 @@
 # under the License.
 
 
+import logging
 from typing import Any
 from urllib.parse import urljoin
 
@@ -149,7 +150,13 @@ class HGraphSession:
             timeout=self._timeout,
             **kwargs,
         )
-        log.debug(  # pylint: disable=logging-fstring-interpolation
-            f"Request: {method} {url} validator={validator} 
kwargs={redact_sensitive_data(kwargs)} {response}"
-        )
+        if log.isEnabledFor(logging.DEBUG):
+            log.debug(
+                "Request: %s %s validator=%s kwargs=%s %s",
+                method,
+                url,
+                validator,
+                redact_sensitive_data(kwargs),
+                response,
+            )
         return validator(response, method=method, path=path)
diff --git a/hugegraph-python-client/src/pyhugegraph/utils/util.py 
b/hugegraph-python-client/src/pyhugegraph/utils/util.py
index 9cab72ff..5a710ff0 100644
--- a/hugegraph-python-client/src/pyhugegraph/utils/util.py
+++ b/hugegraph-python-client/src/pyhugegraph/utils/util.py
@@ -41,6 +41,8 @@ SENSITIVE_KEY_PARTS = (
     "secret",
     "token",
 )
+ESCAPE_MARKERS = ("\\u", "\\U", "\\x", "\\n", "\\r", "\\t")
+RESPONSE_CONTENT_TYPES = {"raw", "json", "text"}
 
 
 def _is_sensitive_key(key) -> bool:
@@ -48,6 +50,19 @@ def _is_sensitive_key(key) -> bool:
     return any(part in key_lower for part in SENSITIVE_KEY_PARTS)
 
 
+def _may_contain_sensitive_key(value: str) -> bool:
+    value_lower = value.lower()
+    return any(part in value_lower for part in SENSITIVE_KEY_PARTS)
+
+
+def _decode_escaped_text(value):
+    if not isinstance(value, str) or "\\" not in value:
+        return value
+    if not any(marker in value for marker in ESCAPE_MARKERS):
+        return value
+    return value.encode("utf-8", errors="replace").decode("unicode_escape", 
errors="replace")
+
+
 def redact_sensitive_data(value):
     if isinstance(value, dict):
         return {
@@ -61,6 +76,8 @@ def redact_sensitive_data(value):
     if isinstance(value, bytes):
         value = value.decode("utf-8", errors="replace")
     if isinstance(value, str):
+        if not _may_contain_sensitive_key(value):
+            return value
         try:
             parsed = json.loads(value)
         except json.JSONDecodeError:
@@ -129,6 +146,9 @@ class ResponseValidation:
         :param path: URL path of the request
         :return: Parsed response content or empty dict if none applicable
         """
+        if self._content_type not in RESPONSE_CONTENT_TYPES:
+            raise ValueError(f"Unknown content type: {self._content_type}")
+
         result = {}
 
         try:
@@ -142,8 +162,6 @@ class ResponseValidation:
                     result = response.json()
                 elif self._content_type == "text":
                     result = response.text
-                else:
-                    raise ValueError(f"Unknown content type: 
{self._content_type}")
 
         except requests.exceptions.HTTPError as e:
             if not self._strict and response.status_code == 404:
@@ -170,21 +188,18 @@ class ResponseValidation:
                     details = response.text or "unknown error"
 
                 req_body = redact_sensitive_data(response.request.body) if 
response.request.body else "Empty body"
-                if isinstance(req_body, str):
-                    req_body = 
req_body.encode("utf-8").decode("unicode_escape")
+                req_body = _decode_escaped_text(req_body)
                 log.error(
                     "%s: %s\n[Body]: %s\n[Server Exception]: %s",
                     method,
-                    str(e).encode("utf-8").decode("unicode_escape"),
+                    _decode_escaped_text(str(e)),
                     req_body,
                     details,
                 )
 
                 if response.status_code == 404:
                     raise NotFoundError(response.content) from e
-                if response.status_code >= 400:
-                    raise ServerError(f"Server Exception: {details}") from e
-                raise e
+                raise ServerError(f"Server Exception: {details}") from e
 
         except Exception as e:
             log.error("Unhandled exception occurred: %s", 
traceback.format_exc())
diff --git a/hugegraph-python-client/src/tests/api/test_auth_routing.py 
b/hugegraph-python-client/src/tests/api/test_auth_routing.py
index ebaf6fba..eb2c13a0 100644
--- a/hugegraph-python-client/src/tests/api/test_auth_routing.py
+++ b/hugegraph-python-client/src/tests/api/test_auth_routing.py
@@ -133,7 +133,10 @@ def test_session_debug_log_redacts_sensitive_kwargs():
     raw_session.post.return_value = response
     session = HGraphSession(cfg, session=raw_session)
 
-    with mock.patch("pyhugegraph.utils.huge_requests.log.debug") as log_debug:
+    with (
+        mock.patch("pyhugegraph.utils.huge_requests.log.isEnabledFor", 
return_value=True),
+        mock.patch("pyhugegraph.utils.huge_requests.log.debug") as log_debug,
+    ):
         session.request(
             "/auth/users",
             method="POST",
@@ -143,3 +146,29 @@ def test_session_debug_log_redacts_sensitive_kwargs():
     logged_args = str(log_debug.call_args)
     assert "super-secret" not in logged_args
     assert "***REDACTED***" in logged_args
+
+
+def test_session_skips_debug_redaction_when_debug_disabled():
+    cfg = DummyCfg(url="http://127.0.0.1:8080";, graphspace=None, 
gs_supported=False, graph_name="g")
+    cfg.username = "admin"
+    cfg.password = "admin-password"
+    cfg.timeout = 30
+    response = mock.Mock(spec=requests.Response)
+    response.status_code = 200
+    response.json.return_value = {"ok": True}
+    response.raise_for_status.return_value = None
+    raw_session = mock.Mock()
+    raw_session.post.return_value = response
+    session = HGraphSession(cfg, session=raw_session)
+
+    with (
+        mock.patch("pyhugegraph.utils.huge_requests.log.isEnabledFor", 
return_value=False),
+        mock.patch("pyhugegraph.utils.huge_requests.redact_sensitive_data") as 
redact,
+    ):
+        session.request(
+            "/auth/users",
+            method="POST",
+            data='{"user_name":"marko","user_password":"super-secret"}',
+        )
+
+    redact.assert_not_called()
diff --git a/hugegraph-python-client/src/tests/api/test_gremlin.py 
b/hugegraph-python-client/src/tests/api/test_gremlin.py
index 46c297a8..a6934b52 100644
--- a/hugegraph-python-client/src/tests/api/test_gremlin.py
+++ b/hugegraph-python-client/src/tests/api/test_gremlin.py
@@ -167,3 +167,11 @@ def test_gremlin_exec_preserves_auth_exception_type():
 
     with pytest.raises(NotAuthorizedError, match="bad credentials"):
         gremlin.exec("g.V()")
+
+
+def test_gremlin_exec_does_not_silently_drop_empty_payload(monkeypatch):
+    gremlin = GremlinManager(_FailingGremlinSession())
+    monkeypatch.setattr(gremlin, "_invoke_request", mock.Mock(return_value={}))
+
+    with pytest.raises(KeyError):
+        gremlin.exec("g.V()")
diff --git a/hugegraph-python-client/src/tests/api/test_response_validation.py 
b/hugegraph-python-client/src/tests/api/test_response_validation.py
index f1269a75..9e23f63f 100644
--- a/hugegraph-python-client/src/tests/api/test_response_validation.py
+++ b/hugegraph-python-client/src/tests/api/test_response_validation.py
@@ -20,8 +20,8 @@ from unittest.mock import Mock
 
 import pytest
 import requests
-from pyhugegraph.utils.exceptions import NotAuthorizedError, 
ResponseParseError, ServerError
-from pyhugegraph.utils.util import ResponseValidation
+from pyhugegraph.utils.exceptions import NotAuthorizedError, NotFoundError, 
ResponseParseError, ServerError
+from pyhugegraph.utils.util import ResponseValidation, redact_sensitive_data
 
 pytestmark = pytest.mark.contract
 
@@ -104,6 +104,37 @@ class TestResponseValidation(unittest.TestCase):
         with pytest.raises(ResponseParseError, match="Failed to parse json 
response"):
             validator(response, "GET", "/graphs/hugegraph")
 
+    def test_unknown_content_type_raises_value_error_without_wrapping(self):
+        response = Mock(spec=requests.Response)
+        validator = ResponseValidation(content_type="xml")
+
+        with pytest.raises(ValueError, match="Unknown content type: xml"):
+            validator(response, "GET", "/graphs/hugegraph")
+
+    def test_strict_404_preserves_not_found_type(self):
+        response = self._mock_error_response(
+            {"message": "missing vertex"},
+            '{"message":"missing vertex"}',
+        )
+        response.status_code = 404
+        validator = ResponseValidation()
+
+        with pytest.raises(NotFoundError):
+            validator(response, "GET", "/graphs/hugegraph")
+
+    def test_error_log_preserves_plain_utf8_request_body(self):
+        response = self._mock_error_response(
+            {"message": "bad request"},
+            '{"message":"bad request"}',
+        )
+        response.request.body = "中文 payload"
+        validator = ResponseValidation()
+
+        with pytest.raises(ServerError), 
unittest.mock.patch("pyhugegraph.utils.util.log.error") as log_error:
+            validator(response, "POST", "/gremlin")
+
+        assert "中文 payload" in str(log_error.call_args)
+
     def test_error_log_redacts_sensitive_request_body(self):
         response = self._mock_error_response(
             {"message": "bad request"},
@@ -119,6 +150,11 @@ class TestResponseValidation(unittest.TestCase):
         assert "super-secret" not in logged_args
         assert "***REDACTED***" in logged_args
 
+    def 
test_redact_sensitive_data_returns_non_sensitive_string_unchanged(self):
+        plain_body = "large non-sensitive payload with unicode 中文"
+
+        assert redact_sensitive_data(plain_body) == plain_body
+
 
 if __name__ == "__main__":
     unittest.main()

Reply via email to