Azure / Azure/azure-functions-nodejs-worker

[Regression] Event Hubs batch sequenceNumberArray exposed as Long-like objects with Worker 3.16.0

Open
#844 1 comment 0 reactions 0 assignees View on GitHub
area:nodejs-functions bug P1
Dominant language
TypeScript
Stars
110
Forks
51
Avg merge
1d 1h
Merged PRs (30d)
2

Description

## Summary

After Functions Host 4.1053 began rolling out, Node.js Event Hubs functions using batch cardinality can receive `context.triggerMetadata.sequenceNumberArray` as Long-like objects instead of JavaScript numbers.

Working behavior with Host 4.1052 / Node.js Worker 3.14.1:

```js
[2251, 2252, 2253]
```

Regressed behavior with Host 4.1053 / Node.js Worker 3.16.0:

```js
[
{ low: 2251, high: 0, unsigned: false },
{ low: 2252, high: 0, unsigned: false },
{ low: 2253, high: 0, unsigned: false }
]
```

The Event Hubs extension and application package do not need to change for the behavior to appear. Recycling from Host 4.1052 to 4.1053 is sufficient.

#### Investigative information

- First observed: 2026-08-26
- Affected Host: 4.1053.200.x
- Known-good Host: 4.1052.300.x
- Affected Node.js Worker: 3.16.0
- Known-good Node.js Worker: 3.14.1
- Event Hubs extension: 6.5.3 on both sides of the comparison
- `@azure/functions`: 4.11.2 on both sides of the customer comparison
- Reproduced on Flex Consumption and Dedicated/App Service plans

Customer-specific app and invocation information is intentionally omitted from this public issue.

#### Repro steps

1. Create a Node.js Event Hubs trigger using batch cardinality (`cardinality: "many"`).
2. Log or otherwise inspect `context.triggerMetadata.sequenceNumberArray`.
3. Run on Host 4.1052 with Node.js Worker 3.14.1 and observe primitive numbers.
4. Run the same application and Event Hubs extension on Host 4.1053 with Node.js Worker 3.16.0.
5. Observe Long-like objects containing `low`, `high`, and `unsigned`.

A wire-level worker test also reproduces the behavior: `@grpc/proto-loader` returns Long objects when no `longs` conversion policy is specified, while `longs: Number` returns `[2251, 2252, 2253]`.

#### Expected behavior

`CollectionSInt64` trigger metadata should continue to be exposed using the existing JavaScript `number` contract:

```js
context.triggerMetadata.sequenceNumberArray // number[]
```

A dependency or bundling change must not silently alter the public shape of trigger metadata.

#### Actual behavior

`CollectionSInt64` values are decoded as protobuf Long objects. The `@azure/functions` trigger metadata camel-casing path subsequently copies their enumerable fields into plain objects, so application code receives objects without `Long.prototype.toNumber()` or `toString()`.

This can break code that performs arithmetic, validation, persistence, or equality checks assuming `sequenceNumberArray` contains numbers.

#### Customer impact

An application is impacted when it does one or more of the following:

- Reads `context.triggerMetadata.sequenceNumberArray` and treats its elements as numbers.
- Uses another trigger metadata value represented by protobuf `sint64` or `CollectionSInt64` and assumes the documented primitive-number shape.
- Serializes, compares, validates, or forwards the affected metadata and depends on its previous shape.

Applications that do not access or use the affected trigger metadata are not expected to be impacted. Event payload delivery and normal Event Hubs trigger execution are not, by themselves, changed by this regression.

#### Known workarounds

##### Dedicated/App Service plans where exact Host pinning is supported

Temporarily pin the Function App to a known-good 4.1052 Host build, such as `4.1052.300.26370`, using the supported exact-version pinning mechanism for that environment. For environments that use `FUNCTIONS_EXTENSION_VERSION` for exact pinning, set it to the known-good exact version and restart the app.

Customers should coordinate an exact runtime pin with Azure Support, especially for Linux-hosted apps, and restore the normal `~4` tracking setting after the hotfix has rolled out. Exact Host pinning is a temporary mitigation, not the long-term fix.

##### Flex Consumption, or when Host pinning is unavailable

Flex Consumption does not provide a customer-controlled exact Host version pin. Normalize the metadata in the Event Hubs handler before using it:

```ts
type LongLike = {
low: number;
high: number;
unsigned?: boolean;
toNumber?: () => number;
};

function toSafeNumber(value: unknown): number {
if (typeof value === "number") {
return value;
}

if (typeof value === "string") {
const result = Number(value);
if (!Number.isSafeInteger(result)) {
throw new RangeError("Value exceeds the JavaScript safe integer range");
}
return result;
}

const long = value as LongLike;
if (typeof long?.toNumber === "function") {
const result = long.toNumber();
if (!Number.isSafeInteger(result)) {
throw new RangeError("Value exceeds the JavaScript safe integer range");
}
return result;
}

if (!Number.isInteger(long?.low) || !Number.isInteger(long?.high)) {
throw new TypeError("Expected a number or protobuf Long-like value");
}

const high = BigInt(long.unsigned ? long.high >>> 0 : long.high);
const result = Number((high << 32n) + BigInt(long.low >>> 0));
if (!Number.isSafeInteger(result)) {
throw new RangeError("Value exceeds the JavaScript safe integer range");
}

return result;
}

const rawSequenceNumbers = context.triggerMetadata?.sequenceNumberArray;
const sequenceNumbers = Array.isArray(rawSequenceNumbers)
? rawSequenceNumbers.map(toSafeNumber)
: [];
```

The application must explicitly call this conversion, as shown by `.map(toSafeNumber)`, before using the metadata. The workaround accepts both the old primitive-number shape and the regressed Long-like shape, so it can remain in place while the platform hotfix rolls out.

#### Root cause

Worker 3.16.0 resolved `protobufjs` 7.6.5 instead of 7.5.6. In protobufjs 7.6.x, `long` became statically resolvable to webpack after the `@protobufjs/inquire` dependency was removed. The worker bundle therefore contains a Long implementation where the previous bundle effectively did not.

The worker currently loads the RPC descriptor without an explicit 64-bit integer conversion policy:

```ts
grpcloader.fromJSON(jsonModule, {
objects: true,
defaults: true,
oneofs: true,
});
```

Because `longs` is unspecified, the decoded JavaScript type depends on whether a Long implementation is available in the bundle.

#### Proposed hotfix direction

The current direction is to release a Node.js Worker hotfix that:

1. Sets `longs: Number` explicitly in the worker's `@grpc/proto-loader` options, restoring the established JavaScript `number` contract.
2. Adds regression coverage for scalar `TypedData.int` and `CollectionSInt64`, including validation against the webpack bundle.
3. Re-evaluates the now-inert `@protobufjs/inquire` package override.

We will post progress updates on this issue, including the fix PR, hotfix worker version, Functions Host integration, and rollout status as those details become available.

#### Related information

- Language: JavaScript/TypeScript
- Binding: Event Hubs trigger with batch cardinality
- Worker code path: `src/GrpcClient.ts`
- Library conversion paths: `src/converters/fromRpcTypedData.ts` and `src/converters/toCamelCase.ts` in `@azure/functions`

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in src/GrpcClient.ts, where the RPC descriptor is loaded, and review the related conversion paths in src/converters/fromRpcTypedData.ts and src/converters/toCamelCase.ts. Add regression coverage for scalar TypedData.int and CollectionSInt64, then validate the behavior against the webpack bundle. Done means trigger metadata continues to expose number values for these fields.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.