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:
parent
d8383311be
commit
46214f5380
@ -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,23 +132,32 @@ 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 {
|
||||||
if (!proxyConfig) {
|
proxyConfig = this.router.routeReq(req);
|
||||||
this.logger.warn(`No proxy configuration for host: ${req.headers.host}`);
|
} catch (err) {
|
||||||
res.statusCode = 404;
|
this.logger.error('Error routing request', err);
|
||||||
res.end('Not Found: No proxy configuration for this host');
|
res.statusCode = 500;
|
||||||
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
res.end('Internal Server Error');
|
||||||
return;
|
if (this.metricsTracker) this.metricsTracker.incrementFailedRequests();
|
||||||
}
|
return;
|
||||||
// Determine backend target
|
}
|
||||||
|
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(
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Open HTTP/2 stream
|
const h2Stream = session.request(hdrs);
|
||||||
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;
|
res.statusCode = status;
|
||||||
// Map headers
|
// Copy headers from HTTP/2 response to HTTP/1 response
|
||||||
for (const [hk, hv] of Object.entries(headers)) {
|
for (const [hk, hv] of Object.entries(hdrs2)) {
|
||||||
if (!hk.startsWith(':') && hv) {
|
if (!hk.startsWith(':') && hv != null) {
|
||||||
res.setHeader(hk, hv as string);
|
res.setHeader(hk, hv as string | string[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.statusCode = status;
|
|
||||||
h2Stream.pipe(res);
|
h2Stream.pipe(res);
|
||||||
});
|
});
|
||||||
h2Stream.on('error', (err) => {
|
h2Stream.on('error', (err) => {
|
||||||
this.logger.error(`HTTP/2 proxy error: ${err.message}`);
|
res.statusCode = 502;
|
||||||
if (!res.headersSent) {
|
res.end(`Bad Gateway: ${err.message}`);
|
||||||
res.statusCode = 502;
|
|
||||||
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user