@duongtrungnguyen/nestro
Version:
Service registry for Nest JS
260 lines • 10.8 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 base_proxy_service_exports = {};
__export(base_proxy_service_exports, {
BaseProxyService: () => BaseProxyService
});
module.exports = __toCommonJS(base_proxy_service_exports);
var import_common = require("@nestjs/common");
var import_http = require("http");
var import_net = require("net");
var import_url = require("url");
var import_discovery = require("../../../discovery");
var import_errors = require("../errors");
var import_utils = require("../utils");
let BaseProxyService = class {
constructor(discoveryService) {
this.discoveryService = discoveryService;
}
/**
* Executes the proxy request and waits for completion.
*
* @param req - The incoming request.
* @param res - The outgoing response.
* @param options - Proxy options.
* @param proxyFn - The proxy function to execute (web or ws).
* @returns A promise that resolves when the proxy is complete.
*/
async executeProxy(req, res, options, proxyFn, callbacks) {
return new Promise((resolve, reject) => {
let isResolved = false;
const timeout = setTimeout(() => {
if (!isResolved) {
isResolved = true;
reject(new import_errors.ProxyError(`Proxy timeout after ${options.timeout || 3e4}ms`, import_errors.ProxyErrorType.TIMEOUT, import_common.HttpStatus.GATEWAY_TIMEOUT));
}
}, options.timeout || 3e4);
const enhancedCallbacks = {
...callbacks,
onError: (err, req2, res2, target) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
const proxyError = this.categorizeError(err);
if (callbacks?.onError) {
callbacks.onError(proxyError, req2, res2, target);
} else {
reject(proxyError);
}
}
},
onEnd: (req2, res2, proxyRes) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
callbacks?.onEnd?.(req2, res2, proxyRes);
resolve();
}
}
};
try {
proxyFn(req, res, options, enhancedCallbacks);
res.on("close", () => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
resolve();
}
}).on("finish", () => {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
resolve();
}
});
} catch (error) {
if (!isResolved) {
isResolved = true;
clearTimeout(timeout);
reject(this.categorizeError(error));
}
}
});
}
/**
* Validates the provided proxy route configuration to ensure that at least
* one of `service` or `target` is specified. Throws a `ProxyError` if both
* are missing, indicating a configuration error.
*
* @param routeConfig - The proxy route configuration object to validate.
* @throws {ProxyError} If neither `service` nor `target` is provided in the configuration.
*/
validateRouteConfig(routeConfig) {
if (!routeConfig.service && !routeConfig.target) {
throw new import_errors.ProxyError(
"Either service or target must be provided in routeConfig",
import_errors.ProxyErrorType.CONFIGURATION_ERROR,
import_common.HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
/**
* Builds and returns the proxy options for a given route configuration.
*
* @param routeConfig - The configuration object for the proxy route, containing path rewrite rules and timeout settings.
* @param defaultOptions - Optional partial proxy options to override or extend the default settings.
* @returns The complete set of proxy options to be used for the proxy middleware.
*/
buildProxyOptions(routeConfig, defaultOptions = {}) {
return {
changeOrigin: true,
xfwd: true,
pathRewrite: routeConfig.pathRewrite,
preserveHeaderKeyCase: true,
proxyTimeout: routeConfig.timeout,
...defaultOptions
};
}
/**
* Handles errors that occur during service discovery and maps them to a standardized `ProxyError`.
*
* @param error - The error encountered during discovery.
* @returns A `ProxyError` instance with an appropriate error type and HTTP status code.
* - If the error is a `DiscoveryError`, returns a `ProxyError` with `SERVICE_UNAVAILABLE` status.
* - Otherwise, returns a `ProxyError` with `INTERNAL_SERVER_ERROR` status.
*/
handleDiscoveryError(error) {
if (error instanceof import_discovery.DiscoveryError) {
return new import_errors.ProxyError(error.message, import_errors.ProxyErrorType.UNKNOWN, import_common.HttpStatus.SERVICE_UNAVAILABLE);
}
return new import_errors.ProxyError(error.message, import_errors.ProxyErrorType.UNKNOWN, import_common.HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Adds x-forwarded headers to the request.
*
* @param req - The incoming HTTP request.
* @param xfwd - Whether to add x-forwarded headers.
* @param isWebSocket - Whether the request is for WebSocket.
*/
addForwardedHeaders(req, xfwd, isWebSocket = false) {
if (!xfwd) return;
const encrypted = (0, import_utils.hasEncryptedConnection)(req);
const values = {
for: req.socket.remoteAddress || "",
port: req.socket.remotePort?.toString() || "",
proto: isWebSocket ? encrypted ? "wss" : "ws" : encrypted ? "https" : "http"
};
for (const header of ["for", "port", "proto"]) {
const headerName = `x-forwarded-${header}`;
const currentValue = req.headers[headerName];
const value = values[header];
req.headers[headerName] = currentValue ? `${currentValue},${value}` : value;
}
if (!req.headers["x-forwarded-host"]) {
req.headers["x-forwarded-host"] = req.headers["host"] || "";
}
}
/**
* Normalizes proxy options by converting string targets to URL objects and applying router logic.
*
* @param options - Proxy configuration options.
* @param path - The request path.
* @returns Normalized proxy options.
* @throws Error if target is missing.
*/
normalizeOptions(options, path) {
const normalized = { ...options };
if (typeof normalized.target === "string") {
try {
normalized.target = new import_url.URL(normalized.target);
} catch {
throw new import_errors.ProxyError(`Invalid target URL: ${normalized.target}`, import_errors.ProxyErrorType.CONFIGURATION_ERROR, import_common.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
if (normalized.router) {
for (const route in normalized.router) {
if (new RegExp(route).test(path)) {
const target = normalized.router[route];
try {
normalized.target = typeof target === "string" ? new import_url.URL(target) : target;
} catch {
throw new import_errors.ProxyError(`Invalid router target URL: ${target}`, import_errors.ProxyErrorType.CONFIGURATION_ERROR, import_common.HttpStatus.INTERNAL_SERVER_ERROR);
}
break;
}
}
}
if (!normalized.target) {
throw new import_errors.ProxyError("Target is required", import_errors.ProxyErrorType.CONFIGURATION_ERROR, import_common.HttpStatus.INTERNAL_SERVER_ERROR);
}
return normalized;
}
/**
* Handles proxy errors for HTTP or WebSocket requests.
*
* @param err - The error object.
* @param req - The incoming HTTP request.
* @param res - The outgoing response or socket.
* @param target - The target server.
* @param onError - Optional custom error handler.
*/
handleError(err, req, res, target, onError) {
const proxyError = this.categorizeError(err);
console.log(proxyError, err, proxyError.toJson());
if (onError) {
onError(err, req, res, target);
} else if (res instanceof import_http.ServerResponse && !res.headersSent) {
res.writeHead(import_common.HttpStatus.SERVICE_UNAVAILABLE, { "Content-Type": "application/json" });
res.end(proxyError.toJson());
} else if (res instanceof import_net.Socket && !res.destroyed) {
res.end();
}
}
categorizeError(error) {
if (error instanceof import_errors.ProxyError) {
return error;
}
if (error.code === "ECONNREFUSED" || error.message.includes("ECONNREFUSED")) {
return new import_errors.ProxyError("Connection refused by target server", import_errors.ProxyErrorType.TARGET_NOT_FOUND, import_common.HttpStatus.BAD_GATEWAY);
}
if (error.code === "ETIMEDOUT" || error.message.includes("ETIMEDOUT") || error.message.includes("timeout")) {
return new import_errors.ProxyError("Timeout occurred while connecting to target server", import_errors.ProxyErrorType.TIMEOUT, import_common.HttpStatus.GATEWAY_TIMEOUT);
}
if (error.code === "ENOTFOUND" || error.message.includes("ENOTFOUND")) {
return new import_errors.ProxyError("Target host not found", import_errors.ProxyErrorType.TARGET_NOT_FOUND, import_common.HttpStatus.NOT_FOUND);
}
return new import_errors.ProxyError(error.message || "Unknown proxy error", import_errors.ProxyErrorType.UNKNOWN, import_common.HttpStatus.INTERNAL_SERVER_ERROR);
}
};
BaseProxyService = __decorateClass([
__decorateParam(0, (0, import_common.Inject)(import_discovery.DiscoveryService))
], BaseProxyService);
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BaseProxyService
});
//# sourceMappingURL=base-proxy.service.js.map