Compare commits

..

6 Commits

Author SHA1 Message Date
8f6bb30367 v25.11.22
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-17 12:12:24 +00:00
ef9bac80ff fix(rustproxy-http): reuse healthy HTTP/2 upstream connections after requests with bodies 2026-03-17 12:12:24 +00:00
9c78701038 v25.11.21
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-17 11:33:34 +00:00
26fd9409a7 fix(rustproxy-http): reuse pooled HTTP/2 connections for requests with and without bodies 2026-03-17 11:33:34 +00:00
cfff128499 v25.11.20
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-17 01:32:35 +00:00
3baff354bd fix(rustproxy-http): avoid downgrading cached backend protocol on H2 stream errors 2026-03-17 01:32:35 +00:00
4 changed files with 34 additions and 36 deletions

View File

@@ -1,5 +1,24 @@
# Changelog
## 2026-03-17 - 25.11.22 - fix(rustproxy-http)
reuse healthy HTTP/2 upstream connections after requests with bodies
- Registers successful HTTP/2 connections in the pool regardless of whether the proxied request included a body
- Continues to avoid pooling upstream connections that returned 502 Bad Gateway responses
## 2026-03-17 - 25.11.21 - fix(rustproxy-http)
reuse pooled HTTP/2 connections for requests with and without bodies
- remove the bodyless-request restriction from HTTP/2 pool checkout
- always return successful HTTP/2 senders to the connection pool after requests
## 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

View File

@@ -1,6 +1,6 @@
{
"name": "@push.rocks/smartproxy",
"version": "25.11.19",
"version": "25.11.22",
"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.",
"main": "dist_ts/index.js",

View File

@@ -677,20 +677,10 @@ impl HttpProxyService {
h2: use_h2,
};
// H2 pool checkout — only for bodyless requests (GET/HEAD/DELETE).
//
// WORKAROUND: Requests with bodies (POST/PUT uploads) always get fresh H2
// connections. Reusing a pooled H2 connection after a large upload can stall
// forever due to depleted connection-level flow control windows. The h2 crate
// 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() {
// H2 pool checkout — reuse pooled connections for all requests.
// The h2 crate properly replenishes connection-level flow control
// windows via release_capacity() as data is consumed.
if use_h2 {
if let Some((mut sender, age)) = self.connection_pool.checkout_h2(&pool_key) {
match tokio::time::timeout(
std::time::Duration::from_millis(500),
@@ -1048,12 +1038,9 @@ impl HttpProxyService {
});
}
// 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 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 !request_had_body && matches!(&result, Ok(ref resp) if resp.status() != StatusCode::BAD_GATEWAY) {
if matches!(&result, Ok(ref resp) if resp.status() != StatusCode::BAD_GATEWAY) {
let g = self.connection_pool.register_h2(pool_key.clone(), sender_for_pool);
gen_holder.store(g, std::sync::atomic::Ordering::Relaxed);
}
@@ -1379,32 +1366,24 @@ impl HttpProxyService {
match sender.send_request(upstream_req).await {
Ok(upstream_response) => {
// Only pool after bodyless requests — uploads deplete connection-level
// flow control windows (see comment at pool checkout above).
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);
}
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
}
Err(e) => {
// H2 request failed — backend advertises h2 via ALPN but doesn't
// actually speak it. Update cache so future requests use H1.
// H2 request failed on a stream level (e.g. RST_STREAM PROTOCOL_ERROR).
// 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);
warn!(
debug!(
backend = %bk,
domain = %domain,
error = %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);
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
if let Some((method, headers)) = retry_state {

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartproxy',
version: '25.11.19',
version: '25.11.22',
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.'
}