@connectrpc/connect-node
Version:
Connect is a family of libraries for building and consuming APIs on different languages and platforms, and [@connectrpc/connect](https://www.npmjs.com/package/@connectrpc/connect) brings type-safe APIs with Protobuf to TypeScript.
155 lines (154 loc) • 7.12 kB
JavaScript
// Copyright 2021-2025 The Connect Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.H2Code = void 0;
exports.connectErrorFromNodeReason = connectErrorFromNodeReason;
exports.unwrapNodeErrorChain = unwrapNodeErrorChain;
exports.getNodeErrorProps = getNodeErrorProps;
exports.connectErrorFromH2ResetCode = connectErrorFromH2ResetCode;
const connect_1 = require("@connectrpc/connect");
/**
* Similar to ConnectError.from(), this function turns any value into
* a ConnectError, but special cases some Node.js specific error codes and
* sets an appropriate Connect error code.
*/
function connectErrorFromNodeReason(reason) {
let code = connect_1.Code.Internal;
const chain = unwrapNodeErrorChain(reason).map(getNodeErrorProps);
if (chain.some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
// We do not want intentional errors from the server to be shadowed
// by client-side errors.
// This can occur if the server has written a response with an error
// and has ended the connection. This response may already sit in a
// buffer on the client, while it is still writing to the request
// body.
// To avoid this problem, we wrap ERR_STREAM_WRITE_AFTER_END as a
// ConnectError with Code.Aborted. The special meaning of this code
// in this situation is documented in StreamingConn.send() and in
// createServerStreamingFn().
code = connect_1.Code.Aborted;
}
else if (chain.some((p) => p.code == "ERR_STREAM_DESTROYED" ||
p.code == "ERR_HTTP2_INVALID_STREAM" ||
p.code == "ECONNRESET")) {
// A handler whose stream is suddenly destroyed usually means the client
// hung up. This behavior is triggered by the conformance test "cancel_after_begin".
code = connect_1.Code.Aborted;
}
else if (chain.some((p) => p.code == "ETIMEDOUT" ||
p.code == "ENOTFOUND" ||
p.code == "EAI_AGAIN" ||
p.code == "ECONNREFUSED")) {
// Calling an unresolvable host should raise a ConnectError with
// Code.Aborted.
// This behavior is covered by the conformance test "unresolvable_host".
code = connect_1.Code.Unavailable;
}
const ce = connect_1.ConnectError.from(reason, code);
if (ce !== reason) {
ce.cause = reason;
}
return ce;
}
/**
* Unwraps a chain of errors, by walking through all "cause" properties
* recursively.
* This function is useful to find the root cause of an error.
*/
function unwrapNodeErrorChain(reason) {
const chain = [];
for (;;) {
if (!(reason instanceof Error)) {
break;
}
if (chain.includes(reason)) {
// safeguard against infinite loop when "cause" points to an ancestor
break;
}
chain.push(reason);
if (!("cause" in reason)) {
break;
}
reason = reason.cause;
}
return chain;
}
/**
* Returns standard Node.js error properties from the given reason, if present.
*
* For context: Every error raised by Node.js APIs should expose a `code`
* property - a string that permanently identifies the error. Some errors may
* have an additional `syscall` property - a string that identifies native
* components, for example "getaddrinfo" of libuv.
* For more information, see https://github.com/nodejs/node/blob/f6052c68c1f9a4400a723e9c0b79da14197ab754/lib/internal/errors.js
*/
function getNodeErrorProps(reason) {
const props = {};
if (reason instanceof Error) {
if ("code" in reason && typeof reason.code == "string") {
props.code = reason.code;
}
if ("syscall" in reason && typeof reason.syscall == "string") {
props.syscall = reason.syscall;
}
}
return props;
}
/**
* Returns a ConnectError for an HTTP/2 error code.
*/
function connectErrorFromH2ResetCode(rstCode) {
switch (rstCode) {
case H2Code.PROTOCOL_ERROR:
case H2Code.INTERNAL_ERROR:
case H2Code.FLOW_CONTROL_ERROR:
case H2Code.SETTINGS_TIMEOUT:
case H2Code.FRAME_SIZE_ERROR:
case H2Code.COMPRESSION_ERROR:
case H2Code.CONNECT_ERROR:
return new connect_1.ConnectError(`http/2 stream closed with error code ${H2Code[rstCode]} (0x${rstCode.toString(16)})`, connect_1.Code.Internal);
case H2Code.REFUSED_STREAM:
return new connect_1.ConnectError(`http/2 stream closed with error code ${H2Code[rstCode]} (0x${rstCode.toString(16)})`, connect_1.Code.Unavailable);
case H2Code.CANCEL:
return new connect_1.ConnectError(`http/2 stream closed with error code ${H2Code[rstCode]} (0x${rstCode.toString(16)})`, connect_1.Code.Canceled);
case H2Code.ENHANCE_YOUR_CALM:
return new connect_1.ConnectError(`http/2 stream closed with error code ${H2Code[rstCode]} (0x${rstCode.toString(16)})`, connect_1.Code.ResourceExhausted);
case H2Code.INADEQUATE_SECURITY:
return new connect_1.ConnectError(`http/2 stream closed with error code ${H2Code[rstCode]} (0x${rstCode.toString(16)})`, connect_1.Code.PermissionDenied);
case H2Code.HTTP_1_1_REQUIRED:
return new connect_1.ConnectError(`http/2 stream closed with error code ${H2Code[rstCode]} (0x${rstCode.toString(16)})`, connect_1.Code.PermissionDenied);
case H2Code.STREAM_CLOSED:
default:
// Intentionally not mapping STREAM_CLOSED (0x5), see https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#errors
break;
}
return undefined;
}
var H2Code;
(function (H2Code) {
H2Code[H2Code["PROTOCOL_ERROR"] = 1] = "PROTOCOL_ERROR";
H2Code[H2Code["INTERNAL_ERROR"] = 2] = "INTERNAL_ERROR";
H2Code[H2Code["FLOW_CONTROL_ERROR"] = 3] = "FLOW_CONTROL_ERROR";
H2Code[H2Code["SETTINGS_TIMEOUT"] = 4] = "SETTINGS_TIMEOUT";
H2Code[H2Code["STREAM_CLOSED"] = 5] = "STREAM_CLOSED";
H2Code[H2Code["FRAME_SIZE_ERROR"] = 6] = "FRAME_SIZE_ERROR";
H2Code[H2Code["REFUSED_STREAM"] = 7] = "REFUSED_STREAM";
H2Code[H2Code["CANCEL"] = 8] = "CANCEL";
H2Code[H2Code["COMPRESSION_ERROR"] = 9] = "COMPRESSION_ERROR";
H2Code[H2Code["CONNECT_ERROR"] = 10] = "CONNECT_ERROR";
H2Code[H2Code["ENHANCE_YOUR_CALM"] = 11] = "ENHANCE_YOUR_CALM";
H2Code[H2Code["INADEQUATE_SECURITY"] = 12] = "INADEQUATE_SECURITY";
H2Code[H2Code["HTTP_1_1_REQUIRED"] = 13] = "HTTP_1_1_REQUIRED";
})(H2Code || (exports.H2Code = H2Code = {}));
;