diff --git a/sdks/typescript/CHANGELOG.md b/sdks/typescript/CHANGELOG.md index f5d6632433..f0af434ca2 100644 --- a/sdks/typescript/CHANGELOG.md +++ b/sdks/typescript/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to Hatchet's TypeScript SDK will be documented in this chang The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.22.4] - 2026-05-19 + +### Added + +- Adds `grpc_max_recv_message_length` and `grpc_max_send_message_length` to client config, also configurable via env vars. Defaults to 4MB. + ## [1.22.3] - 2026-05-18 ### Fixed diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 218f8501a2..a2652736a3 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@hatchet-dev/typescript-sdk", - "version": "1.22.3", + "version": "1.22.4", "description": "Background task orchestration & visibility for developers", "types": "dist/index.d.ts", "files": [ diff --git a/sdks/typescript/src/clients/hatchet-client/client-config.test.ts b/sdks/typescript/src/clients/hatchet-client/client-config.test.ts index 3c0a3851ac..672b3ccd4a 100644 --- a/sdks/typescript/src/clients/hatchet-client/client-config.test.ts +++ b/sdks/typescript/src/clients/hatchet-client/client-config.test.ts @@ -54,3 +54,48 @@ describe('ClientConfigSchema cancellation timing', () => { ).toThrow(); }); }); + +describe('ClientConfigSchema gRPC max message length', () => { + it('applies 4MB defaults for both recv and send', () => { + const cfg = ClientConfigSchema.parse(baseConfig()); + expect(cfg.grpc_max_recv_message_length).toBe(4 * 1024 * 1024); + expect(cfg.grpc_max_send_message_length).toBe(4 * 1024 * 1024); + }); + + it('accepts custom positive integer values', () => { + const cfg = ClientConfigSchema.parse({ + ...baseConfig(), + grpc_max_recv_message_length: 8 * 1024 * 1024, + grpc_max_send_message_length: 16 * 1024 * 1024, + }); + expect(cfg.grpc_max_recv_message_length).toBe(8 * 1024 * 1024); + expect(cfg.grpc_max_send_message_length).toBe(16 * 1024 * 1024); + }); + + it('rejects invalid values', () => { + expect(() => + ClientConfigSchema.parse({ + ...baseConfig(), + grpc_max_recv_message_length: 0, + }) + ).toThrow(); + expect(() => + ClientConfigSchema.parse({ + ...baseConfig(), + grpc_max_send_message_length: -1, + }) + ).toThrow(); + expect(() => + ClientConfigSchema.parse({ + ...baseConfig(), + grpc_max_recv_message_length: 1.5, + }) + ).toThrow(); + expect(() => + ClientConfigSchema.parse({ + ...baseConfig(), + grpc_max_send_message_length: '4mb' as any, + }) + ).toThrow(); + }); +}); diff --git a/sdks/typescript/src/clients/hatchet-client/client-config.ts b/sdks/typescript/src/clients/hatchet-client/client-config.ts index f584810655..a87a03f476 100644 --- a/sdks/typescript/src/clients/hatchet-client/client-config.ts +++ b/sdks/typescript/src/clients/hatchet-client/client-config.ts @@ -54,6 +54,18 @@ export const ClientConfigSchema = z.object({ middleware: TaskMiddlewareSchema, cancellation_grace_period: DurationMsSchema.optional().default(1000), cancellation_warning_threshold: DurationMsSchema.optional().default(300), + grpc_max_recv_message_length: z + .number() + .int() + .positive() + .optional() + .default(4 * 1024 * 1024), + grpc_max_send_message_length: z + .number() + .int() + .positive() + .optional() + .default(4 * 1024 * 1024), }); export type LogConstructor = (context: string, logLevel?: LogLevel) => Logger; @@ -127,10 +139,15 @@ type ClientConfigInferred = z.infer; export type ClientConfig = Omit< ClientConfigInferred, - 'cancellation_grace_period' | 'cancellation_warning_threshold' + | 'cancellation_grace_period' + | 'cancellation_warning_threshold' + | 'grpc_max_recv_message_length' + | 'grpc_max_send_message_length' > & { cancellation_grace_period?: number; cancellation_warning_threshold?: number; + grpc_max_recv_message_length?: number; + grpc_max_send_message_length?: number; } & { credentials?: ChannelCredentials; } & { diff --git a/sdks/typescript/src/util/config-loader/config-loader.test.ts b/sdks/typescript/src/util/config-loader/config-loader.test.ts index 42b663ddb9..d167a3ff9b 100644 --- a/sdks/typescript/src/util/config-loader/config-loader.test.ts +++ b/sdks/typescript/src/util/config-loader/config-loader.test.ts @@ -4,6 +4,8 @@ describe('ConfigLoader', () => { beforeEach(() => { // Clear env vars that might leak from other tests delete process.env.HATCHET_CLIENT_TLS_STRATEGY; + delete process.env.HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH; + delete process.env.HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH; process.env.HATCHET_CLIENT_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJncnBjX2Jyb2FkY2FzdF9hZGRyZXNzIjoiMTI3LjAuMC4xOjgwODAiLCJzZXJ2ZXJfdXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwic3ViIjoiNzA3ZDA4NTUtODBhYi00ZTFmLWExNTYtZjFjNDU0NmNiZjUyIn0K.abcdef'; @@ -14,6 +16,8 @@ describe('ConfigLoader', () => { process.env.HATCHET_CLIENT_TLS_SERVER_NAME = 'TLS_SERVER_NAME'; process.env.HATCHET_CLIENT_WORKER_HEALTHCHECK_ENABLED = 'true'; process.env.HATCHET_CLIENT_WORKER_HEALTHCHECK_PORT = '8001'; + process.env.HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH = String(8 * 1024 * 1024); + process.env.HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH = String(16 * 1024 * 1024); }); it('should load from environment variables', () => { @@ -41,9 +45,25 @@ describe('ConfigLoader', () => { excludedAttributes: [], includeTaskNameInSpanName: false, }, + grpc_max_recv_message_length: 8 * 1024 * 1024, + grpc_max_send_message_length: 16 * 1024 * 1024, }); }); + it('should throw on a malformed grpc max recv message length env var', () => { + process.env.HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH = '4mb'; + expect(() => ConfigLoader.loadClientConfig()).toThrow( + /HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH.*"4mb".*positive integer/ + ); + }); + + it('should throw on a malformed grpc max send message length env var', () => { + process.env.HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH = '4mb'; + expect(() => ConfigLoader.loadClientConfig()).toThrow( + /HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH.*"4mb".*positive integer/ + ); + }); + it('should throw an error if the file is not found', () => { expect(() => ConfigLoader.loadClientConfig( @@ -97,6 +117,8 @@ describe('ConfigLoader', () => { excludedAttributes: ['additional_metadata'], includeTaskNameInSpanName: true, }, + grpc_max_recv_message_length: 8 * 1024 * 1024, + grpc_max_send_message_length: 16 * 1024 * 1024, }); }); diff --git a/sdks/typescript/src/util/config-loader/config-loader.ts b/sdks/typescript/src/util/config-loader/config-loader.ts index a831953638..cb23390c0f 100644 --- a/sdks/typescript/src/util/config-loader/config-loader.ts +++ b/sdks/typescript/src/util/config-loader/config-loader.ts @@ -21,7 +21,9 @@ type EnvVars = | 'HATCHET_CLIENT_WORKER_HEALTHCHECK_ENABLED' | 'HATCHET_CLIENT_WORKER_HEALTHCHECK_PORT' | 'HATCHET_CLIENT_OPENTELEMETRY_EXCLUDED_ATTRIBUTES' - | 'HATCHET_CLIENT_OPENTELEMETRY_INCLUDE_TASK_NAME_IN_SPAN_NAME'; + | 'HATCHET_CLIENT_OPENTELEMETRY_INCLUDE_TASK_NAME_IN_SPAN_NAME' + | 'HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH' + | 'HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH'; type TLSStrategy = 'tls' | 'mtls'; @@ -105,6 +107,18 @@ export class ConfigLoader { this.env('HATCHET_CLIENT_OPENTELEMETRY_INCLUDE_TASK_NAME_IN_SPAN_NAME') === 'true', }; + const grpcMaxRecvMessageLength = + override?.grpc_max_recv_message_length ?? + yaml?.grpc_max_recv_message_length ?? + this.parseIntEnv('HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH') ?? + 4 * 1024 * 1024; + + const grpcMaxSendMessageLength = + override?.grpc_max_send_message_length ?? + yaml?.grpc_max_send_message_length ?? + this.parseIntEnv('HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH') ?? + 4 * 1024 * 1024; + return { token: override?.token ?? yaml?.token ?? this.env('HATCHET_CLIENT_TOKEN'), host_port: grpcBroadcastAddress, @@ -119,9 +133,22 @@ export class ConfigLoader { tenant_id: tenantId, namespace: namespace ? `${namespace}`.toLowerCase() : '', otel: otelConfig, + grpc_max_recv_message_length: grpcMaxRecvMessageLength, + grpc_max_send_message_length: grpcMaxSendMessageLength, }; } + private static parseIntEnv(envName: EnvVars): number | undefined { + const value = this.env(envName); + if (value === undefined || value === '') return undefined; + if (!/^\d+$/.test(value.trim())) { + throw new Error( + `Invalid value for ${envName}: "${value}". Expected a positive integer.` + ); + } + return parseInt(value, 10); + } + private static parseJsonArray(value: string): string[] { try { const parsed = JSON.parse(value); diff --git a/sdks/typescript/src/util/grpc-helpers.ts b/sdks/typescript/src/util/grpc-helpers.ts index 9a2a8749e4..d4c066a074 100644 --- a/sdks/typescript/src/util/grpc-helpers.ts +++ b/sdks/typescript/src/util/grpc-helpers.ts @@ -19,6 +19,8 @@ export const channelFactory = (config: ClientConfig, credentials: ChannelCredent 'grpc.keepalive_permit_without_calls': 1, // Enable gzip compression for all calls on this channel 'grpc.default_compression_algorithm': 2, // 2 = Gzip compression + 'grpc.max_send_message_length': config.grpc_max_send_message_length ?? 4 * 1024 * 1024, + 'grpc.max_receive_message_length': config.grpc_max_recv_message_length ?? 4 * 1024 * 1024, }); export const addTokenMiddleware = (token: string) => diff --git a/sdks/typescript/src/v1/client/admin.ts b/sdks/typescript/src/v1/client/admin.ts index f4f6398fff..87540053cc 100644 --- a/sdks/typescript/src/v1/client/admin.ts +++ b/sdks/typescript/src/v1/client/admin.ts @@ -248,9 +248,11 @@ export class AdminClient { }; }); - const limit = 4 * 1024 * 1024; // FIXME configurable GRPC limit - - const batches = batch(workflowRequests, batchSize, limit); + const batches = batch( + workflowRequests, + batchSize, + this.config.grpc_max_send_message_length ?? 4 * 1024 * 1024 + ); this.logger.debug(`batching ${batches.length} batches`); diff --git a/sdks/typescript/src/version.ts b/sdks/typescript/src/version.ts index e9957e3875..d0eefc1020 100644 --- a/sdks/typescript/src/version.ts +++ b/sdks/typescript/src/version.ts @@ -1 +1 @@ -export const HATCHET_VERSION = '1.22.1'; +export const HATCHET_VERSION = '1.22.4';