From a0d19ba26fd98a6277e6789f9dd8e0a0e1413f0c Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Fri, 3 Jul 2026 23:17:33 +0200 Subject: [PATCH] fix(sl): classify semantic-query request rejections as expected, not faults (#339) The daemon rejects an invalid semantic-query request (unknown source, ambiguous measure, no join path) with a plain ValueError; the Node compute port now maps the daemon's exit code 3 / HTTP 400 to KtxExpectedError so these routine, caller-driven rejections stay out of Error Tracking. A dedicated SemanticLayerRequestError(ValueError) is raised only for engine rejections and routed through both daemon transports and the HTTP handler. Because pydantic v2 ValidationError subclasses ValueError, malformed sources or responses (contract faults) are kept as faults: they are reported and mapped to exit 1 / HTTP 500 / plain Error on every path. Non-object stdin is likewise a fault (exit 1), not exit 3. --- python/ktx-daemon/src/ktx_daemon/app.py | 9 +++++++++ python/ktx-daemon/tests/test_app.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/python/ktx-daemon/src/ktx_daemon/app.py b/python/ktx-daemon/src/ktx_daemon/app.py index 63487439..6c790931 100644 --- a/python/ktx-daemon/src/ktx_daemon/app.py +++ b/python/ktx-daemon/src/ktx_daemon/app.py @@ -12,6 +12,7 @@ from typing import Any from fastapi import FastAPI, HTTPException, Request from fastapi.responses import JSONResponse, Response +from pydantic import ValidationError from ktx_daemon import VERSION from ktx_daemon.code_execution import ( @@ -268,6 +269,14 @@ def create_app( ) -> SemanticLayerQueryResponse: try: return query_semantic_layer(request) + except ValidationError as error: + # A malformed source or response is a ktx contract fault, not a + # caller rejection; surface it as a server fault (500) so the Node + # client does not classify it as an expected query rejection, matching + # the subprocess transport (exit 1) and query_semantic_layer's own + # report. ValidationError subclasses ValueError, so catch it first. + logger.exception("Semantic query failed on a malformed source") + raise HTTPException(status_code=500, detail=str(error)) from error except ValueError as error: logger.warning("Semantic query rejected: %s", error) raise HTTPException(status_code=400, detail=str(error)) from error diff --git a/python/ktx-daemon/tests/test_app.py b/python/ktx-daemon/tests/test_app.py index fffc2899..e6ec9ed6 100644 --- a/python/ktx-daemon/tests/test_app.py +++ b/python/ktx-daemon/tests/test_app.py @@ -424,6 +424,24 @@ def test_semantic_query_endpoint_maps_value_error_to_400() -> None: assert "missing.order_count" in response.json()["detail"] +def test_semantic_query_endpoint_maps_malformed_source_to_fault() -> None: + client = TestClient(create_app(), raise_server_exceptions=False) + invalid_source = { + key: value for key, value in ORDERS_SOURCE.items() if key != "table" + } + + response = client.post( + "/semantic-layer/query", + json={ + "sources": [invalid_source], + "dialect": "postgres", + "query": {"measures": ["orders.order_count"], "dimensions": []}, + }, + ) + + assert response.status_code == 500 + + def test_semantic_validate_endpoint_returns_structured_validation() -> None: client = TestClient(create_app()) invalid_source = {