From 71ca4120296a26176a543045eb06ac4261832579 Mon Sep 17 00:00:00 2001 From: Dmitry Patsura Date: Wed, 17 Dec 2025 12:06:54 +0100 Subject: [PATCH 1/3] feat(server): Introduce CUBEJS_MAX_REQUEST_SIZE env variable (#10260) --- .../reference/environment-variables.mdx | 9 ++++ packages/cubejs-api-gateway/src/gateway.ts | 2 +- packages/cubejs-backend-shared/src/env.ts | 43 ++++++++++++++++ packages/cubejs-backend-shared/src/index.ts | 1 + .../cubejs-backend-shared/test/env.test.ts | 50 ++++++++++++++++++- packages/cubejs-server/src/server.ts | 2 +- .../cubejs-server/src/websocket-server.ts | 3 +- 7 files changed, 106 insertions(+), 4 deletions(-) diff --git a/docs/pages/product/configuration/reference/environment-variables.mdx b/docs/pages/product/configuration/reference/environment-variables.mdx index fc42c1cba3548..10c0746f733e5 100644 --- a/docs/pages/product/configuration/reference/environment-variables.mdx +++ b/docs/pages/product/configuration/reference/environment-variables.mdx @@ -1233,6 +1233,15 @@ after it has finished writing the last response, before a socket will be destroy | ----------------------------------------- | ------------------------ | ------------------------ | | A valid number or string representing one | NodeJS's version default | NodeJS's version default | +## `CUBEJS_MAX_REQUEST_SIZE` + +The maximum allowed size for incoming requests. Applies to both HTTP body parser +and WebSocket message payload limits. Must be between `100kb` and `64mb`. + +| Possible Values | Default in Development | Default in Production | +| ------------------------------------------------- | ---------------------- | --------------------- | +| A size string between 100kb and 64mb (e.g., 1mb) | `50mb` | `50mb` | + ## `CUBEJS_SQL_USER` A username required to access the [SQL API][ref-sql-api]. diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 872bc34a07dd2..2810cb91c5d98 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -325,7 +325,7 @@ class ApiGateway { }); })); - const jsonParser = bodyParser.json({ limit: '1mb' }); + const jsonParser = bodyParser.json({ limit: getEnv('maxRequestSize') }); app.post(`${this.basePath}/v1/load`, jsonParser, userMiddlewares, userAsyncHandler(async (req, res) => { await this.load({ query: req.body.query, diff --git a/packages/cubejs-backend-shared/src/env.ts b/packages/cubejs-backend-shared/src/env.ts index 79aabbf86f085..7128ff1db12de 100644 --- a/packages/cubejs-backend-shared/src/env.ts +++ b/packages/cubejs-backend-shared/src/env.ts @@ -33,6 +33,32 @@ export function convertTimeStrToSeconds( throw new InvalidConfiguration(envName, input, description); } +export function convertSizeToBytes( + input: string, + envName: string, + description: string = 'Must be a number in bytes or size string (1kb, 1mb, 1gb).', +): number { + if (/^\d+$/.test(input)) { + return parseInt(input, 10); + } + + if (input.length > 2) { + switch (input.slice(-2).toLowerCase()) { + case 'kb': + return parseInt(input.slice(0, -2), 10) * 1024; + case 'mb': + return parseInt(input.slice(0, -2), 10) * 1024 * 1024; + case 'gb': + return parseInt(input.slice(0, -2), 10) * 1024 * 1024 * 1024; + default: { + throw new InvalidConfiguration(envName, input, description); + } + } + } + + throw new InvalidConfiguration(envName, input, description); +} + export function asPortNumber(input: number, envName: string) { if (input < 0) { throw new InvalidConfiguration(envName, input, 'Should be a positive integer.'); @@ -148,6 +174,23 @@ const variables: Record any> = { .asInt(), serverKeepAliveTimeout: () => get('CUBEJS_SERVER_KEEP_ALIVE_TIMEOUT') .asInt(), + maxRequestSize: () => { + const value = process.env.CUBEJS_MAX_REQUEST_SIZE || '50mb'; + const bytes = convertSizeToBytes(value, 'CUBEJS_MAX_REQUEST_SIZE'); + + const minBytes = 100 * 1024; // 100kb + const maxBytes = 64 * 1024 * 1024; // 64mb + + if (bytes < minBytes || bytes > maxBytes) { + throw new InvalidConfiguration( + 'CUBEJS_MAX_REQUEST_SIZE', + value, + 'Must be between 100kb and 64mb.' + ); + } + + return bytes; + }, rollupOnlyMode: () => get('CUBEJS_ROLLUP_ONLY') .default('false') .asBoolStrict(), diff --git a/packages/cubejs-backend-shared/src/index.ts b/packages/cubejs-backend-shared/src/index.ts index bcdb0a0b4414e..3e7f0855569d8 100644 --- a/packages/cubejs-backend-shared/src/index.ts +++ b/packages/cubejs-backend-shared/src/index.ts @@ -3,6 +3,7 @@ export { assertDataSource, keyByDataSource, isDockerImage, + convertSizeToBytes, } from './env'; export * from './enums'; export * from './package'; diff --git a/packages/cubejs-backend-shared/test/env.test.ts b/packages/cubejs-backend-shared/test/env.test.ts index 8b15eca83e267..97f40ff41c36b 100644 --- a/packages/cubejs-backend-shared/test/env.test.ts +++ b/packages/cubejs-backend-shared/test/env.test.ts @@ -1,4 +1,4 @@ -import { getEnv, convertTimeStrToSeconds } from '../src/env'; +import { getEnv, convertTimeStrToSeconds, convertSizeToBytes } from '../src/env'; test('convertTimeStrToMs', () => { expect(convertTimeStrToSeconds('1', 'VARIABLE_ENV')).toBe(1); @@ -16,6 +16,28 @@ test('convertTimeStrToMs(exception)', () => { ); }); +test('convertSizeToBytes', () => { + expect(convertSizeToBytes('1024', 'VARIABLE_ENV')).toBe(1024); + expect(convertSizeToBytes('1kb', 'VARIABLE_ENV')).toBe(1024); + expect(convertSizeToBytes('10KB', 'VARIABLE_ENV')).toBe(10 * 1024); + expect(convertSizeToBytes('1mb', 'VARIABLE_ENV')).toBe(1024 * 1024); + expect(convertSizeToBytes('50MB', 'VARIABLE_ENV')).toBe(50 * 1024 * 1024); + expect(convertSizeToBytes('1gb', 'VARIABLE_ENV')).toBe(1024 * 1024 * 1024); + expect(convertSizeToBytes('2GB', 'VARIABLE_ENV')).toBe(2 * 1024 * 1024 * 1024); +}); + +test('convertSizeToBytes(exception)', () => { + expect(() => convertSizeToBytes('', 'VARIABLE_ENV')).toThrowError( + `Value "" is not valid for VARIABLE_ENV. Must be a number in bytes or size string (1kb, 1mb, 1gb).` + ); + expect(() => convertSizeToBytes('abc', 'VARIABLE_ENV')).toThrowError( + `Value "abc" is not valid for VARIABLE_ENV. Must be a number in bytes or size string (1kb, 1mb, 1gb).` + ); + expect(() => convertSizeToBytes('1tb', 'VARIABLE_ENV')).toThrowError( + `Value "1tb" is not valid for VARIABLE_ENV. Must be a number in bytes or size string (1kb, 1mb, 1gb).` + ); +}); + describe('getEnv', () => { test('port(exception)', () => { process.env.PORT = '100000000'; @@ -94,4 +116,30 @@ describe('getEnv', () => { process.env.CUBEJS_LIVE_PREVIEW = 'false'; expect(getEnv('livePreview')).toBe(false); }); + + test('maxRequestSize', () => { + delete process.env.CUBEJS_MAX_REQUEST_SIZE; + expect(getEnv('maxRequestSize')).toBe(50 * 1024 * 1024); // default 50mb + + process.env.CUBEJS_MAX_REQUEST_SIZE = '64mb'; + expect(getEnv('maxRequestSize')).toBe(64 * 1024 * 1024); + + process.env.CUBEJS_MAX_REQUEST_SIZE = '100kb'; + expect(getEnv('maxRequestSize')).toBe(100 * 1024); + + process.env.CUBEJS_MAX_REQUEST_SIZE = '512kb'; + expect(getEnv('maxRequestSize')).toBe(512 * 1024); + }); + + test('maxRequestSize(exception)', () => { + process.env.CUBEJS_MAX_REQUEST_SIZE = '50kb'; + expect(() => getEnv('maxRequestSize')).toThrowError( + 'Value "50kb" is not valid for CUBEJS_MAX_REQUEST_SIZE. Must be between 100kb and 64mb.' + ); + + process.env.CUBEJS_MAX_REQUEST_SIZE = '100mb'; + expect(() => getEnv('maxRequestSize')).toThrowError( + 'Value "100mb" is not valid for CUBEJS_MAX_REQUEST_SIZE. Must be between 100kb and 64mb.' + ); + }); }); diff --git a/packages/cubejs-server/src/server.ts b/packages/cubejs-server/src/server.ts index 064e5bbefa626..45b7da627b252 100644 --- a/packages/cubejs-server/src/server.ts +++ b/packages/cubejs-server/src/server.ts @@ -93,7 +93,7 @@ export class CubejsServer { const app = express(); app.use(cors(this.config.http.cors)); - app.use(bodyParser.json({ limit: '50mb' })); + app.use(bodyParser.json({ limit: getEnv('maxRequestSize') })); if (this.config.gracefulShutdown) { app.use(gracefulMiddleware(this.status, this.config.gracefulShutdown)); diff --git a/packages/cubejs-server/src/websocket-server.ts b/packages/cubejs-server/src/websocket-server.ts index 3d42bf5b02128..44353568b2b8e 100644 --- a/packages/cubejs-server/src/websocket-server.ts +++ b/packages/cubejs-server/src/websocket-server.ts @@ -1,7 +1,7 @@ import WebSocket from 'ws'; import crypto from 'crypto'; import util from 'util'; -import { CancelableInterval, createCancelableInterval } from '@cubejs-backend/shared'; +import { CancelableInterval, createCancelableInterval, getEnv } from '@cubejs-backend/shared'; import type { CubejsServerCore } from '@cubejs-backend/server-core'; import type http from 'http'; @@ -29,6 +29,7 @@ export class WebSocketServer { this.wsServer = new WebSocket.Server({ server, path: this.options.webSocketsBasePath, + maxPayload: getEnv('maxRequestSize'), }); const connectionIdToSocket: Record = {}; From 3915e5a26d49b812f70a537ec9fde0d0f762692e Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Wed, 17 Dec 2025 14:09:38 +0200 Subject: [PATCH 2/3] fix(snowflake-driver): Fix type mapping for numerics with scale (#10259) --- packages/cubejs-snowflake-driver/src/SnowflakeDriver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/cubejs-snowflake-driver/src/SnowflakeDriver.ts b/packages/cubejs-snowflake-driver/src/SnowflakeDriver.ts index fd8a91ce7f524..beb8e758d0069 100644 --- a/packages/cubejs-snowflake-driver/src/SnowflakeDriver.ts +++ b/packages/cubejs-snowflake-driver/src/SnowflakeDriver.ts @@ -953,8 +953,6 @@ export class SnowflakeDriver extends BaseDriver implements DriverInterface { if (scale === 0) { type.type = 'int'; - } else if (precision && scale && scale <= 10) { - type.type = 'decimal'; } else { type.type = this.toGenericType(column.getType(), precision, scale); } From ad4fd7c6d76c691795282a7a0f773447efd97011 Mon Sep 17 00:00:00 2001 From: Igor Lukanin Date: Wed, 17 Dec 2025 13:16:53 +0100 Subject: [PATCH 3/3] docs: Mark query pushdown in the SQL API as GA --- .../core-data-apis/queries.mdx | 7 ---- .../core-data-apis/sql-api.mdx | 42 ++++++------------- .../core-data-apis/sql-api/query-format.mdx | 24 ++--------- .../reference/environment-variables.mdx | 17 +------- 4 files changed, 16 insertions(+), 74 deletions(-) diff --git a/docs/pages/product/apis-integrations/core-data-apis/queries.mdx b/docs/pages/product/apis-integrations/core-data-apis/queries.mdx index 93f428ef791c3..bf40cb31eaf39 100644 --- a/docs/pages/product/apis-integrations/core-data-apis/queries.mdx +++ b/docs/pages/product/apis-integrations/core-data-apis/queries.mdx @@ -263,13 +263,6 @@ with pushdown, Cube will need to transform it and generate the SQL for an upstream [data source][ref-data-sources]. Learn more in the [SQL API documentation][ref-sql-api-qpd]. - - -Query pushdown in the SQL API is available in public preview. -[Read more](https://cube.dev/blog/query-push-down-in-cubes-semantic-layer) in the blog. - - - Queries with pushdown, since they don't include regular queries, can not utilize pre-aggregations; however, they still benefit from [in-memory cache][ref-caching]. Queries with pushdown can also be modified before diff --git a/docs/pages/product/apis-integrations/core-data-apis/sql-api.mdx b/docs/pages/product/apis-integrations/core-data-apis/sql-api.mdx index afdea1d6b0678..7db7e657185e8 100644 --- a/docs/pages/product/apis-integrations/core-data-apis/sql-api.mdx +++ b/docs/pages/product/apis-integrations/core-data-apis/sql-api.mdx @@ -134,20 +134,10 @@ There are trade-offs associated with each query type: -Query pushdown in the SQL API is available in public preview. -[Read more](https://cube.dev/blog/query-push-down-in-cubes-semantic-layer) in the blog. +[Read more](https://cube.dev/blog/query-push-down-in-cubes-semantic-layer) about Query pushdown in the SQL API in the blog. - - -**Query pushdown is disabled by default.** You should explicitly [enable -it](#query-planning). In future versions, it will be enabled by default. Also, -enabling query pushdown would affect how [ungrouped queries][ref-ungrouped-queries] -are executed; check [query format][ref-sql-query-format] for details. - - - ## Configuration ### Cube Core @@ -214,25 +204,8 @@ variables by navigating to Settings → Configration. ### Query planning -**By default, the SQL API executes queries as [regular queries][ref-regular-queries] -or [queries with post-processing][ref-queries-wpp].** Such queries support only a limited -set of SQL functions and operators, and sometimes you can get the following error: -`Error during rewrite: Can't detect Cube query and it may be not supported yet.` - -You can use the `CUBESQL_SQL_PUSH_DOWN` environment variable to instruct the SQL API -to execute such queries as [queries with pushdown][ref-queries-wpd]. - - - -Query pushdown in the SQL API is available in public preview. -[Read more](https://cube.dev/blog/query-push-down-in-cubes-semantic-layer) in the blog. - - - -Query planning is a resource-intensive task, and sometimes you can get the following -error: `Error during rewrite: Can't find rewrite due to 10002 AST node limit reached.` -Use the following environment variables to allocate more resources for query planning: -`CUBESQL_REWRITE_MAX_NODES`, `CUBESQL_REWRITE_MAX_ITERATIONS`, `CUBESQL_REWRITE_TIMEOUT`. +The SQL API executes queries as [regular queries][ref-regular-queries], [queries with +post-processing][ref-queries-wpp], or [queries with pushdown][ref-queries-wpd]. ### Streaming @@ -254,6 +227,15 @@ to establish too many connections at once can lead to an out-of-memory crash. You can use the `CUBEJS_MAX_SESSIONS` environment variable to adjust the session limit. +## Troubleshooting + +### `Can't find rewrite` + +[Query planning](#query-planning) is a resource-intensive task, and sometimes you can get the following +error: `Error during rewrite: Can't find rewrite due to 10002 AST node limit reached.` +Use the following environment variables to allocate more resources for query planning: +`CUBESQL_REWRITE_MAX_NODES`, `CUBESQL_REWRITE_MAX_ITERATIONS`, `CUBESQL_REWRITE_TIMEOUT`. + [link-postgres]: https://www.postgresql.org [ref-dax-api]: /product/apis-integrations/dax-api diff --git a/docs/pages/product/apis-integrations/core-data-apis/sql-api/query-format.mdx b/docs/pages/product/apis-integrations/core-data-apis/sql-api/query-format.mdx index 6928d53da054c..5a0acb39cc932 100644 --- a/docs/pages/product/apis-integrations/core-data-apis/sql-api/query-format.mdx +++ b/docs/pages/product/apis-integrations/core-data-apis/sql-api/query-format.mdx @@ -3,17 +3,9 @@ SQL API runs queries in the Postgres dialect that can reference those tables and columns. -By default since 1.0, the SQL API executes [regular queries][ref-regular-queries], -[queries with post-processing][ref-queries-wpp] and [queries with -pushdown][ref-queries-wpd]. This page explains their format and details if they -are handled differently by the SQL API. - - - -Query pushdown in the SQL API is available in public preview. -[Read more](https://cube.dev/blog/query-push-down-in-cubes-semantic-layer) in the blog. - - +The SQL API is able to execute [regular queries][ref-regular-queries], [queries with +post-processing][ref-queries-wpp] and [queries with pushdown][ref-queries-wpd]. This page +explains their format and details if they are handled differently by the SQL API. ## Data model mapping @@ -202,16 +194,6 @@ queries **not** querying cube tables. ### Query pushdown - - - Query pushdown is currently in public preview, and the API and behavior may - change in future versions. - - Query pushdown is enabled by default since 1.0 and is controlled by - `CUBESQL_SQL_PUSH_DOWN` environment variable. - - - Query pushdown currently has the following limitations: diff --git a/docs/pages/product/configuration/reference/environment-variables.mdx b/docs/pages/product/configuration/reference/environment-variables.mdx index 10c0746f733e5..be11ea3eb7ea8 100644 --- a/docs/pages/product/configuration/reference/environment-variables.mdx +++ b/docs/pages/product/configuration/reference/environment-variables.mdx @@ -933,14 +933,6 @@ The default [time zone][ref-time-zone] for queries. You can set the time zone name in the [TZ Database Name][link-tzdb] format, e.g., `America/Los_Angeles`. - - -Increasing the maximum row limit may cause out-of-memory (OOM) crashes and make -Cube susceptible to denial-of-service (DoS) attacks if it's exposed to -untrusted environments. - - - ## `CUBEJS_DEFAULT_API_SCOPES` [API scopes][ref-rest-scopes] used to allow or disallow access to REST API @@ -1301,14 +1293,7 @@ If `true`, enables query pushdown in the [SQL API][ref-sql-api]. | Possible Values | Default in Development | Default in Production | | --------------- | ---------------------- | --------------------- | -| `true`, `false` | `true` | `true` | - - - -Query pushdown in the SQL API is available in public preview. -[Read more](https://cube.dev/blog/query-push-down-in-cubes-semantic-layer) in the blog. - - +| `true`, `false` | `true` | `true` | ## `CUBESQL_STREAM_MODE`