Skip to content

Sync client omits the / path in the request-target when using a forwarding HTTP proxy, causing HTTP 400 (async client is unaffected) #951

Description

@abhisheksurve45

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

  1. 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).
  2. Create a sync client with http_proxy=... and no proxy_path.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions