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
|
||||
|
||||
## 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)
|
||||
|
||||
|
@ -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.'
|
||||
}
|
||||
|
@ -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<string, string> = {
|
||||
':method': req.method || 'GET',
|
||||
':path': req.url || '/',
|
||||
const hdrs: Record<string, any> = {
|
||||
':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;
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user