@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.
327 lines (326 loc) • 13.3 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.createNodeHttpClient = createNodeHttpClient;
const http = require("http");
const https = require("https");
const connect_1 = require("@connectrpc/connect");
const node_universal_header_js_1 = require("./node-universal-header.js");
const node_error_js_1 = require("./node-error.js");
const protocol_1 = require("@connectrpc/connect/protocol");
const http2_session_manager_js_1 = require("./http2-session-manager.js");
/**
* Create a universal client function, a minimal abstraction of an HTTP client,
* using the Node.js `http`, `https`, or `http2` module.
*
* @private Internal code, does not follow semantic versioning.
*/
function createNodeHttpClient(options) {
var _a;
if (options.httpVersion == "1.1") {
return createNodeHttp1Client(options.nodeOptions);
}
const sessionProvider = (_a = options.sessionProvider) !== null && _a !== void 0 ? _a : ((url) => new http2_session_manager_js_1.Http2SessionManager(url));
return createNodeHttp2Client(sessionProvider);
}
/**
* Create an HTTP client using the Node.js `http` or `https` package.
*
* The HTTP client is a simple function conforming to the type UniversalClientFn.
* It takes an UniversalClientRequest as an argument, and returns a promise for
* an UniversalClientResponse.
*/
function createNodeHttp1Client(httpOptions) {
return async function request(req) {
const sentinel = createSentinel(req.signal);
return new Promise((resolve, reject) => {
sentinel.catch((e) => {
reject(e);
});
h1Request(sentinel, req.url, Object.assign(Object.assign({}, httpOptions), { headers: (0, node_universal_header_js_1.webHeaderToNodeHeaders)(req.header, httpOptions === null || httpOptions === void 0 ? void 0 : httpOptions.headers), method: req.method }), (request) => {
void sinkRequest(req, request, sentinel);
request.on("response", (response) => {
var _a;
response.on("error", sentinel.reject);
sentinel.catch((reason) => response.destroy((0, node_error_js_1.connectErrorFromNodeReason)(reason)));
const trailer = new Headers();
resolve({
status: (_a = response.statusCode) !== null && _a !== void 0 ? _a : 0,
header: (0, node_universal_header_js_1.nodeHeaderToWebHeader)(response.headers),
body: h1ResponseIterable(sentinel, response, trailer),
trailer,
});
});
});
});
};
}
/**
* Create an HTTP client using the Node.js `http2` package.
*
* The HTTP client is a simple function conforming to the type UniversalClientFn.
* It takes an UniversalClientRequest as an argument, and returns a promise for
* an UniversalClientResponse.
*/
function createNodeHttp2Client(sessionProvider) {
return function request(req) {
const sentinel = createSentinel(req.signal);
const sessionManager = sessionProvider(req.url);
return new Promise((resolve, reject) => {
sentinel.catch((e) => {
reject(e);
});
h2Request(sentinel, sessionManager, req.url, req.method, (0, node_universal_header_js_1.webHeaderToNodeHeaders)(req.header), {}, (stream) => {
void sinkRequest(req, stream, sentinel);
stream.on("response", (headers) => {
var _a;
const response = {
status: (_a = headers[":status"]) !== null && _a !== void 0 ? _a : 0,
header: (0, node_universal_header_js_1.nodeHeaderToWebHeader)(headers),
body: h2ResponseIterable(sentinel, stream, sessionManager),
trailer: h2ResponseTrailer(stream),
};
resolve(response);
});
});
});
};
}
function h1Request(sentinel, url, options, onRequest) {
let request;
if (new URL(url).protocol.startsWith("https")) {
request = https.request(url, options);
}
else {
request = http.request(url, options);
}
sentinel.catch((reason) => request.destroy((0, node_error_js_1.connectErrorFromNodeReason)(reason)));
// Node.js will only send headers with the first request body byte by default.
// We force it to send headers right away for consistent behavior between
// HTTP/1.1 and HTTP/2.2 clients.
request.flushHeaders();
request.on("error", sentinel.reject);
request.on("socket", function onRequestSocket(socket) {
function onSocketConnect() {
socket.off("connect", onSocketConnect);
onRequest(request);
}
// If readyState is open, then socket is already open due to keepAlive, so
// the 'connect' event will never fire so call onRequest explicitly
if (socket.readyState === "open") {
onRequest(request);
}
else {
socket.on("connect", onSocketConnect);
}
});
}
function h1ResponseIterable(sentinel, response, trailer) {
const inner = response[Symbol.asyncIterator]();
return {
[Symbol.asyncIterator]() {
return {
async next() {
const r = await sentinel.race(inner.next());
if (r.done === true) {
(0, node_universal_header_js_1.nodeHeaderToWebHeader)(response.trailers).forEach((value, key) => {
trailer.set(key, value);
});
sentinel.resolve();
await sentinel;
}
return r;
},
throw(e) {
sentinel.reject(e);
throw e;
},
};
},
};
}
function h2Request(sentinel, sm, url, method, headers, options, onStream) {
const requestUrl = new URL(url);
if (requestUrl.origin !== sm.authority) {
const message = `cannot make a request to ${requestUrl.origin}: the http2 session is connected to ${sm.authority}`;
sentinel.reject(new connect_1.ConnectError(message, connect_1.Code.Internal));
return;
}
sm.request(method, requestUrl.pathname + requestUrl.search, headers, {}).then((stream) => {
sentinel.catch((reason) => {
if (stream.closed) {
return;
}
// Node.js http2 streams that are aborted via an AbortSignal close with
// an RST_STREAM with code INTERNAL_ERROR.
// To comply with the mapping between gRPC and HTTP/2 codes, we need to
// close with code CANCEL.
// See https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#errors
// See https://www.rfc-editor.org/rfc/rfc7540#section-7
const rstCode = reason instanceof connect_1.ConnectError && reason.code == connect_1.Code.Canceled
? node_error_js_1.H2Code.CANCEL
: node_error_js_1.H2Code.INTERNAL_ERROR;
return new Promise((resolve) => stream.close(rstCode, resolve));
});
stream.on("error", function h2StreamError(e) {
if (stream.writableEnded &&
(0, node_error_js_1.unwrapNodeErrorChain)(e)
.map(node_error_js_1.getNodeErrorProps)
.some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
return;
}
sentinel.reject(e);
});
stream.on("close", function h2StreamClose() {
const err = (0, node_error_js_1.connectErrorFromH2ResetCode)(stream.rstCode);
if (err) {
sentinel.reject(err);
}
});
onStream(stream);
}, (reason) => {
sentinel.reject(reason);
});
}
function h2ResponseTrailer(response) {
const trailer = new Headers();
response.on("trailers", (args) => {
(0, node_universal_header_js_1.nodeHeaderToWebHeader)(args).forEach((value, key) => {
trailer.set(key, value);
});
});
return trailer;
}
function h2ResponseIterable(sentinel, response, sm) {
const inner = response[Symbol.asyncIterator]();
return {
[Symbol.asyncIterator]() {
return {
async next() {
const r = await sentinel.race(inner.next());
if (r.done === true) {
sentinel.resolve();
await sentinel;
}
sm === null || sm === void 0 ? void 0 : sm.notifyResponseByteRead(response);
return r;
},
throw(e) {
sentinel.reject(e);
throw e;
},
};
},
};
}
async function sinkRequest(request, nodeRequest, sentinel) {
if (request.body === undefined) {
await new Promise((resolve) => nodeRequest.end(resolve));
return;
}
const it = request.body[Symbol.asyncIterator]();
return new Promise((resolve) => {
writeNext();
function writeNext() {
if (sentinel.isRejected()) {
return;
}
it.next().then((r) => {
if (r.done === true) {
nodeRequest.end(resolve);
return;
}
nodeRequest.write(r.value, "binary", function (e) {
if (e === null || e === undefined) {
writeNext();
return;
}
if (it.throw !== undefined) {
it.throw((0, node_error_js_1.connectErrorFromNodeReason)(e)).catch(() => {
//
});
}
// If the server responds and closes the connection before the client has written the entire response
// body, we get an ERR_STREAM_WRITE_AFTER_END error code from Node.js here.
// We do want to notify the iterable of the error condition, but we do not want to reject our sentinel,
// because that would also affect the reading side.
if (nodeRequest.writableEnded &&
(0, node_error_js_1.unwrapNodeErrorChain)(e)
.map(node_error_js_1.getNodeErrorProps)
.some((p) => p.code == "ERR_STREAM_WRITE_AFTER_END")) {
return;
}
sentinel.reject(e);
});
}, (e) => {
sentinel.reject(e);
});
}
});
}
function createSentinel(signal) {
let res;
let rej;
let resolved = false;
let rejected = false;
const p = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
const c = {
resolve() {
if (!resolved && !rejected) {
resolved = true;
res === null || res === void 0 ? void 0 : res();
}
},
isResolved() {
return resolved;
},
reject(reason) {
if (!resolved && !rejected) {
rejected = true;
rej === null || rej === void 0 ? void 0 : rej((0, node_error_js_1.connectErrorFromNodeReason)(reason));
}
},
isRejected() {
return rejected;
},
async race(promise) {
const r = await Promise.race([promise, p]);
if (r === undefined && resolved) {
throw new connect_1.ConnectError("sentinel completed early", connect_1.Code.Internal);
}
return r;
},
};
const s = Object.assign(p, c);
function onSignalAbort() {
c.reject((0, protocol_1.getAbortSignalReason)(this));
}
if (signal) {
if (signal.aborted) {
c.reject((0, protocol_1.getAbortSignalReason)(signal));
}
else {
signal.addEventListener("abort", onSignalAbort);
}
p.finally(() => signal.removeEventListener("abort", onSignalAbort)).catch(() => {
// We intentionally swallow sentinel rejection - errors must
// propagate through the request or response iterables.
});
}
return s;
}
;