@duongtrungnguyen/nestro
Version:
Service registry for Nest JS
267 lines • 12 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
if (kind && result) __defProp(target, key, result);
return result;
};
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
var ws_proxy_service_exports = {};
__export(ws_proxy_service_exports, {
WsProxyService: () => WsProxyService
});
module.exports = __toCommonJS(ws_proxy_service_exports);
var import_http = require("http");
var import_common = require("@nestjs/common");
var import_https = require("https");
var import_common2 = require("../../../common");
var import_base_proxy = require("./base-proxy.service");
var import_utils = require("../utils");
var import_constants = require("../constants");
var import_discovery = require("../../../discovery");
let WsProxyService = class extends import_base_proxy.BaseProxyService {
constructor(discoveryService) {
super(discoveryService);
}
/**
* Proxies a WebSocket request to the target server.
* Uses load balancing if service is provided, otherwise uses direct target.
*
* @param req - The incoming HTTP request for WebSocket upgrade.
* @param res - The outgoing HTTP response.
* @param routeConfig - Configuration for the proxy route.
* @returns A promise that resolves when the WebSocket connection is closed.
*/
async proxyRequest(req, res, routeConfig) {
this.validateRouteConfig(routeConfig);
const proxyOptions = this.buildProxyOptions(routeConfig);
const originalUrl = req.originalUrl || req.url;
try {
if (routeConfig.service) {
await this.proxyToService(originalUrl, req, res, proxyOptions, routeConfig.service);
} else {
await this.proxyToTarget(originalUrl, req, res, proxyOptions, routeConfig.target);
}
} catch (error) {
this.handleError(error, req, res, routeConfig.target);
}
}
async proxyToService(originalUrl, req, res, proxyOptions, service) {
try {
await this.discoveryService.discover(service, async (instance, tryAnotherInstance) => {
const targetUrl = (0, import_common2.buildInstanceWsUrl)(instance);
(0, import_common2.debugLog)(WsProxyService.name, `Proxying WebSocket request from ${originalUrl} to ${targetUrl}`);
await this.executeProxy(req, res, { ...proxyOptions, target: targetUrl }, this.handleProxy.bind(this), {
onConnectFailed: (err) => {
(0, import_common2.debugError)(WsProxyService.name, `Connection failed to ${targetUrl}: ${err.message}`);
tryAnotherInstance();
}
});
});
} catch (error) {
throw this.handleDiscoveryError(error);
}
}
async proxyToTarget(originalUrl, req, res, proxyOptions, targetUrl) {
(0, import_common2.debugLog)(WsProxyService.name, `Proxying WebSocket request from ${originalUrl} to ${targetUrl}`);
await this.executeProxy(req, res, { ...proxyOptions, target: targetUrl }, this.handleProxy.bind(this));
}
/**
* Checks if an error is related to connection issues
*
* @param err - The error to check
* @returns boolean indicating if this is a connection error
*/
isConnectionError(err) {
return import_constants.CONNECTION_ERROR_CODES.some((code) => err.message.includes(code) || err.code === code) || /connect|connection|timeout/i.test(err.message);
}
/**
* Proxies a WebSocket connection to the target server.
*
* @param req - The incoming HTTP request for WebSocket upgrade.
* @param socket - The client socket.
* @param head - The first packet of the upgraded stream.
* @param options - Proxy configuration options.
* @param callbacks - Optional callbacks for lifecycle events.
*/
handleProxy(req, _, options = {}, callbacks = {}) {
const socket = req.socket;
const head = Buffer.from("");
try {
const normalizedOptions = this.normalizeOptions(options, req.url || "/");
(0, import_common2.debugLog)(WsProxyService.name, `Proxying WebSocket to: ${normalizedOptions.target}`);
if (req.method !== "GET" || !req.headers.upgrade || req.headers.upgrade.toLowerCase() !== "websocket") {
socket.destroy();
return;
}
this.addForwardedHeaders(req, normalizedOptions.xfwd, true);
this.streamRequest(req, socket, head, normalizedOptions, callbacks);
} catch (err) {
(0, import_common2.debugError)(WsProxyService.name, `WebSocket setup error: ${err.message}`);
this.handleError(err, req, socket, void 0, callbacks.onError);
if (callbacks.onConnectFailed) {
callbacks.onConnectFailed(err);
}
socket.end();
}
}
/**
* Streams a WebSocket connection to the target server.
*
* @param req - The incoming HTTP request.
* @param socket - The client socket.
* @param head - The first packet of the upgraded stream.
* @param options - Normalized proxy options.
* @param callbacks - Optional callbacks for lifecycle events.
*/
streamRequest(req, socket, head, options, callbacks) {
const target = options.target;
const isSSL = target.protocol === "https:" || target.protocol === "wss:";
const proxyOptions = (0, import_utils.setupOutgoing)({}, options, req);
(0, import_utils.setupSocket)(socket);
if (head?.length) socket.unshift(head);
let proxyReq;
try {
proxyReq = (isSSL ? import_https.request : import_http.request)(proxyOptions);
} catch (err) {
this.handleRequestCreationError(err, socket, callbacks);
return;
}
callbacks.onProxyReqWs?.(proxyReq, req, socket, options, head);
this.setupProxyRequestHandlers(req, socket, proxyReq, options, callbacks);
proxyReq.end();
}
/**
* Handles errors that occur during the creation of a WebSocket proxy request.
* Logs the error, invokes the `onConnectFailed` callback if provided, and closes the socket if it is still open.
*
* @param err - The error encountered during request creation.
* @param socket - The socket associated with the WebSocket connection.
* @param callbacks - An object containing optional proxy callback functions.
*/
handleRequestCreationError(err, socket, callbacks) {
(0, import_common2.debugError)(WsProxyService.name, `Failed to create WebSocket proxy request: ${err.message}`);
callbacks.onConnectFailed?.(err);
if (!socket.destroyed) socket.end();
}
/**
* Sets up event handlers for a proxy WebSocket request, managing error handling,
* HTTP fallback, WebSocket upgrade, and request timeout.
*
* @param req - The incoming HTTP request from the client.
* @param socket - The network socket associated with the client connection.
* @param proxyReq - The outgoing proxy request to the target server.
* @param options - Proxy options including target and timeout settings.
* @param callbacks - Callback functions for handling connection and error events.
*/
setupProxyRequestHandlers(req, socket, proxyReq, options, callbacks) {
const onOutgoingError = (err) => {
(0, import_common2.debugError)(WsProxyService.name, `WebSocket proxy error: ${err.message}`);
if (this.isConnectionError(err)) {
callbacks.onConnectFailed?.(err);
} else {
this.handleError(err, req, socket, options.target, callbacks.onError);
}
if (!socket.destroyed) socket.end();
};
proxyReq.on("error", onOutgoingError);
proxyReq.on("response", (proxyRes) => {
if (!proxyRes.headers.upgrade || proxyRes.headers.upgrade.toLowerCase() !== "websocket") {
(0, import_common2.debugWarn)(WsProxyService.name, "WebSocket upgrade failed, falling back to HTTP");
socket.write(this.createHttpHeader(`HTTP/${proxyRes.httpVersion} ${proxyRes.statusCode} ${proxyRes.statusMessage || ""}`, proxyRes.headers));
proxyRes.pipe(socket);
}
});
proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
this.handleWebSocketUpgrade(req, socket, proxyRes, proxySocket, proxyHead, options, callbacks);
});
if (options.timeout) {
proxyReq.setTimeout(options.timeout, () => {
const timeoutError = new Error(`WebSocket proxy timeout after ${options.timeout}ms`);
proxyReq.destroy(timeoutError);
callbacks.onConnectFailed?.(timeoutError);
});
}
}
/**
* Handles the WebSocket upgrade process between the client and the proxy target.
*
* This method sets up the necessary socket piping and event listeners to proxy
* WebSocket connections, including error handling and lifecycle callbacks.
*
* @param req - The incoming HTTP request from the client.
* @param socket - The socket associated with the client connection.
* @param proxyRes - The HTTP response from the proxy target.
* @param proxySocket - The socket connected to the proxy target.
* @param proxyHead - Any buffered data from the proxy target's upgrade response.
* @param options - Proxy configuration options.
* @param callbacks - Callback functions for handling proxy events (open, close, error).
*/
handleWebSocketUpgrade(req, socket, proxyRes, proxySocket, proxyHead, options, callbacks) {
(0, import_common2.debugLog)(WsProxyService.name, "WebSocket upgrade successful");
(0, import_utils.setupSocket)(proxySocket);
if (proxyHead?.length) proxySocket.unshift(proxyHead);
socket.write(this.createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));
proxySocket.pipe(socket).pipe(proxySocket);
proxySocket.on("error", (err) => {
(0, import_common2.debugError)(WsProxyService.name, `WebSocket proxy socket error: ${err.message}`);
this.handleError(err, req, socket, options.target, callbacks.onError);
});
const handleClose = () => callbacks.onClose?.(proxyRes, proxySocket, proxyHead);
proxySocket.on("end", handleClose);
proxySocket.on("close", handleClose);
socket.on("error", () => {
if (!proxySocket.destroyed) proxySocket.end();
});
callbacks.onOpen?.(proxySocket);
}
/**
* Creates an HTTP header string from a status line and headers object
*
* @param line - The status line
* @param headers - The headers object
* @returns Formatted HTTP header string
*/
createHttpHeader(line, headers) {
const lines = [line];
for (const [key, value] of Object.entries(headers)) {
if (value !== void 0) {
if (Array.isArray(value)) {
for (const val of value) {
lines.push(`${key}: ${val}`);
}
} else {
lines.push(`${key}: ${value}`);
}
}
}
return lines.join("\r\n") + "\r\n\r\n";
}
};
WsProxyService = __decorateClass([
(0, import_common.Injectable)(),
__decorateParam(0, (0, import_common.Inject)(import_discovery.DiscoveryService))
], WsProxyService);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
WsProxyService
});
//# sourceMappingURL=ws-proxy.service.js.map