UNPKG

@duongtrungnguyen/nestro

Version:
239 lines 9.32 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 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); import { HttpStatus, Inject } from "@nestjs/common"; import { ServerResponse } from "http"; import { Socket } from "net"; import { URL } from "url"; import { DiscoveryError, DiscoveryService } from "../../../discovery"; import { ProxyError, ProxyErrorType } from "../errors"; import { hasEncryptedConnection } from "../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 ProxyError(`Proxy timeout after ${options.timeout || 3e4}ms`, ProxyErrorType.TIMEOUT, 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 ProxyError( "Either service or target must be provided in routeConfig", ProxyErrorType.CONFIGURATION_ERROR, 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 DiscoveryError) { return new ProxyError(error.message, ProxyErrorType.UNKNOWN, HttpStatus.SERVICE_UNAVAILABLE); } return new ProxyError(error.message, ProxyErrorType.UNKNOWN, 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 = 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 URL(normalized.target); } catch { throw new ProxyError(`Invalid target URL: ${normalized.target}`, ProxyErrorType.CONFIGURATION_ERROR, 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 URL(target) : target; } catch { throw new ProxyError(`Invalid router target URL: ${target}`, ProxyErrorType.CONFIGURATION_ERROR, HttpStatus.INTERNAL_SERVER_ERROR); } break; } } } if (!normalized.target) { throw new ProxyError("Target is required", ProxyErrorType.CONFIGURATION_ERROR, 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 ServerResponse && !res.headersSent) { res.writeHead(HttpStatus.SERVICE_UNAVAILABLE, { "Content-Type": "application/json" }); res.end(proxyError.toJson()); } else if (res instanceof Socket && !res.destroyed) { res.end(); } } categorizeError(error) { if (error instanceof ProxyError) { return error; } if (error.code === "ECONNREFUSED" || error.message.includes("ECONNREFUSED")) { return new ProxyError("Connection refused by target server", ProxyErrorType.TARGET_NOT_FOUND, HttpStatus.BAD_GATEWAY); } if (error.code === "ETIMEDOUT" || error.message.includes("ETIMEDOUT") || error.message.includes("timeout")) { return new ProxyError("Timeout occurred while connecting to target server", ProxyErrorType.TIMEOUT, HttpStatus.GATEWAY_TIMEOUT); } if (error.code === "ENOTFOUND" || error.message.includes("ENOTFOUND")) { return new ProxyError("Target host not found", ProxyErrorType.TARGET_NOT_FOUND, HttpStatus.NOT_FOUND); } return new ProxyError(error.message || "Unknown proxy error", ProxyErrorType.UNKNOWN, HttpStatus.INTERNAL_SERVER_ERROR); } }; BaseProxyService = __decorateClass([ __decorateParam(0, Inject(DiscoveryService)) ], BaseProxyService); export { BaseProxyService }; //# sourceMappingURL=base-proxy.service.js.map