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:
Philipp Kunz 2025-04-19 18:42:36 +00:00
parent d8383311be
commit 46214f5380
4 changed files with 51 additions and 39 deletions

View File

@ -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)

View File

@ -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.'
} }

View File

@ -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;

View File

@ -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';
} }
/** /**