diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index db021fe3..9f8abb10 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js-xds", - "version": "1.2.1", + "version": "1.2.4", "description": "Plugin for @grpc/grpc-js. Adds the xds:// URL scheme and associated features.", "main": "build/src/index.js", "scripts": { @@ -47,7 +47,7 @@ "re2-wasm": "^1.0.1" }, "peerDependencies": { - "@grpc/grpc-js": "~1.2.2" + "@grpc/grpc-js": "~1.2.10" }, "engines": { "node": ">=10.10.0" diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index f3ea9c03..22d816a0 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -311,9 +311,11 @@ export class XdsClient { this.adsBackoff = new BackoffTimeout(() => { this.maybeStartAdsStream(); }); + this.adsBackoff.unref(); this.lrsBackoff = new BackoffTimeout(() => { this.maybeStartLrsStream(); - }) + }); + this.lrsBackoff.unref(); Promise.all([loadBootstrapInfo(), loadAdsProtos()]).then( ([bootstrapInfo, protoDefinitions]) => { diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 5d8360e6..4c8cd9f9 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.2.5", + "version": "1.2.12", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", @@ -57,7 +57,7 @@ "posttest": "npm run check" }, "dependencies": { - "@types/node": "^12.12.47", + "@types/node": ">=12.12.47", "semver": "^6.2.0" }, "files": [ diff --git a/packages/grpc-js/src/backoff-timeout.ts b/packages/grpc-js/src/backoff-timeout.ts index 8cad14f7..7f2ab5eb 100644 --- a/packages/grpc-js/src/backoff-timeout.ts +++ b/packages/grpc-js/src/backoff-timeout.ts @@ -44,6 +44,7 @@ export class BackoffTimeout { private nextDelay: number; private timerId: NodeJS.Timer; private running = false; + private hasRef = true; constructor(private callback: () => void, options?: BackoffOptions) { if (options) { @@ -74,6 +75,9 @@ export class BackoffTimeout { this.callback(); this.running = false; }, this.nextDelay); + if (!this.hasRef) { + this.timerId.unref?.(); + } const nextBackoff = Math.min( this.nextDelay * this.multiplier, this.maxDelay @@ -102,4 +106,14 @@ export class BackoffTimeout { isRunning() { return this.running; } + + ref() { + this.hasRef = true; + this.timerId.ref?.(); + } + + unref() { + this.hasRef = false; + this.timerId.unref?.(); + } } diff --git a/packages/grpc-js/src/call-credentials-filter.ts b/packages/grpc-js/src/call-credentials-filter.ts index 5263d97f..53bdba2f 100644 --- a/packages/grpc-js/src/call-credentials-filter.ts +++ b/packages/grpc-js/src/call-credentials-filter.ts @@ -21,6 +21,7 @@ import { BaseFilter, Filter, FilterFactory } from './filter'; import { Metadata } from './metadata'; import { Status } from './constants'; import { splitHostPort } from './uri-parser'; +import { ServiceError } from './call'; export class CallCredentialsFilter extends BaseFilter implements Filter { private serviceUrl: string; @@ -51,12 +52,21 @@ export class CallCredentialsFilter extends BaseFilter implements Filter { service_url: this.serviceUrl, }); const resultMetadata = await metadata; - resultMetadata.merge(await credsMetadata); + try { + resultMetadata.merge(await credsMetadata); + } catch (error) { + this.stream.cancelWithStatus( + Status.UNAUTHENTICATED, + `Failed to retrieve auth metadata with error: ${error.message}` + ); + return Promise.reject('Failed to retrieve auth metadata'); + } if (resultMetadata.get('authorization').length > 1) { this.stream.cancelWithStatus( Status.INTERNAL, '"authorization" metadata cannot have multiple values' ); + return Promise.reject('"authorization" metadata cannot have multiple values'); } return resultMetadata; } diff --git a/packages/grpc-js/src/call-stream.ts b/packages/grpc-js/src/call-stream.ts index 7d0abb94..8fcc7065 100644 --- a/packages/grpc-js/src/call-stream.ts +++ b/packages/grpc-js/src/call-stream.ts @@ -16,6 +16,7 @@ */ import * as http2 from 'http2'; +import * as os from 'os'; import { CallCredentials } from './call-credentials'; import { Propagate, Status } from './constants'; @@ -37,8 +38,34 @@ const { NGHTTP2_CANCEL, } = http2.constants; -interface NodeError extends Error { +/** + * https://nodejs.org/api/errors.html#errors_class_systemerror + */ +interface SystemError extends Error { + address?: string; code: string; + dest?: string; + errno: number; + info?: object; + message: string; + path?: string; + port?: number; + syscall: string; +} + +/** + * Should do approximately the same thing as util.getSystemErrorName but the + * TypeScript types don't have that function for some reason so I just made my + * own. + * @param errno + */ +function getSystemErrorName(errno: number): string { + for (const [name, num] of Object.entries(os.constants.errno)) { + if (num === errno) { + return name; + } + } + return 'Unknown system error ' + errno; } export type Deadline = Date | number; @@ -206,7 +233,7 @@ export class Http2CallStream implements Call { private listener: InterceptingListener | null = null; - private internalErrorMessage: string | null = null; + private internalError: SystemError | null = null; constructor( private readonly methodName: string, @@ -554,7 +581,7 @@ export class Http2CallStream implements Call { break; case http2.constants.NGHTTP2_INTERNAL_ERROR: code = Status.INTERNAL; - if (this.internalErrorMessage === null) { + if (this.internalError === null) { /* This error code was previously handled in the default case, and * there are several instances of it online, so I wanted to * preserve the original error message so that people find existing @@ -562,11 +589,16 @@ export class Http2CallStream implements Call { * "Internal server error" message. */ details = `Received RST_STREAM with code ${stream.rstCode} (Internal server error)`; } else { - /* The "Received RST_STREAM with code ..." error is preserved - * here for continuity with errors reported online, but the - * error message at the end will probably be more relevant in - * most cases. */ - details = `Received RST_STREAM with code ${stream.rstCode} triggered by internal client error: ${this.internalErrorMessage}`; + if (this.internalError.errno === os.constants.errno.ECONNRESET) { + code = Status.UNAVAILABLE; + details = this.internalError.message; + } else { + /* The "Received RST_STREAM with code ..." error is preserved + * here for continuity with errors reported online, but the + * error message at the end will probably be more relevant in + * most cases. */ + details = `Received RST_STREAM with code ${stream.rstCode} triggered by internal client error: ${this.internalError.message}`; + } } break; default: @@ -580,7 +612,7 @@ export class Http2CallStream implements Call { this.endCall({ code, details, metadata: new Metadata() }); }); }); - stream.on('error', (err: NodeError) => { + stream.on('error', (err: SystemError) => { /* We need an error handler here to stop "Uncaught Error" exceptions * from bubbling up. However, errors here should all correspond to * "close" events, where we will handle the error more granularly */ @@ -589,7 +621,8 @@ export class Http2CallStream implements Call { * https://github.com/nodejs/node/blob/8b8620d580314050175983402dfddf2674e8e22a/lib/internal/http2/core.js#L2267 */ if (err.code !== 'ERR_HTTP2_STREAM_ERROR') { - this.internalErrorMessage = err.message; + this.trace('Node error event: message=' + err.message + ' code=' + err.code + ' errno=' + getSystemErrorName(err.errno) + ' syscall=' + err.syscall); + this.internalError = err; } }); if (!this.pendingRead) { diff --git a/packages/grpc-js/src/channel.ts b/packages/grpc-js/src/channel.ts index dad8a532..41715c41 100644 --- a/packages/grpc-js/src/channel.ts +++ b/packages/grpc-js/src/channel.ts @@ -419,7 +419,7 @@ export class ChannelImplementation implements Channel { ); callStream.cancelWithStatus( Status.INTERNAL, - 'Failed to start HTTP/2 stream' + `Failed to start HTTP/2 stream with error: ${(error as Error).message}` ); } } @@ -441,7 +441,7 @@ export class ChannelImplementation implements Channel { (error: Error & { code: number }) => { // We assume the error code isn't 0 (Status.OK) callStream.cancelWithStatus( - error.code || Status.UNKNOWN, + (typeof error.code === 'number') ? error.code : Status.UNKNOWN, `Getting metadata from plugin failed with error: ${error.message}` ); } @@ -559,6 +559,9 @@ export class ChannelImplementation implements Channel { deadline: Date | number, callback: (error?: Error) => void ): void { + if (this.connectivityState === ConnectivityState.SHUTDOWN) { + throw new Error('Channel has been shut down'); + } let timer = null; if(deadline !== Infinity) { const deadlineDate: Date = diff --git a/packages/grpc-js/src/compression-filter.ts b/packages/grpc-js/src/compression-filter.ts index 6f40ee4b..330eb675 100644 --- a/packages/grpc-js/src/compression-filter.ts +++ b/packages/grpc-js/src/compression-filter.ts @@ -170,7 +170,7 @@ export class CompressionFilter extends BaseFilter implements Filter { async sendMetadata(metadata: Promise): Promise { const headers: Metadata = await metadata; headers.set('grpc-accept-encoding', 'identity,deflate,gzip'); - headers.set('accept-encoding', 'identity,gzip'); + headers.set('accept-encoding', 'identity'); return headers; } diff --git a/packages/grpc-js/src/resolving-load-balancer.ts b/packages/grpc-js/src/resolving-load-balancer.ts index 84fe4ae1..94dd8c4a 100644 --- a/packages/grpc-js/src/resolving-load-balancer.ts +++ b/packages/grpc-js/src/resolving-load-balancer.ts @@ -235,6 +235,7 @@ export class ResolvingLoadBalancer implements LoadBalancer { this.updateState(this.latestChildState, this.latestChildPicker); } }); + this.backoffTimeout.unref(); } private updateResolution() { diff --git a/packages/grpc-js/src/subchannel.ts b/packages/grpc-js/src/subchannel.ts index a435e452..772bdb22 100644 --- a/packages/grpc-js/src/subchannel.ts +++ b/packages/grpc-js/src/subchannel.ts @@ -618,6 +618,7 @@ export class Subchannel { if (this.session) { this.session.ref(); } + this.backoffTimeout.ref(); if (!this.keepaliveWithoutCalls) { this.startKeepalivePings(); } @@ -638,6 +639,7 @@ export class Subchannel { if (this.session) { this.session.unref(); } + this.backoffTimeout.unref(); if (!this.keepaliveWithoutCalls) { this.stopKeepalivePings(); }