@duongtrungnguyen/nestro
Version:
Service registry for Nest JS
290 lines • 12.4 kB
JavaScript
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 { request as httpRequest } from "http";
import { Inject, Injectable } from "@nestjs/common";
import { request as httpsRequest } from "https";
import { buildInstanceHttpUrl, debugError, debugLog, debugWarn } from "../../../common";
import { rewriteCookieProperty, setupOutgoing } from "../utils";
import { BaseProxyService } from "./base-proxy.service";
import { CONNECTION_ERROR_CODES } from "../constants";
import { DiscoveryService } from "../../../discovery";
let HttpProxyService = class extends BaseProxyService {
constructor(discoveryService) {
super(discoveryService);
}
/**
* Proxies an HTTP request to a target server.
* Uses service discovery and load balancing if a service is specified,
* or directly proxies to a specific target URL.
*
* @param req - Incoming request with raw body.
* @param res - Express response object.
* @param routeConfig - Configuration for the route to determine target or service.
*/
async proxyRequest(req, res, routeConfig) {
this.validateRouteConfig(routeConfig);
const proxyOptions = this.buildProxyOptions(routeConfig, {
buffer: this.getRequestBuffer(req)
});
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);
}
}
/**
* Proxies an incoming HTTP request to a discovered service instance.
*
* This method uses the discovery service to find an available instance of the specified service,
* then forwards the HTTP request to that instance using the provided proxy options.
* If the connection to an instance fails, it will attempt to try another available instance.
* Handles errors related to service discovery and proxying.
*
* @param originalUrl - The original URL of the incoming request.
* @param req - The incoming HTTP request object, possibly containing the raw body.
* @param res - The HTTP response object to send the proxied response.
* @param proxyOptions - Options to configure the proxy behavior.
* @param service - The name of the service to which the request should be proxied.
* @returns A promise that resolves when the proxying operation is complete.
* @throws Throws an error if service discovery fails or if proxying cannot be completed.
*/
async proxyToService(originalUrl, req, res, proxyOptions, service) {
try {
await this.discoveryService.discover(service, async (instance, tryAnotherInstance) => {
const targetUrl = buildInstanceHttpUrl(instance);
debugLog(HttpProxyService.name, `Proxying HTTP request from ${originalUrl} to ${targetUrl}`);
await this.executeProxy(req, res, { ...proxyOptions, target: targetUrl }, this.handleProxy.bind(this), {
onConnectFailed: (err) => {
debugError(HttpProxyService.name, `Connection failed to ${targetUrl}: ${err.message}`);
tryAnotherInstance();
}
});
});
} catch (error) {
throw this.handleDiscoveryError(error);
}
}
/**
* Proxies an incoming HTTP request to the specified target URL using the provided proxy options.
*
* @param originalUrl - The original URL of the incoming request.
* @param req - The incoming HTTP request object, potentially containing the raw body.
* @param res - The HTTP response object to send the proxied response.
* @param proxyOptions - Configuration options for the proxy operation.
* @param targetUrl - The destination URL or URL object to which the request should be proxied.
* @returns A promise that resolves when the proxy operation is complete.
*/
async proxyToTarget(originalUrl, req, res, proxyOptions, targetUrl) {
debugLog(HttpProxyService.name, `Proxying HTTP request from ${originalUrl} to ${targetUrl}`);
await this.executeProxy(req, res, { ...proxyOptions, target: targetUrl }, this.handleProxy.bind(this));
}
/**
* Internal handler for setting up and streaming a proxy request.
*
* @param req - Incoming request.
* @param res - Outgoing response.
* @param options - Proxy configuration.
* @param callbacks - Optional lifecycle event callbacks.
*/
handleProxy(req, res, options = {}, callbacks = {}) {
const normalizedOptions = this.normalizeOptions(options, req.url || "/");
if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"]) {
req.headers["content-length"] = "0";
delete req.headers["transfer-encoding"];
}
if (normalizedOptions.timeout) {
req.socket.setTimeout(normalizedOptions.timeout);
}
this.addForwardedHeaders(req, normalizedOptions.xfwd);
this.streamRequest(req, res, normalizedOptions, callbacks);
}
/**
* Streams the HTTP request to the actual target using http/https modules.
*
* @param req - Incoming request.
* @param res - Outgoing response.
* @param options - Normalized proxy options.
* @param callbacks - Optional lifecycle callbacks.
*/
streamRequest(req, res, options, callbacks) {
callbacks.onStart?.(req, res, options.target);
const targetProtocol = options.target.protocol === "https:" ? httpsRequest : httpRequest;
const proxyOptions = setupOutgoing({}, options, req);
const proxyReq = targetProtocol(proxyOptions);
this.handleProxyRequest(req, res, proxyReq, options, callbacks);
this.sendRequest(req, proxyReq, options.buffer);
}
/**
* Extracts the raw request buffer for proxying.
*
* @param req - Incoming request.
* @returns Buffer, string, or undefined.
*/
getRequestBuffer(req) {
if (req.rawBody) return req.rawBody;
if (req.body && Object.keys(req.body).length > 0) {
return JSON.stringify(req.body);
}
return void 0;
}
/**
* Handles the lifecycle and events of a proxied HTTP request.
*
* Sets up event listeners on the proxy request and the original client request to manage errors,
* timeouts, socket events, and responses. Invokes appropriate callbacks for connection failures,
* proxy request events, and errors. Forwards the proxy response to the client.
*
* @param req - The incoming client HTTP request.
* @param res - The server response object to send data back to the client.
* @param proxyReq - The outgoing proxy HTTP request.
* @param options - Proxy configuration options.
* @param callbacks - Callback functions for handling proxy events.
*/
handleProxyRequest(req, res, proxyReq, options, callbacks) {
proxyReq.on("error", (err) => {
if (this.isConnectionError(err)) {
callbacks.onConnectFailed?.(err);
}
this.handleError(err, req, res, options.target, callbacks.onError);
});
proxyReq.on("socket", (socket) => {
callbacks.onProxyReq?.(proxyReq, req, res, socket);
});
if (options.proxyTimeout) {
proxyReq.setTimeout(options.proxyTimeout, () => {
const timeoutError = new Error("Proxy timeout");
proxyReq.destroy(timeoutError);
callbacks.onConnectFailed?.(timeoutError);
});
}
req.on("aborted", () => {
debugError(HttpProxyService.name, "Client request aborted");
proxyReq.destroy();
});
req.on("error", (err) => {
debugError(HttpProxyService.name, `Client request error: ${err.message}`);
proxyReq.destroy();
this.handleError(err, req, res, options.target, callbacks.onError);
});
proxyReq.on("response", (proxyRes) => {
this.handleProxyResponse(req, res, proxyRes, options, callbacks);
});
}
/**
* Handles the response from the proxied server and pipes it to the client response.
*
* This method sets the status code and status message on the client response,
* copies headers from the proxy response, rewrites cookies as necessary, and
* streams the proxy response body to the client. It also invokes the appropriate
* callbacks for proxy response and completion events, and handles errors that
* may occur during the proxying process.
*
* @param req - The original incoming HTTP request from the client.
* @param res - The HTTP response object to send data back to the client.
* @param proxyRes - The HTTP response received from the proxied target server.
* @param options - Proxy options containing configuration such as the target server.
* @param callbacks - Callback functions for handling proxy events such as response, end, and error.
*/
handleProxyResponse(req, res, proxyRes, options, callbacks) {
callbacks.onProxyRes?.(proxyRes, req, res);
res.statusCode = proxyRes.statusCode || 500;
if (proxyRes.statusMessage) {
res.statusMessage = proxyRes.statusMessage;
}
this.copyHeaders(proxyRes, res);
this.rewriteCookies(res, options);
proxyRes.pipe(res);
proxyRes.on("end", () => {
callbacks.onEnd?.(req, res, proxyRes);
debugLog(HttpProxyService.name, `Proxy completed - ${proxyRes.statusCode}`);
});
proxyRes.on("error", (err) => {
if (!res.headersSent) {
this.handleError(new Error(`Proxy response error: ${err.message}`), req, res, options.target, callbacks.onError);
} else if (!res.finished) {
res.end();
}
});
}
/**
* Sends the request body to the proxy
*
* @param req - Client request
* @param proxyReq - Proxy request
* @param buffer - Optional buffer to send
*/
sendRequest(req, proxyReq, buffer) {
if (buffer) {
const data = Buffer.isBuffer(buffer) || typeof buffer === "string" ? buffer : JSON.stringify(buffer);
proxyReq.write(data);
proxyReq.end();
} else {
req.pipe(proxyReq);
}
}
/**
* 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 CONNECTION_ERROR_CODES.some((code) => err.message.includes(code) || err.code === code) || /connect|connection|timeout/i.test(err.message);
}
/**
* Copies headers from proxy response to client response
*
* @param proxyRes - Proxy response
* @param res - Client response
*/
copyHeaders(proxyRes, res) {
const skipHeaders = ["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "te", "trailers", "transfer-encoding", "upgrade"];
for (const [key, value] of Object.entries(proxyRes.headers)) {
if (value !== void 0 && !skipHeaders.includes(key.toLowerCase())) {
try {
res.setHeader(key, value);
} catch (err) {
debugWarn(HttpProxyService.name, `Error setting header ${key}: ${err.message}`);
}
}
}
}
/**
* Optionally rewrites cookie domain and path in the response.
*
* @param res - Outgoing response.
* @param options - Proxy configuration.
*/
rewriteCookies(res, options) {
const cookies = res.getHeader("set-cookie");
if (!cookies) return;
const rewrite = (config, property) => {
const rewriteConfig = typeof config === "string" ? { "*": config } : config;
res.setHeader("set-cookie", rewriteCookieProperty(cookies, rewriteConfig, property));
};
if (options.cookieDomainRewrite) rewrite(options.cookieDomainRewrite, "domain");
if (options.cookiePathRewrite) rewrite(options.cookiePathRewrite, "path");
}
};
HttpProxyService = __decorateClass([
Injectable(),
__decorateParam(0, Inject(DiscoveryService))
], HttpProxyService);
export {
HttpProxyService
};
//# sourceMappingURL=http-proxy.service.js.map