Description
When a query fails server-side, clickhouse-connect raises a DatabaseError (or OperationalError on a retried request) whose only carrier of structured data is the formatted message string. The actual ClickHouse server error code is available to the driver. it arrives in the X-ClickHouse-Exception-Code response header, and the symbolic name (e.g. UNKNOWN_TABLE) plus numeric code are present in the response body. But the driver folds all of that into a single human-readable string and discards the structured fields.
Callers that need to branch on the ClickHouse error code (e.g. 60 UNKNOWN_TABLE, 159 TIMEOUT_EXCEEDED, 241 MEMORY_LIMIT_EXCEEDED) are forced to regex-parse the message string. There is no code, name, or message attribute on the raised exception to branch on.
This is the Python equivalent of the clickhouse-go report in ClickHouse/clickhouse-go#1876, where the HTTP path surfaces server errors as ad-hoc strings rather than a typed exception. clickhouse-connect is HTTP-first, so this is its primary error path, not an edge protocol.
Where it lives
clickhouse_connect/driver/exceptions.py defines the exception hierarchy. None of Error, DatabaseError, or OperationalError carry a code / name field.
clickhouse_connect/driver/httpclient.py, HttpClient._error_handler reads the code from the header and then throws it away into a string:
def _error_handler(self, response: HTTPResponse, retried: bool = False) -> None:
...
if self.show_clickhouse_errors:
err_code = response.headers.get(ex_header) # X-ClickHouse-Exception-Code
if err_code:
err_str = f"Received ClickHouse exception, code: {err_code}"
...
if body:
err_str = f"{err_str}, server response: {body}"
...
raise OperationalError(err_str) if retried else DatabaseError(err_str) from None
The async client has the same pattern in clickhouse_connect/driver/asyncclient.py around line 1912.
ClickHouse server version
Tested against ClickHouse server 26.5.1.882 (reachable over HTTP at localhost:8123).
Reproduction
import clickhouse_connect
from clickhouse_connect.driver.exceptions import DatabaseError
client = clickhouse_connect.get_client(host="localhost", port=8123)
try:
client.query("SELECT * FROM non_existent_table")
except DatabaseError as exc:
# Expected: a structured field to branch on, e.g.
# assert exc.code == 60
# assert exc.name == "UNKNOWN_TABLE"
print("code attr:", getattr(exc, "code", None)) # -> None
print("name attr:", getattr(exc, "name", None)) # -> None
print("str:", str(exc))
# The only way to get the code today is to regex-parse this string:
# "Received ClickHouse exception, code: 60, server response:
# Code: 60. DB::Exception: Unknown table expression identifier
# 'non_existent_table' ... (UNKNOWN_TABLE) (version 26.5.1.882 ...)
# (for url http://localhost:8123)"
Expected vs actual:
- Expected: the raised exception exposes the numeric server code and ideally the symbolic name, e.g.
exc.code == 60 and exc.name == "UNKNOWN_TABLE", so callers can branch without string parsing.
- Actual:
getattr(exc, "code", None) is None. The code exists only as a substring of str(exc), and only when show_clickhouse_errors is enabled.
Suggested fix
This is a feature/enhancement rather than a regression, but the data is already in hand:
- Add a
code: int | None (and optionally name: str) attribute to DatabaseError / OperationalError in clickhouse_connect/driver/exceptions.py.
- In
HttpClient._error_handler (and the async equivalent in asyncclient.py), parse int(response.headers.get(ex_header)) and pass it into the exception, plus optionally extract the (NAME) token from the body, instead of only formatting it into err_str.
Note that the code header is populated independently of show_clickhouse_errors, so the structured field can be set even when the verbose message body is suppressed.
Link
Related upstream report: ClickHouse/clickhouse-go#1876
Description
When a query fails server-side,
clickhouse-connectraises aDatabaseError(orOperationalErroron a retried request) whose only carrier of structured data is the formatted message string. The actual ClickHouse server error code is available to the driver. it arrives in theX-ClickHouse-Exception-Coderesponse header, and the symbolic name (e.g.UNKNOWN_TABLE) plus numeric code are present in the response body. But the driver folds all of that into a single human-readable string and discards the structured fields.Callers that need to branch on the ClickHouse error code (e.g.
60 UNKNOWN_TABLE,159 TIMEOUT_EXCEEDED,241 MEMORY_LIMIT_EXCEEDED) are forced to regex-parse the message string. There is nocode,name, ormessageattribute on the raised exception to branch on.This is the Python equivalent of the clickhouse-go report in ClickHouse/clickhouse-go#1876, where the HTTP path surfaces server errors as ad-hoc strings rather than a typed exception. clickhouse-connect is HTTP-first, so this is its primary error path, not an edge protocol.
Where it lives
clickhouse_connect/driver/exceptions.pydefines the exception hierarchy. None ofError,DatabaseError, orOperationalErrorcarry acode/namefield.clickhouse_connect/driver/httpclient.py,HttpClient._error_handlerreads the code from the header and then throws it away into a string:The async client has the same pattern in
clickhouse_connect/driver/asyncclient.pyaround line 1912.ClickHouse server version
Tested against ClickHouse server
26.5.1.882(reachable over HTTP atlocalhost:8123).Reproduction
Expected vs actual:
exc.code == 60andexc.name == "UNKNOWN_TABLE", so callers can branch without string parsing.getattr(exc, "code", None)isNone. The code exists only as a substring ofstr(exc), and only whenshow_clickhouse_errorsis enabled.Suggested fix
This is a feature/enhancement rather than a regression, but the data is already in hand:
code: int | None(and optionallyname: str) attribute toDatabaseError/OperationalErrorinclickhouse_connect/driver/exceptions.py.HttpClient._error_handler(and the async equivalent inasyncclient.py), parseint(response.headers.get(ex_header))and pass it into the exception, plus optionally extract the(NAME)token from the body, instead of only formatting it intoerr_str.Note that the code header is populated independently of
show_clickhouse_errors, so the structured field can be set even when the verbose message body is suppressed.Link
Related upstream report: ClickHouse/clickhouse-go#1876