Describe the bug
When the synchronous client is used with a forwarding HTTP proxy (http_proxy=... or the HTTP_PROXY environment variable) and no proxy_path is set, the request-target sent to the proxy has no path component:
GET http://host:8123?query=SELECT+1 HTTP/1.1
(note the missing / between the authority and the ?). Many forwarding proxies reject this as a malformed request-target and return HTTP 400 before the request ever reaches ClickHouse.
Root cause: with no proxy_path, the base URL is built as http://host:8123 (no path) in clickhouse_connect/driver/httpclient.py, and the sync backend then concatenates the query string directly onto it in clickhouse_connect/driver/_backend/http_sync.py:
url = f"{self.url}?{urlencode(final_params)}" # -> http://host:8123?query=...
- Without a proxy, urllib3 uses origin-form and normalizes the empty path to
/ (Url.request_uri), so GET /?query=... HTTP/1.1 is sent and it works — which is why this is invisible unless a proxy is involved.
- With a forwarding HTTP proxy, urllib3 forwards the absolute URL verbatim (absolute-form request-target), so the pathless URL reaches the proxy and is rejected.
The async client is not affected: its backend always appends / (clickhouse_connect/driver/_backend/http_async.py:477, url = f"{self.url}/") and passes params separately, so it sends a valid http://host:8123/?query=.... This sync/async inconsistency is the core of the bug.
Steps to reproduce
- Have a forwarding HTTP proxy in front of a plain-HTTP ClickHouse endpoint (e.g. a corporate forward proxy; verified against one that returns
Request Error (invalid_request) for pathless targets).
- Create a sync client with
http_proxy=... and no proxy_path.
- Run any query (e.g.
SELECT 1). It fails with HTTP 400 from the proxy. The same call via get_async_client(...) succeeds.
Expected behaviour
The sync client should send a valid request-target with a / path,http://host:8123/?query=..., matching (a) the async client and (b) urllib3's own normalization for direct connections. SELECT 1 should return [(1,)].
Code example
import clickhouse_connect
# Fails: HTTP 400 "invalid_request" from the proxy
c = clickhouse_connect.get_client(
host="clickhouse.example.com",
port=8123,
username="default",
password="<redacted>",
http_proxy="http://proxy.example.com:80",
)
print(c.query("SELECT 1").result_rows)
# Works unpatched (async backend appends "/"):
# import asyncio
# async def main():
# ac = await clickhouse_connect.get_async_client(
# host="clickhouse.example.com", port=8123,
# username="default", password="<redacted>",
# http_proxy="http://proxy.example.com:80",
# )
# print((await ac.query("SELECT 1")).result_rows) # [(1,)]
# asyncio.run(main())
Suggested fix in clickhouse_connect/driver/_backend/http_sync.py — add the / only when the URL has no path, so an explicit proxy_path is left untouched:
# before
url = f"{self.url}?{urlencode(final_params)}"
# after
authority_and_path = self.url.split("://", 1)[-1]
path_sep = "" if "/" in authority_and_path else "/"
url = f"{self.url}{path_sep}?{urlencode(final_params)}"
(The simpler f"{self.url}/?..." also works and matches the async backend, but it appends a trailing slash in the proxy_path case; the conditional form above is a strict no-op for every existing configuration.)
clickhouse-connect and/or ClickHouse server logs
Proxy response returned to the client (the request never reaches ClickHouse, so there are no server-side logs):
clickhouse_connect.driver.exceptions.DatabaseError: HTTP driver received HTTP status 400,
server response: <HTML><HEAD><TITLE>Request Error</TITLE></HEAD><BODY>
... <big>Request Error (invalid_request)</big> ...
Your request could not be processed. Request could not be handled ...
This could be caused by a misconfiguration, or possibly a malformed request. ...
(for url http://clickhouse.example.com:8123)
Configuration
Environment
- clickhouse-connect version: 1.6.0
- Python version: 3.12.11
- Operating system: macOS (arm64)
ClickHouse server
- ClickHouse Server version: N/A — request is rejected by the forwarding proxy before reaching ClickHouse; reproducible independent of server version.
Describe the bug
When the synchronous client is used with a forwarding HTTP proxy (
http_proxy=...or theHTTP_PROXYenvironment variable) and noproxy_pathis set, the request-target sent to the proxy has no path component:(note the missing
/between the authority and the?). Many forwarding proxies reject this as a malformed request-target and return HTTP 400 before the request ever reaches ClickHouse.Root cause: with no
proxy_path, the base URL is built ashttp://host:8123(no path) inclickhouse_connect/driver/httpclient.py, and the sync backend then concatenates the query string directly onto it inclickhouse_connect/driver/_backend/http_sync.py:/(Url.request_uri), soGET /?query=... HTTP/1.1is sent and it works — which is why this is invisible unless a proxy is involved.The async client is not affected: its backend always appends
/(clickhouse_connect/driver/_backend/http_async.py:477,url = f"{self.url}/") and passes params separately, so it sends a validhttp://host:8123/?query=.... This sync/async inconsistency is the core of the bug.Steps to reproduce
Request Error (invalid_request)for pathless targets).http_proxy=...and noproxy_path.SELECT 1). It fails with HTTP 400 from the proxy. The same call viaget_async_client(...)succeeds.Expected behaviour
The sync client should send a valid request-target with a
/path,http://host:8123/?query=..., matching (a) the async client and (b) urllib3's own normalization for direct connections.SELECT 1should return[(1,)].Code example
Suggested fix in
clickhouse_connect/driver/_backend/http_sync.py— add the/only when the URL has no path, so an explicitproxy_pathis left untouched:(The simpler
f"{self.url}/?..."also works and matches the async backend, but it appends a trailing slash in theproxy_pathcase; the conditional form above is a strict no-op for every existing configuration.)clickhouse-connect and/or ClickHouse server logs
Proxy response returned to the client (the request never reaches ClickHouse, so there are no server-side logs):
Configuration
Environment
ClickHouse server