Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
26c5fe4
grpc-js(-xds): Bump to 1.14.0 and update README
murgatroid99 Sep 12, 2025
3dd281b
Merge pull request #3014 from murgatroid99/grpc-js_v1.14_bump
murgatroid99 Sep 15, 2025
dcb2182
grpc-js: Re-add a couple of accidentally removed HTTP/2 session settings
murgatroid99 Nov 4, 2025
425e7cb
Merge pull request #3023 from murgatroid99/grpc-js_session_memory_fix
murgatroid99 Nov 7, 2025
2f090c6
grpc-js: Fix server keep alive timeout not properly destroying connec…
mattias-wiberg Oct 24, 2025
235474e
grpc-js: Bump to 1.14.2
murgatroid99 Dec 1, 2025
bf0dc8f
Merge pull request #3029 from murgatroid99/grpc-js_keepalive_fix_back…
murgatroid99 Dec 2, 2025
4e26a27
half close right after write
serkanerip Dec 8, 2025
37f2817
revert changes
serkanerip Dec 8, 2025
f6895cb
Send halfClose immediately after messages to prevent late halfClose i…
serkanerip Dec 9, 2025
88a083d
remove halfCloseSent field
serkanerip Dec 10, 2025
4d387d4
Use nextMessageToSend for early half-close
serkanerip Dec 10, 2025
9ea15ce
grpc-js: Bump version to 1.14.3
murgatroid99 Dec 10, 2025
ccd29b2
Merge pull request #3032 from murgatroid99/grpc-js_retry_half_close_1.14
murgatroid99 Dec 11, 2025
4f3c58f
grpc-js-xds: Update RBAC code to handle Node type change, pin @types/…
murgatroid99 Mar 12, 2026
acef8d4
Merge pull request #3043 from murgatroid99/rbac_types_change_fix_1.14
murgatroid99 Mar 17, 2026
234f917
Fix server crash when handling invalid requests
murgatroid99 May 13, 2026
2fe55fd
Fix crashes when receiving malformed compressed data
murgatroid99 May 13, 2026
5029a26
Make compression error a static string
murgatroid99 May 18, 2026
e5e0b1d
grpc-js: Bump version to 1.14.4
murgatroid99 May 18, 2026
6a97456
Merge commit from fork
murgatroid99 May 19, 2026
5b8d37b
Merge commit from fork
murgatroid99 May 19, 2026
a380735
Merge pull request #3052 from murgatroid99/grpc-js_1.14.4
murgatroid99 May 20, 2026
49da446
grpc-js: stop keepalive pings once a session is draining after GOAWAY
ws-gregm Aug 18, 2026
f9344c2
fork: publish @wingspanhq/grpc-js to GitHub Packages
ws-gregm Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/workflows/publish-wingspan-grpc-js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Publish @wingspanhq/grpc-js

on:
push:
branches:
- 'wingspan/**'
paths:
- 'packages/grpc-js/**'
- '.github/workflows/publish-wingspan-grpc-js.yml'
workflow_dispatch:

jobs:
publish:
name: build and publish to GitHub Packages
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Init proto submodules
run: git submodule update --init packages/grpc-js-xds/deps/xds packages/grpc-js-xds/deps/protoc-gen-validate
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://npm.pkg.github.com'
scope: '@wingspanhq'
# --ignore-scripts everywhere: the sibling packages' prepare scripts do
# not build in isolation, and generated proto types are checked in, so
# nothing needs regenerating. proto-loader is built explicitly because
# grpc-js consumes it as a file: devDependency (a symlink) and needs its
# build/ output for type resolution.
- name: Build proto-loader (sibling devDependency)
run: |
npm install --ignore-scripts
./node_modules/.bin/tsc -p .
working-directory: packages/proto-loader
- name: Install grpc-js dependencies
run: npm install --ignore-scripts
working-directory: packages/grpc-js
- name: Copy ORCA protos
run: node copy-protos.js
working-directory: packages/grpc-js
- name: Compile
run: ./node_modules/.bin/tsc -p tsconfig.publish.json
working-directory: packages/grpc-js
- name: Publish
run: npm publish --ignore-scripts
working-directory: packages/grpc-js
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
104 changes: 104 additions & 0 deletions FORK.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Wingspan fork of grpc/grpc-node

This is Wingspan's fork of [grpc/grpc-node](https://github.com/grpc/grpc-node). It exists to
publish **`@wingspanhq/grpc-js`** — a build of `@grpc/grpc-js` that carries one behavioral fix —
to GitHub Packages. No other package in this monorepo is published from the fork.

## Why this fork exists

A graceful HTTP/2 GOAWAY leaves grpc-js's keepalive timer running on both the client transport
and the server session. The next PING that fails or times out triggers the disconnect path,
destroying in-flight RPCs the GOAWAY had deliberately allowed to finish draining. In production
this surfaced as status 14 / `Connection dropped` on calls that were completing normally during
deploys and connection-age recycling.

Reported upstream as [grpc/grpc-node#3068](https://github.com/grpc/grpc-node/issues/3068).
Upstream has pushed back on the premise (RFC 9113 requires a PING to be ACKed regardless of
GOAWAY), so this fix may never merge upstream in its current form. Until that conversation
resolves, this fork is the delivery vehicle. It replaces the previous mechanism — a
`patch-package` postinstall hook in `@wingspanhq/grpc` that rewrote grpc-js's compiled output in
consumers' `node_modules` — which was fragile across package managers and install layouts
(see wingspanHQ/grpc#89).

## What is changed relative to upstream

Branch naming: `wingspan/grpc-js-<upstream-version>`, based on the upstream release tag
`@grpc/grpc-js@<upstream-version>`. Each branch contains exactly two kinds of commits:

1. **The fix** — `packages/grpc-js/src/transport.ts` and `packages/grpc-js/src/server.ts` only.
Client: mark the transport draining on `goaway`, clear the keepalive timeout, refuse new
pings, ignore in-flight ping completions. Server: begin draining before locally initiated
closes (`closeSession`, max connection age) and on peer GOAWAY. Kept free of fork-identity
noise so it can be cherry-picked into an upstream PR.
2. **Fork identity** — package rename to `@wingspanhq/grpc-js`, version `X.Y.Z-wingspan.N`,
GitHub Packages `publishConfig`, this file, and the publish workflow.

`master` tracks upstream and carries no Wingspan changes.

## How it is consumed

`@wingspanhq/grpc` (the internal gRPC framework) depends on it via an npm alias:

```json
"@grpc/grpc-js": "npm:@wingspanhq/grpc-js@1.14.4-wingspan.1"
```

so every `import from '@grpc/grpc-js'` — including deep imports and `@grpc/grpc-js-xds`'s peer
resolution — lands on the fork. Services do not (and should not) depend on this package
directly; it arrives transitively through `@wingspanhq/grpc`.

Expect a yarn peer-dependency warning from `@grpc/grpc-js-xds` (`~1.14.0` does not match a
prerelease version). It is benign: the alias puts the fork at `node_modules/@grpc/grpc-js`,
which is what xds resolves.

## Publishing a new version

The `Publish @wingspanhq/grpc-js` workflow (`.github/workflows/publish-wingspan-grpc-js.yml`)
runs on pushes to `wingspan/**` branches that touch `packages/grpc-js/`, or manually via
workflow dispatch. It builds from checked-in generated types (no proto regeneration) and
publishes to GitHub Packages with the repo's `GITHUB_TOKEN`.

To publish locally instead (requires a token with `write:packages` for wingspanHQ):

```bash
git submodule update --init packages/grpc-js-xds/deps/xds packages/grpc-js-xds/deps/protoc-gen-validate
(cd packages/proto-loader && npm install --ignore-scripts && ./node_modules/.bin/tsc -p .)
cd packages/grpc-js
npm install --ignore-scripts
node copy-protos.js
./node_modules/.bin/tsc -p tsconfig.publish.json
npm publish --ignore-scripts
```

`--ignore-scripts` on install stops the sibling packages' `prepare` scripts, which do not build
in isolation; on publish it skips `prepare`, which would regenerate checked-in proto types with
tooling this flow deliberately avoids. proto-loader must be compiled first because grpc-js
consumes it as a `file:` devDependency (a symlink) and resolves types from its `build/` output.
`tsconfig.publish.json` compiles `src/` only (the published `build/src` tree) with node types
included explicitly; the stock `tsconfig.json` targets upstream's gulp pipeline and compiles
tests too.

## Tracking a new upstream release

When upstream tags `@grpc/grpc-js@X.Y.Z`:

```bash
git fetch upstream --tags # upstream = https://github.com/grpc/grpc-node.git
git checkout -b wingspan/grpc-js-X.Y.Z "@grpc/grpc-js@X.Y.Z"
git cherry-pick <fix commit> # commit 1 from the previous wingspan/ branch
git cherry-pick <identity commit> # then bump version to X.Y.Z-wingspan.1
```

Resolve conflicts in the two touched source files by re-reading the surrounding upstream code —
the fix is small and its anchors (goaway handler, `canSendPing`, ping callback, `closeSession`,
connection-age timers) are stable but not guaranteed. After building, compare the compiled
`transport.js`/`server.js` against the previous branch's output to review what upstream changed
underneath the fix. Then update the alias version and the expected SHA-256 digests in
`@wingspanhq/grpc` (`src/grpcJsPatchIntegrity.ts`) — its startup integrity check hashes the
compiled files and fails closed on drift.

## Exit criteria

If upstream ships an equivalent fix (or #3068 concludes with a config-level answer), point
`@wingspanhq/grpc` back at stock `@grpc/grpc-js`, delete the alias and digests bump, and archive
this fork.
4 changes: 3 additions & 1 deletion packages/grpc-js-xds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ const client = new MyServiceClient('xds:///example.com:123');
- [Outlier Detection](https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md)
- [xDS Retry Support](https://github.com/grpc/proposal/blob/master/A44-xds-retry.md)
- [xDS Aggregate and Logical DNS Clusters](https://github.com/grpc/proposal/blob/master/A37-xds-aggregate-and-logical-dns-clusters.md)
- [xDS Federation](https://github.com/grpc/proposal/blob/master/A47-xds-federation.md) (Currently experimental, enabled by environment variable `GRPC_EXPERIMENTAL_XDS_FEDERATION`)
- [xDS Federation](https://github.com/grpc/proposal/blob/master/A47-xds-federation.md)
- [xDS Custom Load Balancer Configuration](https://github.com/grpc/proposal/blob/master/A52-xds-custom-lb-policies.md) (Custom load balancer registration not currently supported)
- [xDS Ring Hash LB Policy](https://github.com/grpc/proposal/blob/master/A42-xds-ring-hash-lb-policy.md)
- [`pick_first` via xDS](https://github.com/grpc/proposal/blob/master/A62-pick-first.md#pick_first-via-xds-1) (Currently experimental, enabled by environment variable `GRPC_EXPERIMENTAL_PICKFIRST_LB_CONFIG`)
- [xDS-Enabled Servers](https://github.com/grpc/proposal/blob/master/A36-xds-for-servers.md)
- [xDS-Based Security for gRPC Clients and Servers](https://github.com/grpc/proposal/blob/master/A29-xds-tls-security.md)
- [xDS RBAC Support](https://github.com/grpc/proposal/blob/master/A41-xds-rbac.md)
- [`weighted_round_robin` LB policy](https://github.com/grpc/proposal/blob/master/A58-client-side-weighted-round-robin-lb-policy.md) (Inclusion in xDS registry is currently experimental, enabled by environment variable `GRPC_EXPERIMENTAL_XDS_WRR_LB`)
6 changes: 3 additions & 3 deletions packages/grpc-js-xds/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@grpc/grpc-js-xds",
"version": "1.13.0",
"version": "1.14.0",
"description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.",
"main": "build/src/index.js",
"scripts": {
Expand Down Expand Up @@ -38,7 +38,7 @@
"@types/gulp": "^4.0.6",
"@types/gulp-mocha": "0.0.32",
"@types/mocha": "^5.2.6",
"@types/node": ">=20.11.20",
"@types/node": "25.5.0",
"@types/yargs": "^15.0.5",
"grpc-health-check": "file:../grpc-health-check",
"gts": "^5.0.1",
Expand All @@ -55,7 +55,7 @@
"xxhash-wasm": "^1.0.2"
},
"peerDependencies": {
"@grpc/grpc-js": "~1.13.0"
"@grpc/grpc-js": "~1.14.0"
},
"engines": {
"node": ">=10.10.0"
Expand Down
9 changes: 8 additions & 1 deletion packages/grpc-js-xds/src/rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,14 @@ export class AuthenticatedPrincipal implements PrincipalRule {
}
}
}
return this.nameMatcher.apply(info.peerCertificate.subject.CN);
if (info.peerCertificate.subject.CN) {
if (Array.isArray(info.peerCertificate.subject.CN)) {
return info.peerCertificate.subject.CN.some(entry => this.nameMatcher!.apply(entry));
} else {
return this.nameMatcher.apply(info.peerCertificate.subject.CN);
}
}
return false;
}
toString(): string {
return `Authenticated(principal=${this.nameMatcher?.toString() ?? null})`;
Expand Down
14 changes: 9 additions & 5 deletions packages/grpc-js/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "@grpc/grpc-js",
"version": "1.13.4",
"description": "gRPC Library for Node - pure JS implementation",
"name": "@wingspanhq/grpc-js",
"version": "1.14.4-wingspan.1",
"description": "Wingspan fork of @grpc/grpc-js carrying the GOAWAY/keepalive drain fix (grpc/grpc-node#3068)",
"homepage": "https://grpc.io/",
"repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js",
"repository": "git+https://github.com/wingspanHQ/grpc-node.git",
"main": "build/src/index.js",
"engines": {
"node": ">=12.10.0"
Expand Down Expand Up @@ -85,5 +85,9 @@
"deps/googleapis/google/api/*.proto",
"deps/googleapis/google/rpc/*.proto",
"deps/protoc-gen-validate/validate/**/*.proto"
]
],
"publishConfig": {
"registry": "https://npm.pkg.github.com/",
"access": "restricted"
}
}
12 changes: 12 additions & 0 deletions packages/grpc-js/src/compression-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ class DeflateHandler extends CompressionHandler {
let totalLength = 0;
const messageParts: Buffer[] = [];
const decompresser = zlib.createInflate();
decompresser.on('error', (error: Error) => {
reject({
code: Status.INTERNAL,
details: 'Failed to decompress deflate-encoded message'
});
});
decompresser.on('data', (chunk: Buffer) => {
messageParts.push(chunk);
totalLength += chunk.byteLength;
Expand Down Expand Up @@ -161,6 +167,12 @@ class GzipHandler extends CompressionHandler {
let totalLength = 0;
const messageParts: Buffer[] = [];
const decompresser = zlib.createGunzip();
decompresser.on('error', (error: Error) => {
reject({
code: Status.INTERNAL,
details: 'Failed to decompress gzip-encoded message'
});
});
decompresser.on('data', (chunk: Buffer) => {
messageParts.push(chunk);
totalLength += chunk.byteLength;
Expand Down
58 changes: 42 additions & 16 deletions packages/grpc-js/src/retrying-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -760,11 +760,10 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
this.maybeStartHedgingTimer();
}

private handleChildWriteCompleted(childIndex: number) {
const childCall = this.underlyingCalls[childIndex];
const messageIndex = childCall.nextMessageToSend;
private handleChildWriteCompleted(childIndex: number, messageIndex: number) {
this.getBufferEntry(messageIndex).callback?.();
this.clearSentMessages();
const childCall = this.underlyingCalls[childIndex];
childCall.nextMessageToSend += 1;
this.sendNextChildMessage(childIndex);
}
Expand All @@ -774,19 +773,33 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
if (childCall.state === 'COMPLETED') {
return;
}
if (this.getBufferEntry(childCall.nextMessageToSend)) {
const bufferEntry = this.getBufferEntry(childCall.nextMessageToSend);
const messageIndex = childCall.nextMessageToSend;
if (this.getBufferEntry(messageIndex)) {
const bufferEntry = this.getBufferEntry(messageIndex);
switch (bufferEntry.entryType) {
case 'MESSAGE':
childCall.call.sendMessageWithContext(
{
callback: error => {
// Ignore error
this.handleChildWriteCompleted(childIndex);
this.handleChildWriteCompleted(childIndex, messageIndex);
},
},
bufferEntry.message!.message
);
// Optimization: if the next entry is HALF_CLOSE, send it immediately
// without waiting for the message callback. This is safe because the message
// has already been passed to the underlying transport.
const nextEntry = this.getBufferEntry(messageIndex + 1);
if (nextEntry.entryType === 'HALF_CLOSE') {
this.trace(
'Sending halfClose immediately after message to child [' +
childCall.call.getCallNumber() +
'] - optimizing for unary/final message'
);
childCall.nextMessageToSend += 1;
childCall.call.halfClose();
}
break;
case 'HALF_CLOSE':
childCall.nextMessageToSend += 1;
Expand All @@ -813,7 +826,11 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
};
this.writeBuffer.push(bufferEntry);
if (bufferEntry.allocated) {
context.callback?.();
// Run this in next tick to avoid suspending the current execution context
// otherwise it might cause half closing the call before sending message
process.nextTick(() => {
context.callback?.();
});
for (const [callIndex, call] of this.underlyingCalls.entries()) {
if (
call.state === 'ACTIVE' &&
Expand All @@ -823,7 +840,7 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
{
callback: error => {
// Ignore error
this.handleChildWriteCompleted(callIndex);
this.handleChildWriteCompleted(callIndex, messageIndex);
},
},
message
Expand All @@ -843,7 +860,7 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
{
callback: error => {
// Ignore error
this.handleChildWriteCompleted(this.committedCallIndex!);
this.handleChildWriteCompleted(this.committedCallIndex!, messageIndex);
},
},
message
Expand All @@ -868,12 +885,21 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
allocated: false,
});
for (const call of this.underlyingCalls) {
if (
call?.state === 'ACTIVE' &&
call.nextMessageToSend === halfCloseIndex
) {
call.nextMessageToSend += 1;
call.call.halfClose();
if (call?.state === 'ACTIVE') {
// Send halfClose to call when either:
// - nextMessageToSend === halfCloseIndex - 1: last message sent, callback pending (optimization)
// - nextMessageToSend === halfCloseIndex: all messages sent and acknowledged
if (call.nextMessageToSend === halfCloseIndex
|| call.nextMessageToSend === halfCloseIndex - 1) {
this.trace(
'Sending halfClose immediately to child [' +
call.call.getCallNumber() +
'] - all messages already sent'
);
call.nextMessageToSend += 1;
call.call.halfClose();
}
// Otherwise, halfClose will be sent by sendNextChildMessage when message callbacks complete
}
}
}
Expand All @@ -895,4 +921,4 @@ export class RetryingCall implements Call, DeadlineInfoProvider {
return null;
}
}
}
}
Loading
Loading