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.
This commit is contained in:
		| @@ -1,5 +1,13 @@ | |||||||
| # Changelog | # 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) | ## 2025-04-19 - 7.1.1 - fix(commit-info) | ||||||
| Update commit metadata and synchronize project configuration (no code changes) | Update commit metadata and synchronize project configuration (no code changes) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@push.rocks/smartproxy', |   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.' |   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.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -132,10 +132,18 @@ export class RequestHandler { | |||||||
|      |      | ||||||
|     // Apply default headers |     // Apply default headers | ||||||
|     this.applyDefaultHeaders(res); |     this.applyDefaultHeaders(res); | ||||||
|     // If configured to proxy to backends over HTTP/2, use HTTP/2 client sessions |      | ||||||
|     if (this.options.backendProtocol === 'http2') { |     // Determine routing configuration | ||||||
|       // Route and validate config |     let proxyConfig: IReverseProxyConfig | undefined; | ||||||
|       const proxyConfig = this.router.routeReq(req); |     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) { |     if (!proxyConfig) { | ||||||
|       this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); |       this.logger.warn(`No proxy configuration for host: ${req.headers.host}`); | ||||||
|       res.statusCode = 404; |       res.statusCode = 404; | ||||||
| @@ -143,12 +151,13 @@ export class RequestHandler { | |||||||
|       if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); |       if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|       // Determine backend target |     // Determine protocol to backend (per-domain override or global) | ||||||
|  |     const backendProto = proxyConfig.backendProtocol || this.options.backendProtocol; | ||||||
|  |     if (backendProto === 'http2') { | ||||||
|       const destination = this.connectionPool.getNextTarget( |       const destination = this.connectionPool.getNextTarget( | ||||||
|         proxyConfig.destinationIps, |         proxyConfig.destinationIps, | ||||||
|         proxyConfig.destinationPorts[0] |         proxyConfig.destinationPorts[0] | ||||||
|       ); |       ); | ||||||
|       // Obtain or create HTTP/2 session |  | ||||||
|       const key = `${destination.host}:${destination.port}`; |       const key = `${destination.host}:${destination.port}`; | ||||||
|       let session = this.h2Sessions.get(key); |       let session = this.h2Sessions.get(key); | ||||||
|       if (!session || session.closed || (session as any).destroyed) { |       if (!session || session.closed || (session as any).destroyed) { | ||||||
| @@ -158,40 +167,30 @@ export class RequestHandler { | |||||||
|         session.on('close', () => this.h2Sessions.delete(key)); |         session.on('close', () => this.h2Sessions.delete(key)); | ||||||
|       } |       } | ||||||
|       // Build headers for HTTP/2 request |       // Build headers for HTTP/2 request | ||||||
|       const h2Headers: Record<string, string> = { |       const hdrs: Record<string, any> = { | ||||||
|         ':method': req.method || 'GET', |         ':method': req.method, | ||||||
|         ':path': req.url || '/', |         ':path': req.url, | ||||||
|         ':authority': `${destination.host}:${destination.port}` |         ':authority': `${destination.host}:${destination.port}` | ||||||
|       }; |       }; | ||||||
|       for (const [k, v] of Object.entries(req.headers)) { |       for (const [hk, hv] of Object.entries(req.headers)) { | ||||||
|         if (typeof v === 'string' && !k.startsWith(':')) { |         if (typeof hv === 'string') hdrs[hk] = hv; | ||||||
|           h2Headers[k] = v; |  | ||||||
|       } |       } | ||||||
|       } |       const h2Stream = session.request(hdrs); | ||||||
|       // Open HTTP/2 stream |  | ||||||
|       const h2Stream = session.request(h2Headers); |  | ||||||
|       // Pipe client request body to backend |  | ||||||
|       req.pipe(h2Stream); |       req.pipe(h2Stream); | ||||||
|       // Handle backend response |       h2Stream.on('response', (hdrs2: any) => { | ||||||
|       h2Stream.on('response', (headers, flags) => { |         const status = (hdrs2[':status'] as number) || 502; | ||||||
|         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); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         res.statusCode = status; |         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[]); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|         h2Stream.pipe(res); |         h2Stream.pipe(res); | ||||||
|       }); |       }); | ||||||
|       h2Stream.on('error', (err) => { |       h2Stream.on('error', (err) => { | ||||||
|         this.logger.error(`HTTP/2 proxy error: ${err.message}`); |  | ||||||
|         if (!res.headersSent) { |  | ||||||
|         res.statusCode = 502; |         res.statusCode = 502; | ||||||
|         res.end(`Bad Gateway: ${err.message}`); |         res.end(`Bad Gateway: ${err.message}`); | ||||||
|         } else { |  | ||||||
|           res.end(); |  | ||||||
|         } |  | ||||||
|         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); |         if (this.metricsTracker) this.metricsTracker.incrementFailedRequests(); | ||||||
|       }); |       }); | ||||||
|       return; |       return; | ||||||
|   | |||||||
| @@ -60,6 +60,11 @@ export interface IReverseProxyConfig { | |||||||
|     pass: string; |     pass: string; | ||||||
|   }; |   }; | ||||||
|   rewriteHostHeader?: boolean; |   rewriteHostHeader?: boolean; | ||||||
|  |   /** | ||||||
|  |    * Protocol to use when proxying to this backend: 'http1' or 'http2'. | ||||||
|  |    * Overrides the global backendProtocol option if set. | ||||||
|  |    */ | ||||||
|  |   backendProtocol?: 'http1' | 'http2'; | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user