import * as plugins from '../../plugins.js'; import type { IHttpRouteContext } from '../../core/models/route-context.js'; import type { ILogger } from './models/types.js'; import type { IMetricsTracker } from './request-handler.js'; /** * HTTP/2 Request Handler Helper - handles HTTP/2 streams with specific destinations * This is a helper class for the main RequestHandler */ export class Http2RequestHandler { /** * Handle HTTP/2 stream with direct HTTP/2 backend */ public static async handleHttp2WithHttp2Destination( stream: plugins.http2.ServerHttp2Stream, headers: plugins.http2.IncomingHttpHeaders, destination: { host: string, port: number }, routeContext: IHttpRouteContext, sessions: Map, logger: ILogger, metricsTracker?: IMetricsTracker | null ): Promise { const key = `${destination.host}:${destination.port}`; // Get or create a client HTTP/2 session let session = sessions.get(key); if (!session || session.closed || (session as any).destroyed) { try { // Connect to the backend HTTP/2 server session = plugins.http2.connect(`http://${destination.host}:${destination.port}`); sessions.set(key, session); // Handle session errors and cleanup session.on('error', (err) => { logger.error(`HTTP/2 session error to ${key}: ${err.message}`); sessions.delete(key); }); session.on('close', () => { logger.debug(`HTTP/2 session closed to ${key}`); sessions.delete(key); }); } catch (err) { logger.error(`Failed to establish HTTP/2 session to ${key}: ${err.message}`); stream.respond({ ':status': 502 }); stream.end('Bad Gateway: Failed to establish connection to backend'); if (metricsTracker) metricsTracker.incrementFailedRequests(); return; } } try { // Build headers for backend HTTP/2 request const h2Headers: Record = { ':method': headers[':method'], ':path': headers[':path'], ':authority': `${destination.host}:${destination.port}` }; // Copy other headers, excluding pseudo-headers for (const [key, value] of Object.entries(headers)) { if (!key.startsWith(':') && typeof value === 'string') { h2Headers[key] = value; } } logger.debug( `Proxying HTTP/2 request to ${destination.host}:${destination.port}${headers[':path']}`, { method: headers[':method'] } ); // Create HTTP/2 request stream to the backend const h2Stream = session.request(h2Headers); // Pipe client stream to backend stream stream.pipe(h2Stream); // Handle responses from the backend h2Stream.on('response', (responseHeaders) => { // Map status and headers to client response const resp: Record = { ':status': responseHeaders[':status'] as number }; // Copy non-pseudo headers for (const [key, value] of Object.entries(responseHeaders)) { if (!key.startsWith(':') && value !== undefined) { resp[key] = value; } } // Send headers to client stream.respond(resp); // Pipe backend response to client h2Stream.pipe(stream); // Track successful requests stream.on('end', () => { if (metricsTracker) metricsTracker.incrementRequestsServed(); logger.debug( `HTTP/2 request completed: ${headers[':method']} ${headers[':path']} ${responseHeaders[':status']}`, { method: headers[':method'], status: responseHeaders[':status'] } ); }); }); // Handle backend errors h2Stream.on('error', (err) => { logger.error(`HTTP/2 stream error: ${err.message}`); // Only send error response if headers haven't been sent if (!stream.headersSent) { stream.respond({ ':status': 502 }); stream.end(`Bad Gateway: ${err.message}`); } else { stream.end(); } if (metricsTracker) metricsTracker.incrementFailedRequests(); }); // Handle client stream errors stream.on('error', (err) => { logger.debug(`Client HTTP/2 stream error: ${err.message}`); h2Stream.destroy(); if (metricsTracker) metricsTracker.incrementFailedRequests(); }); } catch (err: any) { logger.error(`Error handling HTTP/2 request: ${err.message}`); // Only send error response if headers haven't been sent if (!stream.headersSent) { stream.respond({ ':status': 500 }); stream.end('Internal Server Error'); } else { stream.end(); } if (metricsTracker) metricsTracker.incrementFailedRequests(); } } /** * Handle HTTP/2 stream with HTTP/1 backend */ public static async handleHttp2WithHttp1Destination( stream: plugins.http2.ServerHttp2Stream, headers: plugins.http2.IncomingHttpHeaders, destination: { host: string, port: number }, routeContext: IHttpRouteContext, logger: ILogger, metricsTracker?: IMetricsTracker | null ): Promise { try { // Build headers for HTTP/1 proxy request, excluding HTTP/2 pseudo-headers const outboundHeaders: Record = {}; for (const [key, value] of Object.entries(headers)) { if (typeof key === 'string' && typeof value === 'string' && !key.startsWith(':')) { outboundHeaders[key] = value; } } // Always rewrite host header to match target outboundHeaders.host = `${destination.host}:${destination.port}`; logger.debug( `Proxying HTTP/2 request to HTTP/1 backend ${destination.host}:${destination.port}${headers[':path']}`, { method: headers[':method'] } ); // Create HTTP/1 proxy request const proxyReq = plugins.http.request( { hostname: destination.host, port: destination.port, path: headers[':path'] as string, method: headers[':method'] as string, headers: outboundHeaders }, (proxyRes) => { // Map status and headers back to HTTP/2 const responseHeaders: Record = { ':status': proxyRes.statusCode || 500 }; // Copy headers from HTTP/1 response to HTTP/2 response for (const [key, value] of Object.entries(proxyRes.headers)) { if (value !== undefined) { responseHeaders[key] = value as string | string[]; } } // Send headers to client stream.respond(responseHeaders); // Pipe HTTP/1 response to HTTP/2 stream proxyRes.pipe(stream); // Clean up when client disconnects stream.on('close', () => proxyReq.destroy()); stream.on('error', () => proxyReq.destroy()); // Track successful requests stream.on('end', () => { if (metricsTracker) metricsTracker.incrementRequestsServed(); logger.debug( `HTTP/2 to HTTP/1 request completed: ${headers[':method']} ${headers[':path']} ${proxyRes.statusCode}`, { method: headers[':method'], status: proxyRes.statusCode } ); }); } ); // Handle proxy request errors proxyReq.on('error', (err) => { logger.error(`HTTP/1 proxy error: ${err.message}`); // Only send error response if headers haven't been sent if (!stream.headersSent) { stream.respond({ ':status': 502 }); stream.end(`Bad Gateway: ${err.message}`); } else { stream.end(); } if (metricsTracker) metricsTracker.incrementFailedRequests(); }); // Pipe client stream to proxy request stream.pipe(proxyReq); // Handle client stream errors stream.on('error', (err) => { logger.debug(`Client HTTP/2 stream error: ${err.message}`); proxyReq.destroy(); if (metricsTracker) metricsTracker.incrementFailedRequests(); }); } catch (err: any) { logger.error(`Error handling HTTP/2 to HTTP/1 request: ${err.message}`); // Only send error response if headers haven't been sent if (!stream.headersSent) { stream.respond({ ':status': 500 }); stream.end('Internal Server Error'); } else { stream.end(); } if (metricsTracker) metricsTracker.incrementFailedRequests(); } } }