fix(http-proxy): avoid repeated HTTP/3 recaching after QUIC fallback and document backend protocol selection
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-19 - 25.16.1 - fix(http-proxy)
|
||||
avoid repeated HTTP/3 recaching after QUIC fallback and document backend protocol selection
|
||||
|
||||
- Suppress Alt-Svc HTTP/3 recaching after a failed QUIC backend connection to prevent repeated H3 timeout fallback loops
|
||||
- Force an ALPN probe on TCP fallback so auto detection correctly reselects HTTP/2 or HTTP/1.1 after H3 connection failure
|
||||
- Add README documentation for best-effort backendProtocol selection and supported protocol modes
|
||||
|
||||
## 2026-03-19 - 25.16.0 - feat(quic,http3)
|
||||
add HTTP/3 proxy handling and hot-reload QUIC TLS configuration
|
||||
|
||||
|
||||
57
readme.md
57
readme.md
@@ -328,6 +328,41 @@ const proxy = new SmartProxy({
|
||||
});
|
||||
```
|
||||
|
||||
### 🚄 Best-Effort Backend Protocol (H3 > H2 > H1)
|
||||
|
||||
SmartProxy automatically uses the **highest protocol your backend supports** for HTTP requests. The backend protocol is independent of the client protocol — a client using HTTP/1.1 can be forwarded over HTTP/3 to the backend, and vice versa.
|
||||
|
||||
```typescript
|
||||
const route: IRouteConfig = {
|
||||
name: 'auto-protocol',
|
||||
match: { ports: 443, domains: 'app.example.com' },
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [{ host: 'backend', port: 8443 }],
|
||||
tls: { mode: 'terminate', certificate: 'auto' },
|
||||
options: {
|
||||
backendProtocol: 'auto' // 👈 Default — best-effort selection
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**How protocol discovery works (browser model):**
|
||||
|
||||
1. First request → TLS ALPN probe detects H2 or H1
|
||||
2. Backend response inspected for `Alt-Svc: h3=":port"` header
|
||||
3. If H3 advertised → cached and used for subsequent requests via QUIC
|
||||
4. Graceful fallback: H3 failure → H2 → H1 with automatic cache invalidation
|
||||
|
||||
| `backendProtocol` | Behavior |
|
||||
|---|---|
|
||||
| `'auto'` (default) | Best-effort: H3 > H2 > H1 with Alt-Svc discovery |
|
||||
| `'http1'` | Always HTTP/1.1 |
|
||||
| `'http2'` | Always HTTP/2 (hard-fail if unsupported) |
|
||||
| `'http3'` | Always HTTP/3 via QUIC (hard-fail if unsupported) |
|
||||
|
||||
> **Note:** WebSocket upgrades always use HTTP/1.1 to the backend regardless of `backendProtocol`, since there's no performance benefit from H2/H3 Extended CONNECT for tunneled connections, and backend support is rare.
|
||||
|
||||
### 🔁 Dual-Stack TCP + UDP Route
|
||||
|
||||
Listen on both TCP and UDP with a single route — handle each transport with its own handler:
|
||||
@@ -776,6 +811,28 @@ interface IRouteLoadBalancing {
|
||||
}
|
||||
```
|
||||
|
||||
### Backend Protocol Options
|
||||
|
||||
```typescript
|
||||
// Set on action.options
|
||||
{
|
||||
action: {
|
||||
type: 'forward',
|
||||
targets: [...],
|
||||
options: {
|
||||
backendProtocol: 'auto' | 'http1' | 'http2' | 'http3'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Value | Backend Behavior |
|
||||
|-------|-----------------|
|
||||
| `'auto'` | Best-effort: discovers H3 via Alt-Svc, probes H2 via ALPN, falls back to H1 |
|
||||
| `'http1'` | Always HTTP/1.1 (no ALPN probe) |
|
||||
| `'http2'` | Always HTTP/2 (hard-fail if handshake fails) |
|
||||
| `'http3'` | Always HTTP/3 over QUIC (3s connect timeout, hard-fail if unreachable) |
|
||||
|
||||
### UDP & QUIC Options
|
||||
|
||||
```typescript
|
||||
|
||||
@@ -696,13 +696,17 @@ impl HttpProxyService {
|
||||
};
|
||||
|
||||
// Derive legacy flags for the existing H1/H2 connection path
|
||||
let (use_h2, needs_alpn_probe) = match &protocol_decision {
|
||||
let (use_h2, mut needs_alpn_probe) = match &protocol_decision {
|
||||
ProtocolDecision::H1 => (false, false),
|
||||
ProtocolDecision::H2 => (true, false),
|
||||
ProtocolDecision::H3 { .. } => (false, false), // H3 path handled separately below
|
||||
ProtocolDecision::AlpnProbe => (false, true),
|
||||
};
|
||||
|
||||
// Track whether H3 connect failed — suppresses Alt-Svc re-caching to prevent
|
||||
// the loop: H3 cached → QUIC timeout → H2/H1 fallback → Alt-Svc re-caches H3 → repeat
|
||||
let mut h3_connect_failed = false;
|
||||
|
||||
// --- H3 path: try QUIC connection before TCP ---
|
||||
if let ProtocolDecision::H3 { port: h3_port } = protocol_decision {
|
||||
let h3_pool_key = crate::connection_pool::PoolKey {
|
||||
@@ -738,14 +742,13 @@ impl HttpProxyService {
|
||||
Err(e) => {
|
||||
warn!(backend = %upstream_key, error = %e,
|
||||
"H3 backend connect failed, falling back to H2/H1");
|
||||
// Invalidate H3 from cache — next request will ALPN probe for H2/H1
|
||||
if is_auto_detect_mode {
|
||||
self.protocol_cache.insert(
|
||||
protocol_cache_key.clone(),
|
||||
crate::protocol_cache::DetectedProtocol::H1,
|
||||
);
|
||||
h3_connect_failed = true;
|
||||
// Force ALPN probe on TCP fallback so we correctly detect H2 vs H1
|
||||
// (don't cache anything yet — let the ALPN probe decide)
|
||||
if is_auto_detect_mode && upstream.use_tls {
|
||||
needs_alpn_probe = true;
|
||||
}
|
||||
// Fall through to TCP path (ALPN probe for auto, or H1 for explicit)
|
||||
// Fall through to TCP path
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -946,7 +949,11 @@ impl HttpProxyService {
|
||||
self.metrics.backend_connection_closed(&upstream_key);
|
||||
|
||||
// --- Alt-Svc discovery: check if backend advertises H3 ---
|
||||
if is_auto_detect_mode {
|
||||
// Suppress Alt-Svc caching when we just failed an H3 attempt to prevent the loop:
|
||||
// H3 cached → QUIC timeout → fallback → Alt-Svc re-caches H3 → repeat.
|
||||
// The ALPN probe already cached H1 or H2; it will expire after 5min TTL,
|
||||
// at which point we'll re-probe and see Alt-Svc again, retrying QUIC then.
|
||||
if is_auto_detect_mode && !h3_connect_failed {
|
||||
if let Ok(ref resp) = result {
|
||||
if let Some(alt_svc) = resp.headers().get("alt-svc").and_then(|v| v.to_str().ok()) {
|
||||
if let Some(h3_port) = parse_alt_svc_h3_port(alt_svc) {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '25.16.0',
|
||||
version: '25.16.1',
|
||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user