UNPKG

@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
"use strict"; // 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; }