mirror of https://github.com/grpc/grpc-node.git
Merge pull request #1658 from murgatroid99/grpc-js_internal_error_reporting
grpc-js: Propagate internal stream errors from the http2 module
This commit is contained in:
commit
7fc29e7867
|
@ -37,6 +37,10 @@ const {
|
||||||
NGHTTP2_CANCEL,
|
NGHTTP2_CANCEL,
|
||||||
} = http2.constants;
|
} = http2.constants;
|
||||||
|
|
||||||
|
interface NodeError extends Error {
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type Deadline = Date | number;
|
export type Deadline = Date | number;
|
||||||
|
|
||||||
export interface CallStreamOptions {
|
export interface CallStreamOptions {
|
||||||
|
@ -202,6 +206,8 @@ export class Http2CallStream implements Call {
|
||||||
|
|
||||||
private listener: InterceptingListener | null = null;
|
private listener: InterceptingListener | null = null;
|
||||||
|
|
||||||
|
private internalErrorMessage: string | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly methodName: string,
|
private readonly methodName: string,
|
||||||
private readonly channel: ChannelImplementation,
|
private readonly channel: ChannelImplementation,
|
||||||
|
@ -518,66 +524,86 @@ export class Http2CallStream implements Call {
|
||||||
this.maybeOutputStatus();
|
this.maybeOutputStatus();
|
||||||
});
|
});
|
||||||
stream.on('close', () => {
|
stream.on('close', () => {
|
||||||
this.trace('HTTP/2 stream closed with code ' + stream.rstCode);
|
/* Use process.next tick to ensure that this code happens after any
|
||||||
/* If we have a final status with an OK status code, that means that
|
* "error" event that may be emitted at about the same time, so that
|
||||||
* we have received all of the messages and we have processed the
|
* we can bubble up the error message from that event. */
|
||||||
* trailers and the call completed successfully, so it doesn't matter
|
process.nextTick(() => {
|
||||||
* how the stream ends after that */
|
this.trace('HTTP/2 stream closed with code ' + stream.rstCode);
|
||||||
if (this.finalStatus?.code === Status.OK) {
|
/* If we have a final status with an OK status code, that means that
|
||||||
return;
|
* we have received all of the messages and we have processed the
|
||||||
}
|
* trailers and the call completed successfully, so it doesn't matter
|
||||||
let code: Status;
|
* how the stream ends after that */
|
||||||
let details = '';
|
if (this.finalStatus?.code === Status.OK) {
|
||||||
switch (stream.rstCode) {
|
return;
|
||||||
case http2.constants.NGHTTP2_NO_ERROR:
|
}
|
||||||
/* If we get a NO_ERROR code and we already have a status, the
|
let code: Status;
|
||||||
* stream completed properly and we just haven't fully processed
|
let details = '';
|
||||||
* it yet */
|
switch (stream.rstCode) {
|
||||||
if (this.finalStatus !== null) {
|
case http2.constants.NGHTTP2_NO_ERROR:
|
||||||
return;
|
/* If we get a NO_ERROR code and we already have a status, the
|
||||||
}
|
* stream completed properly and we just haven't fully processed
|
||||||
code = Status.INTERNAL;
|
* it yet */
|
||||||
details = `Received RST_STREAM with code ${stream.rstCode}`;
|
if (this.finalStatus !== null) {
|
||||||
break;
|
return;
|
||||||
case http2.constants.NGHTTP2_REFUSED_STREAM:
|
}
|
||||||
code = Status.UNAVAILABLE;
|
code = Status.INTERNAL;
|
||||||
details = 'Stream refused by server';
|
details = `Received RST_STREAM with code ${stream.rstCode}`;
|
||||||
break;
|
break;
|
||||||
case http2.constants.NGHTTP2_CANCEL:
|
case http2.constants.NGHTTP2_REFUSED_STREAM:
|
||||||
code = Status.CANCELLED;
|
code = Status.UNAVAILABLE;
|
||||||
details = 'Call cancelled';
|
details = 'Stream refused by server';
|
||||||
break;
|
break;
|
||||||
case http2.constants.NGHTTP2_ENHANCE_YOUR_CALM:
|
case http2.constants.NGHTTP2_CANCEL:
|
||||||
code = Status.RESOURCE_EXHAUSTED;
|
code = Status.CANCELLED;
|
||||||
details = 'Bandwidth exhausted';
|
details = 'Call cancelled';
|
||||||
break;
|
break;
|
||||||
case http2.constants.NGHTTP2_INADEQUATE_SECURITY:
|
case http2.constants.NGHTTP2_ENHANCE_YOUR_CALM:
|
||||||
code = Status.PERMISSION_DENIED;
|
code = Status.RESOURCE_EXHAUSTED;
|
||||||
details = 'Protocol not secure enough';
|
details = 'Bandwidth exhausted';
|
||||||
break;
|
break;
|
||||||
case http2.constants.NGHTTP2_INTERNAL_ERROR:
|
case http2.constants.NGHTTP2_INADEQUATE_SECURITY:
|
||||||
code = Status.INTERNAL;
|
code = Status.PERMISSION_DENIED;
|
||||||
/* This error code was previously handled in the default case, and
|
details = 'Protocol not secure enough';
|
||||||
* there are several instances of it online, so I wanted to
|
break;
|
||||||
* preserve the original error message so that people find existing
|
case http2.constants.NGHTTP2_INTERNAL_ERROR:
|
||||||
* information in searches, but also include the more recognizable
|
code = Status.INTERNAL;
|
||||||
* "Internal server error" message. */
|
if (this.internalErrorMessage === null) {
|
||||||
details = `Received RST_STREAM with code ${stream.rstCode} (Internal server error)`;
|
/* This error code was previously handled in the default case, and
|
||||||
break;
|
* there are several instances of it online, so I wanted to
|
||||||
default:
|
* preserve the original error message so that people find existing
|
||||||
code = Status.INTERNAL;
|
* information in searches, but also include the more recognizable
|
||||||
details = `Received RST_STREAM with code ${stream.rstCode}`;
|
* "Internal server error" message. */
|
||||||
}
|
details = `Received RST_STREAM with code ${stream.rstCode} (Internal server error)`;
|
||||||
// This is a no-op if trailers were received at all.
|
} else {
|
||||||
// This is OK, because status codes emitted here correspond to more
|
/* The "Received RST_STREAM with code ..." error is preserved
|
||||||
// catastrophic issues that prevent us from receiving trailers in the
|
* here for continuity with errors reported online, but the
|
||||||
// first place.
|
* error message at the end will probably be more relevant in
|
||||||
this.endCall({ code, details, metadata: new Metadata() });
|
* most cases. */
|
||||||
|
details = `Received RST_STREAM with code ${stream.rstCode} triggered by internal client error: ${this.internalErrorMessage}`;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
code = Status.INTERNAL;
|
||||||
|
details = `Received RST_STREAM with code ${stream.rstCode}`;
|
||||||
|
}
|
||||||
|
// This is a no-op if trailers were received at all.
|
||||||
|
// This is OK, because status codes emitted here correspond to more
|
||||||
|
// catastrophic issues that prevent us from receiving trailers in the
|
||||||
|
// first place.
|
||||||
|
this.endCall({ code, details, metadata: new Metadata() });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
stream.on('error', (err: Error) => {
|
stream.on('error', (err: NodeError) => {
|
||||||
/* We need an error handler here to stop "Uncaught Error" exceptions
|
/* We need an error handler here to stop "Uncaught Error" exceptions
|
||||||
* from bubbling up. However, errors here should all correspond to
|
* from bubbling up. However, errors here should all correspond to
|
||||||
* "close" events, where we will handle the error more granularly */
|
* "close" events, where we will handle the error more granularly */
|
||||||
|
/* Specifically looking for stream errors that were *not* constructed
|
||||||
|
* from a RST_STREAM response here:
|
||||||
|
* https://github.com/nodejs/node/blob/8b8620d580314050175983402dfddf2674e8e22a/lib/internal/http2/core.js#L2267
|
||||||
|
*/
|
||||||
|
if (err.code !== 'ERR_HTTP2_STREAM_ERROR') {
|
||||||
|
this.internalErrorMessage = err.message;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (!this.pendingRead) {
|
if (!this.pendingRead) {
|
||||||
stream.pause();
|
stream.pause();
|
||||||
|
|
Loading…
Reference in New Issue