@push.rocks/smartproxy
Version:
A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.
737 lines • 68.7 kB
JavaScript
import * as plugins from '../../plugins.js';
import '../../core/models/socket-augmentation.js';
import { createLogger, } from './models/types.js';
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
import { ConnectionPool } from './connection-pool.js';
import { ContextCreator } from './context-creator.js';
import { HttpRequestHandler } from './http-request-handler.js';
import { Http2RequestHandler } from './http2-request-handler.js';
import { toBaseContext } from '../../core/models/route-context.js';
import { TemplateUtils } from '../../core/utils/template-utils.js';
import { SecurityManager } from './security-manager.js';
/**
* Handles HTTP request processing and proxying
*/
export class RequestHandler {
constructor(options, connectionPool, routeManager, functionCache, // FunctionCache - using any to avoid circular dependency
router // HttpRouter - using any to avoid circular dependency
) {
this.options = options;
this.connectionPool = connectionPool;
this.routeManager = routeManager;
this.functionCache = functionCache;
this.router = router;
this.defaultHeaders = {};
this.metricsTracker = null;
// HTTP/2 client sessions for backend proxying
this.h2Sessions = new Map();
// Context creator for route contexts
this.contextCreator = new ContextCreator();
// Rate limit cleanup interval
this.rateLimitCleanupInterval = null;
this.logger = createLogger(options.logLevel || 'info');
this.securityManager = new SecurityManager(this.logger);
// Schedule rate limit cleanup every minute
this.rateLimitCleanupInterval = setInterval(() => {
this.securityManager.cleanupExpiredRateLimits();
}, 60000);
// Make sure the interval doesn't keep the process alive
if (this.rateLimitCleanupInterval.unref) {
this.rateLimitCleanupInterval.unref();
}
}
/**
* Set the route manager instance
*/
setRouteManager(routeManager) {
this.routeManager = routeManager;
}
/**
* Set the metrics tracker instance
*/
setMetricsTracker(tracker) {
this.metricsTracker = tracker;
}
/**
* Set default headers to be included in all responses
*/
setDefaultHeaders(headers) {
this.defaultHeaders = {
...this.defaultHeaders,
...headers
};
this.logger.info('Updated default response headers');
}
/**
* Get all default headers
*/
getDefaultHeaders() {
return { ...this.defaultHeaders };
}
/**
* Select the appropriate target from the targets array based on sub-matching criteria
*/
selectTarget(targets, context) {
// Sort targets by priority (higher first)
const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
// Find the first matching target
for (const target of sortedTargets) {
if (!target.match) {
// No match criteria means this is a default/fallback target
return target;
}
// Check port match
if (target.match.ports && !target.match.ports.includes(context.port)) {
continue;
}
// Check path match (supports wildcards)
if (target.match.path && context.path) {
const pathPattern = target.match.path.replace(/\*/g, '.*');
const pathRegex = new RegExp(`^${pathPattern}$`);
if (!pathRegex.test(context.path)) {
continue;
}
}
// Check method match
if (target.match.method && context.method && !target.match.method.includes(context.method)) {
continue;
}
// Check headers match
if (target.match.headers && context.headers) {
let headersMatch = true;
for (const [key, pattern] of Object.entries(target.match.headers)) {
const headerValue = context.headers[key.toLowerCase()];
if (!headerValue) {
headersMatch = false;
break;
}
if (pattern instanceof RegExp) {
if (!pattern.test(headerValue)) {
headersMatch = false;
break;
}
}
else if (headerValue !== pattern) {
headersMatch = false;
break;
}
}
if (!headersMatch) {
continue;
}
}
// All criteria matched
return target;
}
// No matching target found, return the first target without match criteria (default)
return sortedTargets.find(t => !t.match) || null;
}
/**
* Apply CORS headers to response if configured
* Implements Phase 5.5: Context-aware CORS handling
*
* @param res The server response to apply headers to
* @param req The incoming request
* @param route Optional route config with CORS settings
*/
applyCorsHeaders(res, req, route) {
// Use route-specific CORS config if available, otherwise use global config
let corsConfig = null;
// Route CORS config takes precedence if enabled
if (route?.headers?.cors?.enabled) {
corsConfig = route.headers.cors;
this.logger.debug(`Using route-specific CORS config for ${route.name || 'unnamed route'}`);
}
// Fall back to global CORS config if available
else if (this.options.cors) {
corsConfig = this.options.cors;
this.logger.debug('Using global CORS config');
}
// If no CORS config available, skip
if (!corsConfig) {
return;
}
// Get origin from request
const origin = req.headers.origin;
// Apply Allow-Origin (with dynamic validation if needed)
if (corsConfig.allowOrigin) {
// Handle multiple origins in array format
if (Array.isArray(corsConfig.allowOrigin)) {
if (origin && corsConfig.allowOrigin.includes(origin)) {
// Match found, set specific origin
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin'); // Important for caching
}
else if (corsConfig.allowOrigin.includes('*')) {
// Wildcard match
res.setHeader('Access-Control-Allow-Origin', '*');
}
}
// Handle single origin or wildcard
else if (corsConfig.allowOrigin === '*') {
res.setHeader('Access-Control-Allow-Origin', '*');
}
// Match single origin against request
else if (origin && corsConfig.allowOrigin === origin) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
// Use template variables if present
else if (origin && corsConfig.allowOrigin.includes('{')) {
const resolvedOrigin = TemplateUtils.resolveTemplateVariables(corsConfig.allowOrigin, { domain: req.headers.host });
if (resolvedOrigin === origin || resolvedOrigin === '*') {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
}
}
// Apply other CORS headers
if (corsConfig.allowMethods) {
res.setHeader('Access-Control-Allow-Methods', corsConfig.allowMethods);
}
if (corsConfig.allowHeaders) {
res.setHeader('Access-Control-Allow-Headers', corsConfig.allowHeaders);
}
if (corsConfig.allowCredentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (corsConfig.exposeHeaders) {
res.setHeader('Access-Control-Expose-Headers', corsConfig.exposeHeaders);
}
if (corsConfig.maxAge) {
res.setHeader('Access-Control-Max-Age', corsConfig.maxAge.toString());
}
// Handle CORS preflight requests if enabled (default: true)
if (req.method === 'OPTIONS' && corsConfig.preflight !== false) {
res.statusCode = 204; // No content
res.end();
return;
}
}
// First implementation of applyRouteHeaderModifications moved to the second implementation below
/**
* Apply default headers to response
*/
applyDefaultHeaders(res) {
// Apply default headers
for (const [key, value] of Object.entries(this.defaultHeaders)) {
if (!res.hasHeader(key)) {
res.setHeader(key, value);
}
}
// Add server identifier if not already set
if (!res.hasHeader('Server')) {
res.setHeader('Server', 'NetworkProxy');
}
}
/**
* Apply URL rewriting based on route configuration
* Implements Phase 5.2: URL rewriting using route context
*
* @param req The request with the URL to rewrite
* @param route The route configuration containing rewrite rules
* @param routeContext Context for template variable resolution
* @returns True if URL was rewritten, false otherwise
*/
applyUrlRewriting(req, route, routeContext) {
// Check if route has URL rewriting configuration
if (!route.action.advanced?.urlRewrite) {
return false;
}
const rewriteConfig = route.action.advanced.urlRewrite;
// Store original URL for logging
const originalUrl = req.url;
if (rewriteConfig.pattern && rewriteConfig.target) {
try {
// Create a RegExp from the pattern
const regex = new RegExp(rewriteConfig.pattern, rewriteConfig.flags || '');
// Apply rewriting with template variable resolution
let target = rewriteConfig.target;
// Replace template variables in target with values from context
target = TemplateUtils.resolveTemplateVariables(target, routeContext);
// If onlyRewritePath is set, split URL into path and query parts
if (rewriteConfig.onlyRewritePath && req.url) {
const [path, query] = req.url.split('?');
const rewrittenPath = path.replace(regex, target);
req.url = query ? `${rewrittenPath}?${query}` : rewrittenPath;
}
else {
// Perform the replacement on the entire URL
req.url = req.url?.replace(regex, target);
}
this.logger.debug(`URL rewritten: ${originalUrl} -> ${req.url}`);
return true;
}
catch (err) {
this.logger.error(`Error in URL rewriting: ${err}`);
return false;
}
}
return false;
}
/**
* Apply header modifications from route configuration
* Implements Phase 5.1: Route-based header manipulation
*/
applyRouteHeaderModifications(route, req, res) {
// Check if route has header modifications
if (!route.headers) {
return;
}
// Apply request header modifications (these will be sent to the backend)
if (route.headers.request && req.headers) {
for (const [key, value] of Object.entries(route.headers.request)) {
// Skip if header already exists and we're not overriding
if (req.headers[key.toLowerCase()] && !value.startsWith('!')) {
continue;
}
// Handle special delete directive (!delete)
if (value === '!delete') {
delete req.headers[key.toLowerCase()];
this.logger.debug(`Deleted request header: ${key}`);
continue;
}
// Handle forced override (!value)
let finalValue;
if (value.startsWith('!') && value !== '!delete') {
// Keep the ! but resolve any templates in the rest
const templateValue = value.substring(1);
finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {});
}
else {
// Resolve templates in the entire value
finalValue = TemplateUtils.resolveTemplateVariables(value, {});
}
// Set the header
req.headers[key.toLowerCase()] = finalValue;
this.logger.debug(`Modified request header: ${key}=${finalValue}`);
}
}
// Apply response header modifications (these will be stored for later use)
if (route.headers.response) {
for (const [key, value] of Object.entries(route.headers.response)) {
// Skip if header already exists and we're not overriding
if (res.hasHeader(key) && !value.startsWith('!')) {
continue;
}
// Handle special delete directive (!delete)
if (value === '!delete') {
res.removeHeader(key);
this.logger.debug(`Deleted response header: ${key}`);
continue;
}
// Handle forced override (!value)
let finalValue;
if (value.startsWith('!') && value !== '!delete') {
// Keep the ! but resolve any templates in the rest
const templateValue = value.substring(1);
finalValue = '!' + TemplateUtils.resolveTemplateVariables(templateValue, {});
}
else {
// Resolve templates in the entire value
finalValue = TemplateUtils.resolveTemplateVariables(value, {});
}
// Set the header
res.setHeader(key, finalValue);
this.logger.debug(`Modified response header: ${key}=${finalValue}`);
}
}
}
/**
* Handle an HTTP request
*/
async handleRequest(req, res) {
// Record start time for logging
const startTime = Date.now();
// Get route before applying CORS (we might need its settings)
// Try to find a matching route using RouteManager
let matchingRoute = null;
if (this.routeManager) {
try {
// Create a connection ID for this request
const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
// Create route context for function-based targets
const routeContext = this.contextCreator.createHttpRouteContext(req, {
connectionId,
clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0',
serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0',
tlsVersion: req.socket.getTLSVersion?.() || undefined
});
const matchResult = this.routeManager.findMatchingRoute(toBaseContext(routeContext));
matchingRoute = matchResult?.route || null;
}
catch (err) {
this.logger.error('Error finding matching route', err);
}
}
// Apply CORS headers with route-specific settings if available
this.applyCorsHeaders(res, req, matchingRoute);
// If this is an OPTIONS request, the response has already been ended in applyCorsHeaders
// so we should return early to avoid trying to set more headers
if (req.method === 'OPTIONS') {
// Increment metrics for OPTIONS requests too
if (this.metricsTracker) {
this.metricsTracker.incrementRequestsServed();
}
return;
}
// Apply default headers
this.applyDefaultHeaders(res);
// We already have the connection ID and routeContext from CORS handling
const connectionId = `http-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
// Create route context for function-based targets (if we don't already have one)
const routeContext = this.contextCreator.createHttpRouteContext(req, {
connectionId,
clientIp: req.socket.remoteAddress?.replace('::ffff:', '') || '0.0.0.0',
serverIp: req.socket.localAddress?.replace('::ffff:', '') || '0.0.0.0',
tlsVersion: req.socket.getTLSVersion?.() || undefined
});
// Check security restrictions if we have a matching route
if (matchingRoute) {
// Check IP filtering and rate limiting
if (!this.securityManager.isAllowed(matchingRoute, routeContext)) {
this.logger.warn(`Access denied for ${routeContext.clientIp} to ${matchingRoute.name || 'unnamed'}`);
res.statusCode = 403;
res.end('Forbidden: Access denied by security policy');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
// Check basic auth
if (matchingRoute.security?.basicAuth?.enabled) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
// No auth header provided - send 401 with WWW-Authenticate header
res.statusCode = 401;
const realm = matchingRoute.security.basicAuth.realm || 'Protected Area';
res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`);
res.end('Authentication Required');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
// Verify credentials
try {
const credentials = Buffer.from(authHeader.substring(6), 'base64').toString('utf-8');
const [username, password] = credentials.split(':');
if (!this.securityManager.checkBasicAuth(matchingRoute, username, password)) {
res.statusCode = 401;
const realm = matchingRoute.security.basicAuth.realm || 'Protected Area';
res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`);
res.end('Invalid Credentials');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
}
catch (err) {
this.logger.error(`Error verifying basic auth: ${err}`);
res.statusCode = 401;
res.end('Authentication Error');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
}
// Check JWT auth
if (matchingRoute.security?.jwtAuth?.enabled) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
// No auth header provided - send 401
res.statusCode = 401;
res.end('Authentication Required: JWT token missing');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
// Verify token
const token = authHeader.substring(7);
if (!this.securityManager.verifyJwtToken(matchingRoute, token)) {
res.statusCode = 401;
res.end('Invalid or Expired JWT');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
}
}
// If we found a matching route with forward action, select appropriate target
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
this.logger.debug(`Found matching route: ${matchingRoute.name || 'unnamed'}`);
// Select the appropriate target from the targets array
const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
port: routeContext.port,
path: routeContext.path,
headers: routeContext.headers,
method: routeContext.method
});
if (!selectedTarget) {
this.logger.error(`No matching target found for route ${matchingRoute.name}`);
req.socket.end();
return;
}
// Extract target information, resolving functions if needed
let targetHost;
let targetPort;
try {
// Check function cache for host and resolve or use cached value
if (typeof selectedTarget.host === 'function') {
// Generate a function ID for caching (use route name or ID if available)
const functionId = `host-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
// Check if we have a cached result
if (this.functionCache) {
const cachedHost = this.functionCache.getCachedHost(routeContext, functionId);
if (cachedHost !== undefined) {
targetHost = cachedHost;
this.logger.debug(`Using cached host value for ${functionId}`);
}
else {
// Resolve the function and cache the result
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
targetHost = resolvedHost;
// Cache the result
this.functionCache.cacheHost(routeContext, functionId, resolvedHost);
this.logger.debug(`Resolved and cached function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
}
}
else {
// No cache available, just resolve
const resolvedHost = selectedTarget.host(routeContext);
targetHost = resolvedHost;
this.logger.debug(`Resolved function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
}
}
else {
targetHost = selectedTarget.host;
}
// Check function cache for port and resolve or use cached value
if (typeof selectedTarget.port === 'function') {
// Generate a function ID for caching
const functionId = `port-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
// Check if we have a cached result
if (this.functionCache) {
const cachedPort = this.functionCache.getCachedPort(routeContext, functionId);
if (cachedPort !== undefined) {
targetPort = cachedPort;
this.logger.debug(`Using cached port value for ${functionId}`);
}
else {
// Resolve the function and cache the result
const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
targetPort = resolvedPort;
// Cache the result
this.functionCache.cachePort(routeContext, functionId, resolvedPort);
this.logger.debug(`Resolved and cached function-based port to: ${resolvedPort}`);
}
}
else {
// No cache available, just resolve
const resolvedPort = selectedTarget.port(routeContext);
targetPort = resolvedPort;
this.logger.debug(`Resolved function-based port to: ${resolvedPort}`);
}
}
else {
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port;
}
// Select a single host if an array was provided
const selectedHost = Array.isArray(targetHost)
? targetHost[Math.floor(Math.random() * targetHost.length)]
: targetHost;
// Create a destination for the connection pool
const destination = {
host: selectedHost,
port: targetPort
};
// Apply URL rewriting if configured
this.applyUrlRewriting(req, matchingRoute, routeContext);
// Apply header modifications if configured
this.applyRouteHeaderModifications(matchingRoute, req, res);
// Continue with handling using the resolved destination
HttpRequestHandler.handleHttpRequestWithDestination(req, res, destination, routeContext, startTime, this.logger, this.metricsTracker, matchingRoute // Pass the route config for additional processing
);
return;
}
catch (err) {
this.logger.error(`Error evaluating function-based target: ${err}`);
res.statusCode = 500;
res.end('Internal Server Error: Failed to evaluate target functions');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
}
// If no route was found, return 404
this.logger.warn(`No route configuration for host: ${req.headers.host}`);
res.statusCode = 404;
res.end('Not Found: No route configuration for this host');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
}
/**
* Handle HTTP/2 stream requests with function-based target support
*/
async handleHttp2(stream, headers) {
const startTime = Date.now();
// Create a connection ID for this HTTP/2 stream
const connectionId = `http2-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
// Get client IP and server IP from the socket
const socket = stream.session?.socket;
const clientIp = socket?.remoteAddress?.replace('::ffff:', '') || '0.0.0.0';
const serverIp = socket?.localAddress?.replace('::ffff:', '') || '0.0.0.0';
// Create route context for function-based targets
const routeContext = this.contextCreator.createHttp2RouteContext(stream, headers, {
connectionId,
clientIp,
serverIp
});
// Try to find a matching route using RouteManager
let matchingRoute = null;
if (this.routeManager) {
try {
const matchResult = this.routeManager.findMatchingRoute(toBaseContext(routeContext));
matchingRoute = matchResult?.route || null;
}
catch (err) {
this.logger.error('Error finding matching route for HTTP/2 request', err);
}
}
// If we found a matching route with forward action, select appropriate target
if (matchingRoute && matchingRoute.action.type === 'forward' && matchingRoute.action.targets && matchingRoute.action.targets.length > 0) {
this.logger.debug(`Found matching route for HTTP/2 request: ${matchingRoute.name || 'unnamed'}`);
// Select the appropriate target from the targets array
const selectedTarget = this.selectTarget(matchingRoute.action.targets, {
port: routeContext.port,
path: routeContext.path,
headers: routeContext.headers,
method: routeContext.method
});
if (!selectedTarget) {
this.logger.error(`No matching target found for route ${matchingRoute.name}`);
stream.respond({ ':status': 502 });
stream.end();
return;
}
// Extract target information, resolving functions if needed
let targetHost;
let targetPort;
try {
// Check function cache for host and resolve or use cached value
if (typeof selectedTarget.host === 'function') {
// Generate a function ID for caching (use route name or ID if available)
const functionId = `host-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
// Check if we have a cached result
if (this.functionCache) {
const cachedHost = this.functionCache.getCachedHost(routeContext, functionId);
if (cachedHost !== undefined) {
targetHost = cachedHost;
this.logger.debug(`Using cached host value for HTTP/2: ${functionId}`);
}
else {
// Resolve the function and cache the result
const resolvedHost = selectedTarget.host(toBaseContext(routeContext));
targetHost = resolvedHost;
// Cache the result
this.functionCache.cacheHost(routeContext, functionId, resolvedHost);
this.logger.debug(`Resolved and cached HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
}
}
else {
// No cache available, just resolve
const resolvedHost = selectedTarget.host(routeContext);
targetHost = resolvedHost;
this.logger.debug(`Resolved HTTP/2 function-based host to: ${Array.isArray(resolvedHost) ? resolvedHost.join(', ') : resolvedHost}`);
}
}
else {
targetHost = selectedTarget.host;
}
// Check function cache for port and resolve or use cached value
if (typeof selectedTarget.port === 'function') {
// Generate a function ID for caching
const functionId = `port-http2-${matchingRoute.id || matchingRoute.name || 'unnamed'}`;
// Check if we have a cached result
if (this.functionCache) {
const cachedPort = this.functionCache.getCachedPort(routeContext, functionId);
if (cachedPort !== undefined) {
targetPort = cachedPort;
this.logger.debug(`Using cached port value for HTTP/2: ${functionId}`);
}
else {
// Resolve the function and cache the result
const resolvedPort = selectedTarget.port(toBaseContext(routeContext));
targetPort = resolvedPort;
// Cache the result
this.functionCache.cachePort(routeContext, functionId, resolvedPort);
this.logger.debug(`Resolved and cached HTTP/2 function-based port to: ${resolvedPort}`);
}
}
else {
// No cache available, just resolve
const resolvedPort = selectedTarget.port(routeContext);
targetPort = resolvedPort;
this.logger.debug(`Resolved HTTP/2 function-based port to: ${resolvedPort}`);
}
}
else {
targetPort = selectedTarget.port === 'preserve' ? routeContext.port : selectedTarget.port;
}
// Select a single host if an array was provided
const selectedHost = Array.isArray(targetHost)
? targetHost[Math.floor(Math.random() * targetHost.length)]
: targetHost;
// Create a destination for forwarding
const destination = {
host: selectedHost,
port: targetPort
};
// Handle HTTP/2 stream based on backend protocol
const backendProtocol = matchingRoute.action.options?.backendProtocol || this.options.backendProtocol;
if (backendProtocol === 'http2') {
// Forward to HTTP/2 backend
return Http2RequestHandler.handleHttp2WithHttp2Destination(stream, headers, destination, routeContext, this.h2Sessions, this.logger, this.metricsTracker);
}
else {
// Forward to HTTP/1.1 backend
return Http2RequestHandler.handleHttp2WithHttp1Destination(stream, headers, destination, routeContext, this.logger, this.metricsTracker);
}
}
catch (err) {
this.logger.error(`Error evaluating function-based target for HTTP/2: ${err}`);
stream.respond({ ':status': 500 });
stream.end('Internal Server Error: Failed to evaluate target functions');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
return;
}
}
// Fall back to legacy routing if no matching route found
const method = headers[':method'] || 'GET';
const path = headers[':path'] || '/';
// No route was found
stream.respond({ ':status': 404 });
stream.end('Not Found: No route configuration for this request');
if (this.metricsTracker)
this.metricsTracker.incrementFailedRequests();
}
/**
* Cleanup resources and stop intervals
*/
destroy() {
if (this.rateLimitCleanupInterval) {
clearInterval(this.rateLimitCleanupInterval);
this.rateLimitCleanupInterval = null;
}
// Close all HTTP/2 sessions
for (const [key, session] of this.h2Sessions) {
session.close();
}
this.h2Sessions.clear();
// Clear function cache if it has a destroy method
if (this.functionCache && typeof this.functionCache.destroy === 'function') {
this.functionCache.destroy();
}
this.logger.debug('RequestHandler destroyed');
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVxdWVzdC1oYW5kbGVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvcHJveGllcy9odHRwLXByb3h5L3JlcXVlc3QtaGFuZGxlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sMENBQTBDLENBQUM7QUFDbEQsT0FBTyxFQUdMLFlBQVksR0FDYixNQUFNLG1CQUFtQixDQUFDO0FBQzNCLE9BQU8sRUFBRSxrQkFBa0IsSUFBSSxZQUFZLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQztBQUN6RixPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDdEQsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQy9ELE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBR2pFLE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxvQ0FBb0MsQ0FBQztBQUNuRSxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sb0NBQW9DLENBQUM7QUFDbkUsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBYXhEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLGNBQWM7SUFnQnpCLFlBQ1UsT0FBMEIsRUFDMUIsY0FBOEIsRUFDOUIsWUFBMkIsRUFDM0IsYUFBbUIsRUFBRSx5REFBeUQ7SUFDOUUsTUFBWSxDQUFDLHNEQUFzRDs7UUFKbkUsWUFBTyxHQUFQLE9BQU8sQ0FBbUI7UUFDMUIsbUJBQWMsR0FBZCxjQUFjLENBQWdCO1FBQzlCLGlCQUFZLEdBQVosWUFBWSxDQUFlO1FBQzNCLGtCQUFhLEdBQWIsYUFBYSxDQUFNO1FBQ25CLFdBQU0sR0FBTixNQUFNLENBQU07UUFwQmQsbUJBQWMsR0FBOEIsRUFBRSxDQUFDO1FBRS9DLG1C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