Skip to content

Commit 0fb84db

Browse files
Matias SalinasMatias Salinas
authored andcommitted
fix (52) Empty reply from server errors caused by unsupported encodings
1 parent 606e387 commit 0fb84db

File tree

3 files changed

+87
-14
lines changed

3 files changed

+87
-14
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ ARG BINARY
44

55
COPY --chmod=755 ${BINARY} /cachebolt
66

7-
ENTRYPOINT ["/cachebolt"]
7+
ENTRYPOINT ["/cachebolt"]

config.yaml

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,56 @@
1-
app_id: gitlab-proxy
1+
# 🔧 Unique identifier for this CacheBolt instance
2+
app_id: my-service
3+
4+
# 🌐 Port to bind the main proxy server (default: 3000)
25
proxy_port: 3000
6+
7+
# 🛠️ Port to bind the admin interface and /metrics (default: 3001)
38
admin_port: 3001
9+
10+
# 🚦 Maximum number of concurrent outbound requests to the downstream service
411
max_concurrent_requests: 200
512

6-
downstream_base_url: https://gitlab.falabella.tech
7-
storage_backend: local
13+
# 🌐 Base URL of the upstream API/backend to which requests are proxied
14+
downstream_base_url: http://localhost:4000
15+
16+
# 💾 Backend used for persistent cache storage
17+
# Available options: gcs, s3, azure, local
18+
storage_backend: s3
19+
20+
# 🪣 Name of the Google Cloud Storage bucket (used if storage_backend is 'gcs')
21+
gcs_bucket: cachebolt
822

9-
gcs_bucket: ""
10-
s3_bucket: ""
11-
azure_container: ""
23+
# 🪣 Name of the Amazon S3 bucket (used if storage_backend is 's3')
24+
s3_bucket: my-cachebolt-bucket
1225

26+
# 📦 Name of the Azure Blob Storage container (used if storage_backend is 'azure')
27+
azure_container: cachebolt-container
28+
29+
# 🧠 Memory cache configuration
1330
cache:
31+
# 🚨 System memory usage threshold (%) above which in-memory cache will start evicting entries
1432
memory_threshold: 90
33+
34+
# 🔁 Percentage of requests (per key) that should trigger a refresh from backend instead of using cache
35+
# Example: 10% means 1 in every 10 requests will bypass cache
1536
refresh_percentage: 1
37+
38+
# 🗑️ Cache lifetime before refresh the key
1639
ttl_seconds: 10
1740

41+
# ⚠️ Latency-based failover configuration
1842
latency_failover:
43+
# ⌛ Default maximum allowed latency in milliseconds for any request
1944
default_max_latency_ms: 1000
2045

46+
# 🛣️ Path-specific latency thresholds
47+
path_rules:
48+
- pattern: "^/api/v1/products/.*"
49+
max_latency_ms: 15000
50+
- pattern: "^/auth/.*"
51+
max_latency_ms: 10000
52+
53+
# 🚫 List of request headers to ignore when computing cache keys (case-insensitive)
2154
ignored_headers:
2255
- postman-token
23-
- if-none-match
56+
- if-none-match

src/proxy.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
8686
let uri = req.uri().to_string();
8787
tracing::debug!("🔗 Received request for URI: {}", uri);
8888

89+
tracing::debug!("🔎 Incoming request headers:");
90+
for (k, v) in req.headers().iter() {
91+
tracing::debug!(" {}: {:?}", k, v);
92+
}
93+
8994
// Increment total request counter for each URI
9095
counter!("cachebolt_proxy_requests_total", "uri" => uri.clone()).increment(1);
9196

@@ -173,9 +178,11 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
173178
}
174179

175180
// Split response into parts
176-
let (parts, body) = resp.into_parts();
181+
let (mut parts, body) = resp.into_parts();
177182
let body_bytes = hyper::body::to_bytes(body).await.unwrap_or_default();
178183

184+
parts.headers.remove("content-length");
185+
179186
let headers_vec = parts
180187
.headers
181188
.iter()
@@ -214,7 +221,10 @@ pub async fn proxy_handler(req: Request<Body>) -> impl IntoResponse {
214221
);
215222
}
216223
} else {
217-
tracing::info!("⏩ Cache bypass activated for '{}' due to client header", uri);
224+
tracing::info!(
225+
"⏩ Cache bypass activated for '{}' due to client header",
226+
uri
227+
);
218228
}
219229

220230
Response::from_parts(parts, Body::from(body_bytes))
@@ -308,11 +318,22 @@ pub fn hash_uri(uri: &str) -> String {
308318
}
309319

310320
/// Sends an outbound GET request to the downstream backend
321+
/// Sends an outbound GET request to the downstream backend, forwarding all headers except 'accept-encoding'.
322+
/// This prevents curl: (52) Empty reply from server errors caused by unsupported encodings.
323+
///
324+
/// # Arguments
325+
/// - `uri`: The path to append to the downstream base URL.
326+
/// - `original_req`: The incoming Axum request, from which headers are forwarded.
327+
///
328+
/// # Returns
329+
/// - `Ok(Response)` with the downstream response if successful.
330+
/// - `Err(())` if the downstream call fails or the request could not be built.
311331
pub async fn forward_request(uri: &str, original_req: Request<Body>) -> Result<Response<Body>, ()> {
332+
// Get the config and build the downstream full URL
312333
let cfg = CONFIG.get().unwrap();
313334
let full_url = format!("{}{}", cfg.downstream_base_url, uri);
314335

315-
// Log scheme/host/path para debug (opcional, pero muy útil)
336+
// Debug: Log the scheme, host, and path of the downstream URL
316337
if let Ok(parsed_url) = url::Url::parse(&full_url) {
317338
tracing::info!(
318339
"🌐 Downstream request: scheme='{}' host='{}' path='{}'",
@@ -322,14 +343,33 @@ pub async fn forward_request(uri: &str, original_req: Request<Body>) -> Result<R
322343
);
323344
}
324345

346+
// Parse downstream_base_url to extract the host (domain)
347+
let downstream_host = url::Url::parse(&cfg.downstream_base_url)
348+
.ok()
349+
.and_then(|u| u.host_str().map(|s| s.to_string()))
350+
.unwrap_or_else(|| "".to_string());
351+
352+
// Build the request, starting with the URL and GET method
325353
let mut builder = Request::builder().uri(full_url.clone()).method("GET");
326354

327-
// Copia todos los headers originales
355+
// Copy all headers from the incoming request,
356+
// except for 'accept-encoding' and 'host'
357+
// (We want to control the Host header for SNI/proxying, and avoid content-encoding issues.)
328358
for (key, value) in original_req.headers().iter() {
359+
if key.as_str().eq_ignore_ascii_case("accept-encoding")
360+
|| key.as_str().eq_ignore_ascii_case("host")
361+
{
362+
continue;
363+
}
329364
builder = builder.header(key, value);
330365
}
331366

332-
// Construye el request
367+
// Inject the Host header, if it was successfully extracted from the downstream_base_url
368+
if !downstream_host.is_empty() {
369+
builder = builder.header("Host", downstream_host);
370+
}
371+
372+
// Build the final request object with empty body
333373
let req = match builder.body(Body::empty()) {
334374
Ok(req) => req,
335375
Err(e) => {
@@ -338,7 +378,7 @@ pub async fn forward_request(uri: &str, original_req: Request<Body>) -> Result<R
338378
}
339379
};
340380

341-
// Ejecuta la request, maneja errores con logs detallados
381+
// Send the HTTP request to the downstream service
342382
match HTTP_CLIENT.request(req).await {
343383
Ok(resp) => Ok(resp),
344384
Err(e) => {

0 commit comments

Comments
 (0)