From 579bb7db9baa57b2265485aab41540b9113f726a Mon Sep 17 00:00:00 2001 From: Alexander Kireev Date: Fri, 3 Jul 2026 06:10:28 +0700 Subject: [PATCH] grpc-js: use Uint8Array-safe .set() instead of Buffer-only .copy() in compression filter CompressionFilter's writeMessage methods call message.copy(output, 5) to frame outgoing messages. copy() only exists on Buffer, so if a request serializer hands back a plain Uint8Array instead (protobufjs does this when its Buffer util is unavailable, e.g. under some bundlers), this throws 'TypeError: message.copy is not a function' deep inside grpc-js with no useful context. Buffer.prototype.set works for both types, so swapping to that keeps the framing logic identical while fixing the crash. Added a test that exercises both the identity and non-identity (NoCompress) code paths with a Uint8Array payload. --- packages/grpc-js/src/compression-filter.ts | 4 +- .../grpc-js/test/test-compression-filter.ts | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 packages/grpc-js/test/test-compression-filter.ts diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index e4428a1fb..c69653b36 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -56,7 +56,7 @@ abstract class CompressionHandler { const output = Buffer.allocUnsafe(messageBuffer.length + 5); output.writeUInt8(compress ? 1 : 0, 0); output.writeUInt32BE(messageBuffer.length, 1); - messageBuffer.copy(output, 5); + output.set(messageBuffer, 5); return output; } /** @@ -84,7 +84,7 @@ class IdentityHandler extends CompressionHandler { * uncompressed */ output.writeUInt8(0, 0); output.writeUInt32BE(message.length, 1); - message.copy(output, 5); + output.set(message, 5); return output; } diff --git a/packages/grpc-js/test/test-compression-filter.ts b/packages/grpc-js/test/test-compression-filter.ts new file mode 100644 index 000000000..6d1bbee1f --- /dev/null +++ b/packages/grpc-js/test/test-compression-filter.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2026 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as assert from 'assert'; +import { WriteFlags, WriteObject } from '../src/call-interface'; +import { CompressionAlgorithms } from '../src/compression-algorithms'; +import { CompressionFilter } from '../src/compression-filter'; + +describe('CompressionFilter', () => { + describe('sendMessage', () => { + /* Request serializers are caller-supplied and only nominally typed as + * returning a Buffer. Libraries such as protobufjs can fall back to + * returning a plain Uint8Array at runtime (e.g. when its optional + * Buffer utility is unavailable), so the filter needs to handle that + * without crashing. */ + it('frames a Uint8Array message under identity compression', async () => { + const filter = new CompressionFilter({}, {}); + const payload = new Uint8Array([1, 2, 3, 4, 5]); + assert.strictEqual(payload instanceof Buffer, false); + const writeObject: WriteObject = { + message: payload as unknown as Buffer, + }; + const result = await filter.sendMessage(Promise.resolve(writeObject)); + assert.strictEqual(result.message.readUInt8(0), 0); + assert.strictEqual(result.message.readUInt32BE(1), payload.length); + assert.deepStrictEqual( + Array.from(result.message.subarray(5)), + Array.from(payload) + ); + }); + + it('frames a Uint8Array message when compression is skipped via NoCompress', async () => { + const filter = new CompressionFilter( + { 'grpc.default_compression_algorithm': CompressionAlgorithms.gzip }, + {} + ); + const payload = new Uint8Array([9, 8, 7, 6]); + assert.strictEqual(payload instanceof Buffer, false); + const writeObject: WriteObject = { + message: payload as unknown as Buffer, + flags: WriteFlags.NoCompress, + }; + const result = await filter.sendMessage(Promise.resolve(writeObject)); + assert.strictEqual(result.message.readUInt8(0), 0); + assert.strictEqual(result.message.readUInt32BE(1), payload.length); + assert.deepStrictEqual( + Array.from(result.message.subarray(5)), + Array.from(payload) + ); + }); + }); +});