diff --git a/packages/grpc-js-xds/package.json b/packages/grpc-js-xds/package.json index b5321bd02..f6cb72352 100644 --- a/packages/grpc-js-xds/package.json +++ b/packages/grpc-js-xds/package.json @@ -37,7 +37,7 @@ "@types/gulp": "^4.0.6", "@types/gulp-mocha": "0.0.32", "@types/mocha": "^5.2.6", - "@types/node": "^13.11.1", + "@types/node": ">=20.11.20", "@types/yargs": "^15.0.5", "find-free-ports": "^3.1.1", "gts": "^5.0.1", diff --git a/packages/grpc-js-xds/src/load-balancer-priority.ts b/packages/grpc-js-xds/src/load-balancer-priority.ts index c70c606a2..d1a996982 100644 --- a/packages/grpc-js-xds/src/load-balancer-priority.ts +++ b/packages/grpc-js-xds/src/load-balancer-priority.ts @@ -184,8 +184,8 @@ export class PriorityLoadBalancer implements LoadBalancer { private connectivityState: ConnectivityState = ConnectivityState.IDLE; private picker: Picker; private childBalancer: ChildLoadBalancerHandler; - private failoverTimer: NodeJS.Timer | null = null; - private deactivationTimer: NodeJS.Timer | null = null; + private failoverTimer: NodeJS.Timeout | null = null; + private deactivationTimer: NodeJS.Timeout | null = null; private seenReadyOrIdleSinceTransientFailure = false; constructor(private parent: PriorityLoadBalancer, private name: string, ignoreReresolutionRequests: boolean) { this.childBalancer = new ChildLoadBalancerHandler(experimental.createChildChannelControlHelper(this.parent.channelControlHelper, { diff --git a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts index 8d26538e7..9428c7d23 100644 --- a/packages/grpc-js-xds/src/load-balancer-weighted-target.ts +++ b/packages/grpc-js-xds/src/load-balancer-weighted-target.ts @@ -170,7 +170,7 @@ export class WeightedTargetLoadBalancer implements LoadBalancer { private connectivityState: ConnectivityState = ConnectivityState.IDLE; private picker: Picker; private childBalancer: ChildLoadBalancerHandler; - private deactivationTimer: NodeJS.Timer | null = null; + private deactivationTimer: NodeJS.Timeout | null = null; private weight: number = 0; constructor(private parent: WeightedTargetLoadBalancer, private name: string) { diff --git a/packages/grpc-js-xds/src/server.ts b/packages/grpc-js-xds/src/server.ts index 7c04122eb..d1bd2b5a1 100644 --- a/packages/grpc-js-xds/src/server.ts +++ b/packages/grpc-js-xds/src/server.ts @@ -460,7 +460,7 @@ interface MatchFieldEvaluator { isMoreSpecific: (matcher1: MatcherType, matcher2: MatcherType) => boolean; } -type FieldType = MatcherType extends CidrRange ? (string | undefined) : MatcherType extends (ConnectionSourceType) ? {localAddress: string, remoteAddress?: (string | undefined)} : MatcherType extends number ? number | undefined : never; +type FieldType = MatcherType extends CidrRange ? (string | undefined) : MatcherType extends (ConnectionSourceType) ? {localAddress?: (string | undefined), remoteAddress?: (string | undefined)} : MatcherType extends number ? number | undefined : never; function cidrRangeMatch(range: CidrRange | undefined, address: string | undefined): boolean { return !range || (!!address && inCidrRange(range, address)); @@ -473,14 +473,14 @@ function cidrRangeMoreSpecific(range1: CidrRange | undefined, range2: CidrRange return !!range1 && range1.prefixLen > range2.prefixLen; } -function sourceTypeMatch(sourceType: ConnectionSourceType, addresses: {localAddress: string, remoteAddress?: (string | undefined)}): boolean { +function sourceTypeMatch(sourceType: ConnectionSourceType, addresses: {localAddress?: (string | undefined), remoteAddress?: (string | undefined)}): boolean { switch (sourceType) { case "ANY": return true; case "SAME_IP_OR_LOOPBACK": - return !!addresses.remoteAddress && isSameIpOrLoopback(addresses.remoteAddress, addresses.localAddress); + return !!addresses.localAddress && !!addresses.remoteAddress && isSameIpOrLoopback(addresses.remoteAddress, addresses.localAddress); case "EXTERNAL": - return !!addresses.remoteAddress && !isSameIpOrLoopback(addresses.remoteAddress, addresses.localAddress); + return !!addresses.localAddress && !!addresses.remoteAddress && !isSameIpOrLoopback(addresses.remoteAddress, addresses.localAddress); } } @@ -490,7 +490,7 @@ const cidrRangeEvaluator: MatchFieldEvaluator = { +const sourceTypeEvaluator: MatchFieldEvaluator = { isMatch: sourceTypeMatch, matcherEqual: (matcher1, matcher2) => matcher1 === matcher2, isMoreSpecific: (matcher1, matcher2) => matcher1 !== 'ANY' && matcher2 === 'ANY' diff --git a/packages/grpc-js-xds/src/xds-client.ts b/packages/grpc-js-xds/src/xds-client.ts index f3df183ff..0879d6b27 100644 --- a/packages/grpc-js-xds/src/xds-client.ts +++ b/packages/grpc-js-xds/src/xds-client.ts @@ -104,7 +104,7 @@ export class Watcher implements ResourceWatcherInterface { const RESOURCE_TIMEOUT_MS = 15_000; class ResourceTimer { - private timer: NodeJS.Timer | null = null; + private timer: NodeJS.Timeout | null = null; private resourceSeen = false; constructor(private callState: AdsCallState, private type: XdsResourceType, private name: XdsResourceName) {} @@ -672,7 +672,7 @@ class ClusterLoadReportMap { } class LrsCallState { - private statsTimer: NodeJS.Timer | null = null; + private statsTimer: NodeJS.Timeout | null = null; private sentInitialMessage = false; constructor(private client: XdsSingleServerClient, private call: LrsCall, private node: Node) { call.on('data', (message: LoadStatsResponse__Output) => { diff --git a/packages/grpc-js-xds/test/client.ts b/packages/grpc-js-xds/test/client.ts index a09056c96..e11b65e2e 100644 --- a/packages/grpc-js-xds/test/client.ts +++ b/packages/grpc-js-xds/test/client.ts @@ -42,7 +42,7 @@ const BOOTSTRAP_CONFIG_KEY = 'grpc.TEST_ONLY_DO_NOT_USE_IN_PROD.xds_bootstrap_co export class XdsTestClient { private client: EchoTestServiceClient; - private callInterval: NodeJS.Timer; + private callInterval: NodeJS.Timeout; constructor(target: string, bootstrapInfo: string, options?: ChannelOptions) { this.client = new loadedProtos.grpc.testing.EchoTestService(target, credentials.createInsecure(), {...options, [BOOTSTRAP_CONFIG_KEY]: bootstrapInfo}); diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index ffa8539c7..b11495c6f 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,6 +1,6 @@ { "name": "@grpc/grpc-js", - "version": "1.12.1", + "version": "1.12.5", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", diff --git a/packages/grpc-js/src/certificate-provider.ts b/packages/grpc-js/src/certificate-provider.ts index e8bcaed8f..60d4cfa3f 100644 --- a/packages/grpc-js/src/certificate-provider.ts +++ b/packages/grpc-js/src/certificate-provider.ts @@ -15,9 +15,10 @@ * */ -import * as fs from 'fs/promises'; +import * as fs from 'fs'; import * as logging from './logging'; import { LogVerbosity } from './constants'; +import { promisify } from 'util'; const TRACER_NAME = 'certificate_provider'; @@ -56,6 +57,8 @@ export interface FileWatcherCertificateProviderConfig { refreshIntervalMs: number; } +const readFilePromise = promisify(fs.readFile); + export class FileWatcherCertificateProvider implements CertificateProvider { private refreshTimer: NodeJS.Timeout | null = null; private fileResultPromise: Promise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]> | null = null; @@ -82,9 +85,9 @@ export class FileWatcherCertificateProvider implements CertificateProvider { return; } this.fileResultPromise = Promise.allSettled([ - this.config.certificateFile ? fs.readFile(this.config.certificateFile) : Promise.reject(), - this.config.privateKeyFile ? fs.readFile(this.config.privateKeyFile) : Promise.reject(), - this.config.caCertificateFile ? fs.readFile(this.config.caCertificateFile) : Promise.reject() + this.config.certificateFile ? readFilePromise(this.config.certificateFile) : Promise.reject(), + this.config.privateKeyFile ? readFilePromise(this.config.privateKeyFile) : Promise.reject(), + this.config.caCertificateFile ? readFilePromise(this.config.caCertificateFile) : Promise.reject() ]); this.fileResultPromise.then(([certificateResult, privateKeyResult, caCertificateResult]) => { if (!this.refreshTimer) { diff --git a/packages/grpc-js/src/subchannel-call.ts b/packages/grpc-js/src/subchannel-call.ts index bee00119f..d2b5f076a 100644 --- a/packages/grpc-js/src/subchannel-call.ts +++ b/packages/grpc-js/src/subchannel-call.ts @@ -140,6 +140,8 @@ export class Http2SubchannelCall implements SubchannelCall { private serverEndedCall = false; + private connectionDropped = false; + constructor( private readonly http2Stream: http2.ClientHttp2Stream, private readonly callEventTracker: CallEventTracker, @@ -188,7 +190,22 @@ export class Http2SubchannelCall implements SubchannelCall { try { messages = this.decoder.write(data); } catch (e) { - this.cancelWithStatus(Status.RESOURCE_EXHAUSTED, (e as Error).message); + /* Some servers send HTML error pages along with HTTP status codes. + * When the client attempts to parse this as a length-delimited + * message, the parsed message size is greater than the default limit, + * resulting in a message decoding error. In that situation, the HTTP + * error code information is more useful to the user than the + * RESOURCE_EXHAUSTED error is, so we report that instead. Normally, + * we delay processing the HTTP status until after the stream ends, to + * prioritize reporting the gRPC status from trailers if it is present, + * but when there is a message parsing error we end the stream early + * before processing trailers. */ + if (this.httpStatusCode !== undefined && this.httpStatusCode !== 200) { + const mappedStatus = mapHttpStatusCode(this.httpStatusCode); + this.cancelWithStatus(mappedStatus.code, mappedStatus.details); + } else { + this.cancelWithStatus(Status.RESOURCE_EXHAUSTED, (e as Error).message); + } return; } @@ -240,8 +257,16 @@ export class Http2SubchannelCall implements SubchannelCall { details = 'Stream refused by server'; break; case http2.constants.NGHTTP2_CANCEL: - code = Status.CANCELLED; - details = 'Call cancelled'; + /* Bug reports indicate that Node synthesizes a NGHTTP2_CANCEL + * code from connection drops. We want to prioritize reporting + * an unavailable status when that happens. */ + if (this.connectionDropped) { + code = Status.UNAVAILABLE; + details = 'Connection dropped'; + } else { + code = Status.CANCELLED; + details = 'Call cancelled'; + } break; case http2.constants.NGHTTP2_ENHANCE_YOUR_CALM: code = Status.RESOURCE_EXHAUSTED; @@ -321,10 +346,15 @@ export class Http2SubchannelCall implements SubchannelCall { } public onDisconnect() { - this.endCall({ - code: Status.UNAVAILABLE, - details: 'Connection dropped', - metadata: new Metadata(), + this.connectionDropped = true; + /* Give the call an event loop cycle to finish naturally before reporting + * the disconnection as an error. */ + setImmediate(() => { + this.endCall({ + code: Status.UNAVAILABLE, + details: 'Connection dropped', + metadata: new Metadata(), + }); }); } diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 063fc86d9..807705c49 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -227,6 +227,11 @@ class Http2Transport implements Transport { this.handleDisconnect(); }); + session.socket.once('close', () => { + this.trace('connection closed'); + this.handleDisconnect(); + }); + if (logging.isTracerEnabled(TRACER_NAME)) { session.on('remoteSettings', (settings: http2.Settings) => { this.trace( @@ -382,17 +387,13 @@ class Http2Transport implements Transport { * Handle connection drops, but not GOAWAYs. */ private handleDisconnect() { - if (this.disconnectHandled) { - return; - } this.clearKeepaliveTimeout(); this.reportDisconnectToOwner(false); - /* Give calls an event loop cycle to finish naturally before reporting the - * disconnnection to them. */ + for (const call of this.activeCalls) { + call.onDisconnect(); + } + // Wait an event loop cycle before destroying the connection setImmediate(() => { - for (const call of this.activeCalls) { - call.onDisconnect(); - } this.session.destroy(); }); } diff --git a/packages/grpc-js/test/fixtures/ca.pem b/packages/grpc-js/test/fixtures/ca.pem index 6c8511a73..9eb75fa0f 100644 --- a/packages/grpc-js/test/fixtures/ca.pem +++ b/packages/grpc-js/test/fixtures/ca.pem @@ -1,15 +1,17 @@ -----BEGIN CERTIFICATE----- -MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla -Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 -YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT -BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 -+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu -g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd -Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV -HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau -sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m -oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG -Dfcog5wrJytaQ6UA0wE= +MIICzDCCAjWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4 +YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50ZXN0Lmdvb2dsZS5jb20wHhcNMjQxMTE0 +MDEzMjM3WhcNNDQxMTA5MDEzMjM3WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMI +SWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENv +LjEaMBgGA1UEAxQRKi50ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBAOHDFScoLCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlu +mN+fm+AjPEK5GHhGn1BgzkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wH +NVX77fBZOgp9VlSMVfyd9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGj +gYswgYgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwTwYDVR0RBEgwRoIQKi50ZXN0 +Lmdvb2dsZS5mcoIYd2F0ZXJ6b29pLnRlc3QuZ29vZ2xlLmJlghIqLnRlc3QueW91 +dHViZS5jb22HBMCoAQMwHQYDVR0OBBYEFI50QV/hcKkD2qyS/Melvq16+zY8MA0G +CSqGSIb3DQEBCwUAA4GBAIxvZ4kg9HAvzM31p4J04h1MFfnDx8O4Hwogzpaqk+0M +qz8L1ojJy5jus1g8+RBguT1rv5TmRFpMWrp50XQ0bMFHoOcNyL0htxoOhmoKHoX9 +dM0KWtgUheeBcEm83UzFFlGKlna22+pdUHLEnuX+i25s+Lbi4/LGf6KwUlgPL/Vk -----END CERTIFICATE----- diff --git a/packages/grpc-js/test/fixtures/server1.pem b/packages/grpc-js/test/fixtures/server1.pem index f3d43fcc5..9eb75fa0f 100644 --- a/packages/grpc-js/test/fixtures/server1.pem +++ b/packages/grpc-js/test/fixtures/server1.pem @@ -1,16 +1,17 @@ -----BEGIN CERTIFICATE----- -MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET -MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ -dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx -MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV -BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 -ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco -LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg -zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd -9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw -CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy -em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G -CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 -hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh -y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 +MIICzDCCAjWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzER +MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4 +YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50ZXN0Lmdvb2dsZS5jb20wHhcNMjQxMTE0 +MDEzMjM3WhcNNDQxMTA5MDEzMjM3WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMI +SWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENv +LjEaMBgGA1UEAxQRKi50ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD +gY0AMIGJAoGBAOHDFScoLCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlu +mN+fm+AjPEK5GHhGn1BgzkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wH +NVX77fBZOgp9VlSMVfyd9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGj +gYswgYgwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwTwYDVR0RBEgwRoIQKi50ZXN0 +Lmdvb2dsZS5mcoIYd2F0ZXJ6b29pLnRlc3QuZ29vZ2xlLmJlghIqLnRlc3QueW91 +dHViZS5jb22HBMCoAQMwHQYDVR0OBBYEFI50QV/hcKkD2qyS/Melvq16+zY8MA0G +CSqGSIb3DQEBCwUAA4GBAIxvZ4kg9HAvzM31p4J04h1MFfnDx8O4Hwogzpaqk+0M +qz8L1ojJy5jus1g8+RBguT1rv5TmRFpMWrp50XQ0bMFHoOcNyL0htxoOhmoKHoX9 +dM0KWtgUheeBcEm83UzFFlGKlna22+pdUHLEnuX+i25s+Lbi4/LGf6KwUlgPL/Vk -----END CERTIFICATE-----