Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfff128499 | |||
| 3baff354bd | |||
| c2eacd1b30 | |||
| 1fdbfcf0aa |
14
changelog.md
14
changelog.md
@@ -1,5 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-17 - 25.11.20 - fix(rustproxy-http)
|
||||||
|
avoid downgrading cached backend protocol on H2 stream errors
|
||||||
|
|
||||||
|
- Treat HTTP/2 stream-level failures as retryable request errors instead of evidence that the backend only supports HTTP/1.1
|
||||||
|
- Keep protocol cache entries unchanged after successful H2 handshakes so future requests continue using HTTP/2
|
||||||
|
- Lower log severity for this fallback path from warning to debug while still recording backend H2 failure metrics
|
||||||
|
|
||||||
|
## 2026-03-16 - 25.11.19 - fix(rustproxy-http)
|
||||||
|
avoid reusing pooled HTTP/2 connections for requests with bodies to prevent upload flow-control stalls
|
||||||
|
|
||||||
|
- Limit HTTP/2 pool checkout to bodyless requests such as GET, HEAD, and DELETE
|
||||||
|
- Skip re-registering HTTP/2 connections in the pool after requests that send a body
|
||||||
|
- Prevent stalled uploads caused by depleted connection-level flow control windows on reused HTTP/2 connections
|
||||||
|
|
||||||
## 2026-03-16 - 25.11.18 - fix(repo)
|
## 2026-03-16 - 25.11.18 - fix(repo)
|
||||||
no changes to commit
|
no changes to commit
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "25.11.18",
|
"version": "25.11.20",
|
||||||
"private": false,
|
"private": false,
|
||||||
"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.",
|
"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.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -677,12 +677,20 @@ impl HttpProxyService {
|
|||||||
h2: use_h2,
|
h2: use_h2,
|
||||||
};
|
};
|
||||||
|
|
||||||
// H2 pool checkout with async readiness validation.
|
// H2 pool checkout — only for bodyless requests (GET/HEAD/DELETE).
|
||||||
// checkout_h2 does synchronous is_closed()/is_ready() checks, but these
|
//
|
||||||
// reflect cached state — the H2 connection driver (a separate tokio task)
|
// WORKAROUND: Requests with bodies (POST/PUT uploads) always get fresh H2
|
||||||
// may not have processed a pending GOAWAY/RST yet. The ready().await
|
// connections. Reusing a pooled H2 connection after a large upload can stall
|
||||||
// forces the runtime to yield, giving the driver a chance to detect failures.
|
// forever due to depleted connection-level flow control windows. The h2 crate
|
||||||
if use_h2 {
|
// has no stall/timeout detection (https://github.com/hyperium/hyper/issues/2899),
|
||||||
|
// and Go/nginx HTTP/2 servers have known issues with connection-level window
|
||||||
|
// replenishment after large transfers (https://github.com/golang/go/issues/16481,
|
||||||
|
// https://github.com/golang/go/issues/56558). A fresh connection guarantees
|
||||||
|
// clean flow control state. The overhead is ~3-5ms for TLS+H2 handshake.
|
||||||
|
//
|
||||||
|
// TODO: Revisit once h2 crate adds flow control stall detection, or once
|
||||||
|
// Go/nginx H2 connection-level window handling is confirmed reliable.
|
||||||
|
if use_h2 && body.is_end_stream() {
|
||||||
if let Some((mut sender, age)) = self.connection_pool.checkout_h2(&pool_key) {
|
if let Some((mut sender, age)) = self.connection_pool.checkout_h2(&pool_key) {
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(500),
|
std::time::Duration::from_millis(500),
|
||||||
@@ -1040,10 +1048,12 @@ impl HttpProxyService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone sender for potential pool registration; register only after first request succeeds
|
// Only pool the H2 connection if the request had no body.
|
||||||
|
// Requests with bodies (uploads) deplete connection-level flow control windows.
|
||||||
|
let request_had_body = !body.is_end_stream();
|
||||||
let sender_for_pool = sender.clone();
|
let sender_for_pool = sender.clone();
|
||||||
let result = self.forward_h2_with_sender(sender, parts, body, upstream_headers, upstream_path, route, route_id, source_ip, Some(pool_key), domain, conn_activity).await;
|
let result = self.forward_h2_with_sender(sender, parts, body, upstream_headers, upstream_path, route, route_id, source_ip, Some(pool_key), domain, conn_activity).await;
|
||||||
if matches!(&result, Ok(ref resp) if resp.status() != StatusCode::BAD_GATEWAY) {
|
if !request_had_body && matches!(&result, Ok(ref resp) if resp.status() != StatusCode::BAD_GATEWAY) {
|
||||||
let g = self.connection_pool.register_h2(pool_key.clone(), sender_for_pool);
|
let g = self.connection_pool.register_h2(pool_key.clone(), sender_for_pool);
|
||||||
gen_holder.store(g, std::sync::atomic::Ordering::Relaxed);
|
gen_holder.store(g, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
@@ -1369,29 +1379,28 @@ impl HttpProxyService {
|
|||||||
|
|
||||||
match sender.send_request(upstream_req).await {
|
match sender.send_request(upstream_req).await {
|
||||||
Ok(upstream_response) => {
|
Ok(upstream_response) => {
|
||||||
// H2 works! Register sender in pool for multiplexed reuse
|
// Only pool after bodyless requests — uploads deplete connection-level
|
||||||
let g = self.connection_pool.register_h2(pool_key.clone(), sender);
|
// flow control windows (see comment at pool checkout above).
|
||||||
gen_holder.store(g, std::sync::atomic::Ordering::Relaxed);
|
if retry_state.is_some() {
|
||||||
|
let g = self.connection_pool.register_h2(pool_key.clone(), sender);
|
||||||
|
gen_holder.store(g, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
self.build_streaming_response(upstream_response, route, route_id, source_ip, conn_activity).await
|
self.build_streaming_response(upstream_response, route, route_id, source_ip, conn_activity).await
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// H2 request failed — backend advertises h2 via ALPN but doesn't
|
// H2 request failed on a stream level (e.g. RST_STREAM PROTOCOL_ERROR).
|
||||||
// actually speak it. Update cache so future requests use H1.
|
// The H2 handshake succeeded, so the backend genuinely speaks H2 — don't
|
||||||
|
// poison the protocol cache. Only handshake-level failures (below) should
|
||||||
|
// downgrade the cache to H1.
|
||||||
let bk = format!("{}:{}", upstream.host, upstream.port);
|
let bk = format!("{}:{}", upstream.host, upstream.port);
|
||||||
warn!(
|
debug!(
|
||||||
backend = %bk,
|
backend = %bk,
|
||||||
domain = %domain,
|
domain = %domain,
|
||||||
error = %e,
|
error = %e,
|
||||||
error_debug = ?e,
|
error_debug = ?e,
|
||||||
"Auto-detect: H2 request failed, falling back to H1"
|
"H2 stream error, retrying this request as H1"
|
||||||
);
|
);
|
||||||
self.metrics.backend_h2_failure(&bk);
|
self.metrics.backend_h2_failure(&bk);
|
||||||
let cache_key = crate::protocol_cache::ProtocolCacheKey {
|
|
||||||
host: upstream.host.clone(),
|
|
||||||
port: upstream.port,
|
|
||||||
requested_host: requested_host.clone(),
|
|
||||||
};
|
|
||||||
self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1);
|
|
||||||
|
|
||||||
// Retry as H1 for bodyless requests; return 502 for requests with bodies
|
// Retry as H1 for bodyless requests; return 502 for requests with bodies
|
||||||
if let Some((method, headers)) = retry_state {
|
if let Some((method, headers)) = retry_state {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '25.11.18',
|
version: '25.11.20',
|
||||||
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.'
|
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