Skip to content

Commit dd791e2

Browse files
authored
feat(clickhouse-driver): Support passing custom HTTP headers (cube-js#11161)
Allow forwarding arbitrary HTTP headers on every request to ClickHouse, configured via the `headers` option in `driver_factory` (mirroring the Trino/Presto driver).
1 parent c7d3ad1 commit dd791e2

6 files changed

Lines changed: 177 additions & 4 deletions

File tree

docs-mintlify/admin/connect-to-data/data-sources/clickhouse.mdx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,80 @@ To enable SSL-encrypted connections between Cube and ClickHouse, set the
122122
configure custom certificates, please check out [Enable SSL Connections to the
123123
Database][ref-recipe-enable-ssl].
124124

125+
## Custom headers
126+
127+
The ClickHouse driver supports forwarding custom HTTP headers on every request to
128+
the ClickHouse server. This is useful when requests pass through a proxy or gateway
129+
that expects additional headers (e.g., for routing or tracing). See the [ClickHouse
130+
JavaScript client configuration][clickhouse-docs-js-config] for more details.
131+
132+
Custom headers can't be configured via environment variables. Instead, use the
133+
[`driver_factory`](/reference/configuration/config#driver_factory) configuration
134+
option to pass a `headers` object to the driver:
135+
136+
<CodeGroup>
137+
138+
```python title="Python"
139+
from cube import config
140+
141+
@config('driver_factory')
142+
def driver_factory(ctx: dict) -> dict:
143+
return {
144+
'type': 'clickhouse',
145+
'headers': {
146+
'X-Custom-Header': 'value',
147+
'X-Routing-Group': 'analytics'
148+
}
149+
}
150+
```
151+
152+
```javascript title="JavaScript"
153+
module.exports = {
154+
driverFactory: ({ dataSource }) => ({
155+
type: "clickhouse",
156+
headers: {
157+
"X-Custom-Header": "value",
158+
"X-Routing-Group": "analytics"
159+
}
160+
})
161+
};
162+
```
163+
164+
</CodeGroup>
165+
166+
In multitenant deployments, you can use the [security
167+
context](/embedding/authentication/security-context) to pass per-tenant headers, for example
168+
to forward a user token from the API request down to ClickHouse:
169+
170+
<CodeGroup>
171+
172+
```python title="Python"
173+
from cube import config
174+
175+
@config('driver_factory')
176+
def driver_factory(ctx: dict) -> dict:
177+
security_context = ctx['securityContext']
178+
return {
179+
'type': 'clickhouse',
180+
'headers': {
181+
'X-Custom-User-Token': security_context['token']
182+
}
183+
}
184+
```
185+
186+
```javascript title="JavaScript"
187+
module.exports = {
188+
driverFactory: ({ securityContext }) => ({
189+
type: "clickhouse",
190+
headers: {
191+
"X-Custom-User-Token": securityContext.token
192+
}
193+
})
194+
};
195+
```
196+
197+
</CodeGroup>
198+
125199
## Additional Configuration
126200

127201
You can connect to a ClickHouse database when your user's permissions are
@@ -134,6 +208,7 @@ You can connect to a ClickHouse database with compression enabled, by setting
134208
[clickhouse]: https://clickhouse.tech/
135209
[clickhouse-docs-users]:
136210
https://clickhouse.tech/docs/en/operations/settings/settings-users/
211+
[clickhouse-docs-js-config]: https://clickhouse.com/docs/integrations/javascript#configuration
137212
[clickhouse-readonly]: https://clickhouse.com/docs/en/operations/settings/permissions-for-queries#readonly
138213
[ref-caching-using-preaggs-build-strats]: /docs/pre-aggregations/using-pre-aggregations#pre-aggregation-build-strategies
139214
[ref-recipe-enable-ssl]: /recipes/configuration/using-ssl-connections-to-data-source

docs/content/product/configuration/data-sources/clickhouse.mdx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,44 @@ To enable SSL-encrypted connections between Cube and ClickHouse, set the
120120
configure custom certificates, please check out [Enable SSL Connections to the
121121
Database][ref-recipe-enable-ssl].
122122

123+
## Custom headers
124+
125+
The ClickHouse driver supports forwarding custom HTTP headers on every request to
126+
the ClickHouse server. This is useful when requests pass through a proxy or gateway
127+
that expects additional headers (e.g., for routing or tracing). See the [ClickHouse
128+
JavaScript client configuration][clickhouse-docs-js-config] for more details.
129+
130+
Custom headers can't be configured via environment variables. Instead, use the
131+
[`driverFactory`][ref-config-driverfactory] configuration option to pass a `headers`
132+
object to the driver:
133+
134+
```javascript
135+
module.exports = {
136+
driverFactory: ({ dataSource }) => ({
137+
type: "clickhouse",
138+
headers: {
139+
"X-Custom-Header": "value",
140+
"X-Routing-Group": "analytics"
141+
}
142+
})
143+
};
144+
```
145+
146+
In multitenant deployments, you can use the [security context][ref-sec-ctx] to pass
147+
per-tenant headers, for example to forward a user token from the API request down to
148+
ClickHouse:
149+
150+
```javascript
151+
module.exports = {
152+
driverFactory: ({ securityContext }) => ({
153+
type: "clickhouse",
154+
headers: {
155+
"X-Custom-User-Token": securityContext.token
156+
}
157+
})
158+
};
159+
```
160+
123161
## Additional Configuration
124162

125163
You can connect to a ClickHouse database when your user's permissions are
@@ -132,7 +170,10 @@ You can connect to a ClickHouse database with compression enabled, by setting
132170
[clickhouse]: https://clickhouse.tech/
133171
[clickhouse-docs-users]:
134172
https://clickhouse.tech/docs/en/operations/settings/settings-users/
173+
[clickhouse-docs-js-config]: https://clickhouse.com/docs/integrations/javascript#configuration
135174
[clickhouse-readonly]: https://clickhouse.com/docs/en/operations/settings/permissions-for-queries#readonly
175+
[ref-config-driverfactory]: /product/configuration/reference/config#driver_factory
176+
[ref-sec-ctx]: /product/auth/context
136177
[ref-caching-using-preaggs-build-strats]:
137178
/product/caching/using-pre-aggregations#pre-aggregation-build-strategies
138179
[ref-recipe-enable-ssl]:

packages/cubejs-clickhouse-driver/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
"watch": "tsc -w",
2424
"lint": "eslint src/* test/* --ext .ts",
2525
"lint:fix": "eslint --fix src/* test/* --ext .ts",
26-
"integration": "jest dist/test",
27-
"integration:clickhouse": "jest dist/test"
26+
"unit": "jest dist/test/unit",
27+
"integration": "jest dist/test/integration",
28+
"integration:clickhouse": "jest dist/test/integration"
2829
},
2930
"dependencies": {
3031
"@clickhouse/client": "^1.12.0",

packages/cubejs-clickhouse-driver/src/ClickHouseDriver.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ export interface ClickHouseDriverOptions {
9595
* Whether this driver is used for pre-aggregations.
9696
*/
9797
preAggregations?: boolean,
98+
99+
/**
100+
* Custom HTTP headers attached to every request sent to ClickHouse.
101+
* @see https://clickhouse.com/docs/integrations/javascript#configuration
102+
*/
103+
headers?: Record<string, string>,
98104
}
99105

100106
interface ClickhouseDriverExportRequiredAWS {
@@ -121,6 +127,7 @@ type ClickHouseDriverConfig = {
121127
exportBucket: ClickhouseDriverExportAWS | null,
122128
compression: { response?: boolean; request?: boolean },
123129
clickhouseSettings: ClickHouseSettings,
130+
headers: Record<string, string>,
124131
};
125132

126133
export class ClickHouseDriver extends BaseDriver implements DriverInterface {
@@ -188,6 +195,8 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
188195
// can not be changed
189196
...(this.readOnlyMode ? {} : { join_use_nulls: 1 }),
190197
},
198+
// Custom HTTP headers can only be passed via driver_factory, not env vars.
199+
headers: config.headers ?? {},
191200
};
192201

193202
const maxPoolSize = config.maxPoolSize ?? getEnv('dbMaxPoolSize', { dataSource, preAggregations }) ?? 8;
@@ -245,6 +254,7 @@ export class ClickHouseDriver extends BaseDriver implements DriverInterface {
245254
clickhouse_settings: this.config.clickhouseSettings,
246255
request_timeout: this.config.requestTimeout,
247256
max_open_connections: maxPoolSize,
257+
http_headers: this.config.headers,
248258
});
249259
}
250260

packages/cubejs-clickhouse-driver/test/ClickHouseDriver.test.ts renamed to packages/cubejs-clickhouse-driver/test/integration/ClickHouseDriver.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ClickhouseDBRunner } from '@cubejs-backend/testing-shared';
22
import { streamToArray } from '@cubejs-backend/shared';
33

4-
import { ClickHouseDriver } from '../src';
5-
import type { ClickHouseDriverOptions } from '../src';
4+
import { ClickHouseDriver } from '../../src';
5+
import type { ClickHouseDriverOptions } from '../../src';
66

77
describe('ClickHouseDriver', () => {
88
jest.setTimeout(20 * 1000);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { createClient } from '@clickhouse/client';
2+
3+
import { ClickHouseDriver } from '../../src';
4+
5+
jest.mock('@clickhouse/client', () => ({
6+
createClient: jest.fn(() => ({
7+
close: jest.fn(),
8+
})),
9+
}));
10+
11+
const createClientMock = createClient as unknown as jest.Mock;
12+
13+
describe('ClickHouseDriver custom headers', () => {
14+
beforeEach(() => {
15+
createClientMock.mockClear();
16+
});
17+
18+
it('forwards custom headers to the underlying client as http_headers', () => {
19+
const headers = {
20+
'X-Custom-Header': 'custom-value',
21+
};
22+
23+
// eslint-disable-next-line no-new
24+
new ClickHouseDriver({
25+
host: 'localhost',
26+
port: '8123',
27+
dataSource: 'default',
28+
headers,
29+
});
30+
31+
expect(createClientMock).toHaveBeenCalled();
32+
expect(createClientMock.mock.calls[0][0]).toMatchObject({ http_headers: headers });
33+
});
34+
35+
it('defaults http_headers to an empty object when no headers are configured', () => {
36+
// eslint-disable-next-line no-new
37+
new ClickHouseDriver({
38+
host: 'localhost',
39+
port: '8123',
40+
dataSource: 'default',
41+
});
42+
43+
expect(createClientMock).toHaveBeenCalled();
44+
expect(createClientMock.mock.calls[0][0]).toMatchObject({ http_headers: {} });
45+
});
46+
});

0 commit comments

Comments
 (0)