From 46214f5380f611f78c770fe0a211f31a05c76051 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Sat, 19 Apr 2025 18:42:36 +0000 Subject: [PATCH] fix(networkproxy/requesthandler): Improve HTTP/2 request handling and error management in the proxy request handler; add try-catch around routing and update header processing to support per-backend protocol overrides. --- changelog.md | 8 +++ ts/00_commitinfo_data.ts | 2 +- ts/networkproxy/classes.np.requesthandler.ts | 75 ++++++++++---------- ts/networkproxy/classes.np.types.ts | 5 ++ 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/changelog.md b/changelog.md index 2a9430b..f7b0d6a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-04-19 - 7.1.2 - fix(networkproxy/requesthandler) +Improve HTTP/2 request handling and error management in the proxy request handler; add try-catch around routing and update header processing to support per-backend protocol overrides. + +- Wrapped the routing call (router.routeReq) in a try-catch block to better handle errors and missing host headers. +- Returns a 500 error and increments failure metrics if routing fails. +- Refactored HTTP/2 branch to copy all headers appropriately and map response headers into HTTP/1 response. +- Added support for per-backend protocol override via the new backendProtocol option in IReverseProxyConfig. + ## 2025-04-19 - 7.1.1 - fix(commit-info) Update commit metadata and synchronize project configuration (no code changes) diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 50ebad8..7d10300 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartproxy', - version: '7.1.1', + version: '7.1.2', description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, dynamic routing with authentication options, and automatic ACME certificate management.' } diff --git a/ts/networkproxy/classes.np.requesthandler.ts b/ts/networkproxy/classes.np.requesthandler.ts index bdf56d7..b87aab4 100644 --- a/ts/networkproxy/classes.np.requesthandler.ts +++ b/ts/networkproxy/classes.np.requesthandler.ts @@ -132,23 +132,32 @@ export class RequestHandler { // Apply default headers this.applyDefaultHeaders(res); - // If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions - if (this.options.backendProtocol === 'http2') { - // Route and validate config - const proxyConfig = this.router.routeReq(req); - if (!proxyConfig) { - this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); - res.statusCode = 404; - res.end('Not Found: No proxy configuration for this host'); - if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); - return; - } - // Determine backend target + + // Determine routing configuration + let proxyConfig: IReverseProxyConfig | undefined; + try { + proxyConfig = this.router.routeReq(req); + } catch (err) { + this.logger.error('Error routing request', err); + res.statusCode = 500; + res.end('Internal Server Error'); + if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); + return; + } + if (!proxyConfig) { + this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); + res.statusCode = 404; + res.end('Not Found: No proxy configuration for this host'); + if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); + return; + } + // Determine protocol to backend (per-domain override or global) + const backendProto = proxyConfig.backendProtocol || this.options.backendProtocol; + if (backendProto === 'http2') { const destination = this.connectionPool.getNextTarget( proxyConfig.destinationIps, proxyConfig.destinationPorts[0] ); - // Obtain or create HTTP/2 session const key = `${destination.host}:${destination.port}`; let session = this.h2Sessions.get(key); if (!session || session.closed || (session as any).destroyed) { @@ -158,40 +167,30 @@ export class RequestHandler { session.on('close', () => this.h2Sessions.delete(key)); } // Build headers for HTTP/2 request - const h2Headers: Record = { - ':method': req.method || 'GET', - ':path': req.url || '/', + const hdrs: Record = { + ':method': req.method, + ':path': req.url, ':authority': `${destination.host}:${destination.port}` }; - for (const [k, v] of Object.entries(req.headers)) { - if (typeof v === 'string' && !k.startsWith(':')) { - h2Headers[k] = v; - } + for (const [hk, hv] of Object.entries(req.headers)) { + if (typeof hv === 'string') hdrs[hk] = hv; } - // Open HTTP/2 stream - const h2Stream = session.request(h2Headers); - // Pipe client request body to backend + const h2Stream = session.request(hdrs); req.pipe(h2Stream); - // Handle backend response - h2Stream.on('response', (headers, flags) => { - const status = headers[':status'] as number || 502; - // Map headers - for (const [hk, hv] of Object.entries(headers)) { - if (!hk.startsWith(':') && hv) { - res.setHeader(hk, hv as string); + h2Stream.on('response', (hdrs2: any) => { + const status = (hdrs2[':status'] as number) || 502; + res.statusCode = status; + // Copy headers from HTTP/2 response to HTTP/1 response + for (const [hk, hv] of Object.entries(hdrs2)) { + if (!hk.startsWith(':') && hv != null) { + res.setHeader(hk, hv as string | string[]); } } - res.statusCode = status; h2Stream.pipe(res); }); h2Stream.on('error', (err) => { - this.logger.error(`HTTP/2 proxy error: ${err.message}`); - if (!res.headersSent) { - res.statusCode = 502; - res.end(`Bad Gateway: ${err.message}`); - } else { - res.end(); - } + res.statusCode = 502; + res.end(`Bad Gateway: ${err.message}`); if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); }); return; diff --git a/ts/networkproxy/classes.np.types.ts b/ts/networkproxy/classes.np.types.ts index 02fc2bc..9ffd56b 100644 --- a/ts/networkproxy/classes.np.types.ts +++ b/ts/networkproxy/classes.np.types.ts @@ -60,6 +60,11 @@ export interface IReverseProxyConfig { pass: string; }; rewriteHostHeader?: boolean; + /** + * Protocol to use when proxying to this backend: 'http1' or 'http2'. + * Overrides the global backendProtocol option if set. + */ + backendProtocol?: 'http1' | 'http2'; } /**