@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.
226 lines (225 loc) • 11.7 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.
var __asyncValues = (this && this.__asyncValues) || function (o) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var m = o[Symbol.asyncIterator], i;
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
var g = generator.apply(thisArg, _arguments || []), i, q = [];
return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;
function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }
function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }
function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
function fulfill(value) { resume("next", value); }
function reject(value) { resume("throw", value); }
function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.universalRequestFromNodeRequest = universalRequestFromNodeRequest;
exports.universalResponseToNodeResponse = universalResponseToNodeResponse;
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");
function universalRequestFromNodeRequest(nodeRequest, ...rest) {
var _a, _b;
const nodeResponse = rest.length === 3 ? rest[0] : undefined;
const parsedJsonBody = rest.length === 3 ? rest[1] : rest[0];
const contextValues = rest.length === 3 ? rest[2] : rest[1];
const encrypted = "encrypted" in nodeRequest.socket && nodeRequest.socket.encrypted;
const protocol = encrypted ? "https" : "http";
const authority = "authority" in nodeRequest
? nodeRequest.authority
: nodeRequest.headers.host;
const pathname = (_a = nodeRequest.url) !== null && _a !== void 0 ? _a : "";
if (authority === undefined) {
throw new connect_1.ConnectError("unable to determine request authority from Node.js server request", connect_1.Code.Internal);
}
const body = parsedJsonBody !== undefined
? parsedJsonBody
: asyncIterableFromNodeServerRequest(nodeRequest);
const abortController = new AbortController();
if ("stream" in nodeRequest) {
// HTTP/2 has error codes we want to honor
nodeRequest.once("close", () => {
const err = (0, node_error_js_1.connectErrorFromH2ResetCode)(nodeRequest.stream.rstCode);
if (err !== undefined) {
abortController.abort(err);
}
else {
abortController.abort();
}
});
}
else {
// HTTP/1.1 does not have error codes, but Node.js has ECONNRESET
const nodeResponsOrRequest = nodeResponse !== null && nodeResponse !== void 0 ? nodeResponse : nodeRequest;
const onH1Error = (e) => {
nodeRequest.off("error", onH1Error);
nodeResponsOrRequest.off("close", onH1Close);
abortController.abort((0, node_error_js_1.connectErrorFromNodeReason)(e));
};
const onH1Close = () => {
nodeRequest.off("error", onH1Error);
nodeResponsOrRequest.off("close", onH1Close);
// When subscribed to the response, this can get called before "error"
abortController.abort(nodeRequest.errored
? (0, node_error_js_1.connectErrorFromNodeReason)(nodeRequest.errored)
: undefined);
};
nodeRequest.once("error", onH1Error);
// Node emits close on the request as soon as all data is read.
// We instead subscribe to the response (if available)
//
// Ref: https://github.com/nodejs/node/issues/40775
nodeResponsOrRequest.once("close", onH1Close);
}
return {
httpVersion: nodeRequest.httpVersion,
method: (_b = nodeRequest.method) !== null && _b !== void 0 ? _b : "",
url: new URL(pathname, `${protocol}://${authority}`).toString(),
header: (0, node_universal_header_js_1.nodeHeaderToWebHeader)(nodeRequest.headers),
body,
signal: abortController.signal,
contextValues: contextValues,
};
}
/**
* Writes a UniversalServerResponse to a Node.js server response.
* This function helps to implement adapters to server frameworks running
* on Node.js. Please be careful using this function in your own code, as we
* may have to make changes to it in the future.
*/
async function universalResponseToNodeResponse(universalResponse, nodeResponse) {
var _a, _b, _c;
const it = (_a = universalResponse.body) === null || _a === void 0 ? void 0 : _a[Symbol.asyncIterator]();
let isWriteError = false;
try {
if (it !== undefined) {
let chunk = await it.next();
isWriteError = true;
// we deliberately send headers after first read, not before,
// because we have to give the implementation a chance to
// set response headers
nodeResponse.writeHead(universalResponse.status, (0, node_universal_header_js_1.webHeaderToNodeHeaders)(universalResponse.header));
isWriteError = false;
for (; chunk.done !== true; chunk = await it.next()) {
isWriteError = true;
await write(nodeResponse, chunk.value);
if ("flush" in nodeResponse &&
typeof nodeResponse.flush == "function") {
// The npm package "compression" is an express middleware that is widely used,
// for example in next.js. It uses the npm package "compressible" to determine
// whether to apply compression to a response. Unfortunately, "compressible"
// matches every mime type that ends with "+json", causing our server-streaming
// RPCs to be buffered.
// The package modifies the response object, and adds a flush() method, which
// flushes the underlying gzip or deflate stream from the Node.js zlib module.
// The method is added here:
// https://github.com/expressjs/compression/blob/ad5113b98cafe1382a0ece30bb4673707ac59ce7/index.js#L70
nodeResponse.flush();
}
isWriteError = false;
}
}
if (!nodeResponse.headersSent) {
nodeResponse.writeHead(universalResponse.status, (0, node_universal_header_js_1.webHeaderToNodeHeaders)(universalResponse.header));
}
if (universalResponse.trailer) {
nodeResponse.addTrailers((0, node_universal_header_js_1.webHeaderToNodeHeaders)(universalResponse.trailer));
}
await new Promise((resolve) => {
// The npm package "compression" crashes when a callback is passed to end()
// https://github.com/expressjs/compression/blob/ad5113b98cafe1382a0ece30bb4673707ac59ce7/index.js#L115
nodeResponse.once("end", resolve);
nodeResponse.end();
});
}
catch (e) {
// Report write errors to the handler.
if (isWriteError) {
(_b = it === null || it === void 0 ? void 0 : it.throw) === null || _b === void 0 ? void 0 : _b.call(it, e).catch(() => { });
}
throw (0, node_error_js_1.connectErrorFromNodeReason)(e);
}
finally {
(_c = it === null || it === void 0 ? void 0 : it.return) === null || _c === void 0 ? void 0 : _c.call(it).catch(() => { });
}
}
function asyncIterableFromNodeServerRequest(request) {
return __asyncGenerator(this, arguments, function* asyncIterableFromNodeServerRequest_1() {
var _a, e_1, _b, _c;
try {
for (var _d = true, request_1 = __asyncValues(request), request_1_1; request_1_1 = yield __await(request_1.next()), _a = request_1_1.done, !_a; _d = true) {
_c = request_1_1.value;
_d = false;
const chunk = _c;
yield yield __await(chunk);
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (!_d && !_a && (_b = request_1.return)) yield __await(_b.call(request_1));
}
finally { if (e_1) throw e_1.error; }
}
});
}
function write(stream, data) {
return new Promise((resolve, reject) => {
if (stream.errored) {
return error(stream.errored);
}
stream.once("error", error);
stream.once("drain", drain);
// flushed == false: the stream wishes for the calling code to wait for
// the 'drain' event to be emitted before continuing to write additional
// data.
const flushed = stream.write(data, "binary", function (err) {
if (err && !flushed) {
// We are never getting a "drain" nor an "error" event if the stream
// has already ended (ERR_STREAM_WRITE_AFTER_END), so we have to
// resolve our promise in this callback.
error(err);
// However, once we do that (and remove our event listeners), we _do_
// get an "error" event, which ends up as an uncaught exception.
// We silence this error specifically with the following listener.
// All of this seems very fragile.
stream.once("error", () => {
//
});
}
});
if (flushed) {
drain();
}
function error(err) {
stream.off("error", error);
stream.off("drain", drain);
reject(err);
}
function drain() {
stream.off("error", error);
stream.off("drain", drain);
resolve();
}
});
}
;