fix(http): decompress gzip-encoded git request bodies

Modern git clients send Content-Encoding: gzip on POST requests to
/git-upload-pack for efficiency. Without decompression, the compressed
binary data was passed directly to git upload-pack, which expected
pkt-line format, causing:

  fatal: protocol error: bad line length character: ??
  error: RPC failed; HTTP 500

This was discovered in production when git clone requests consistently
failed with HTTP 500 errors. The fix extracts the Content-Encoding
header and uses flate2::GzDecoder to decompress gzip bodies before
passing them to the git subprocess.
This commit is contained in:
DanConwayDev
2026-01-21 15:55:08 +00:00
parent 7da6c0c601
commit 68cca2e28a
+39 -3
View File
@@ -159,14 +159,23 @@ impl Service<Request<Incoming>> for HttpService {
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.map(|s| s.to_string()); .map(|s| s.to_string());
// Extract Content-Encoding header to handle gzip-compressed request bodies
// Modern git clients send gzip-compressed POST bodies for efficiency
let content_encoding = req
.headers()
.get("content-encoding")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_lowercase());
tracing::debug!( tracing::debug!(
"Git request: {} {} (npub={}, id={}, subpath={}, protocol={:?})", "Git request: {} {} (npub={}, id={}, subpath={}, protocol={:?}, encoding={:?})",
method, method,
path, path,
npub, npub,
identifier, identifier,
subpath, subpath,
git_protocol git_protocol,
content_encoding
); );
let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier);
@@ -175,12 +184,39 @@ impl Service<Request<Incoming>> for HttpService {
return Box::pin(async move { return Box::pin(async move {
// Collect request body once before the match statement // Collect request body once before the match statement
let body_bytes = req let raw_body = req
.collect() .collect()
.await .await
.map(|collected| collected.to_bytes()) .map(|collected| collected.to_bytes())
.unwrap_or_else(|_| Bytes::new()); .unwrap_or_else(|_| Bytes::new());
// Decompress gzip-encoded request bodies
// Git clients send Content-Encoding: gzip for POST requests
let body_bytes = if content_encoding.as_deref() == Some("gzip") {
use flate2::read::GzDecoder;
use std::io::Read;
let mut decoder = GzDecoder::new(&raw_body[..]);
let mut decompressed = Vec::new();
match decoder.read_to_end(&mut decompressed) {
Ok(_) => {
tracing::debug!(
"Decompressed gzip body: {} -> {} bytes",
raw_body.len(),
decompressed.len()
);
Bytes::from(decompressed)
}
Err(e) => {
tracing::warn!("Failed to decompress gzip body: {}", e);
// Fall back to raw body (might work if not actually gzip)
raw_body
}
}
} else {
raw_body
};
let result = match (method.as_ref(), subpath.as_str()) { let result = match (method.as_ref(), subpath.as_str()) {
// GET /info/refs?service=git-upload-pack or git-receive-pack // GET /info/refs?service=git-upload-pack or git-receive-pack
(m, sp) if m == Method::GET && sp.starts_with("info/refs") => { (m, sp) if m == Method::GET && sp.starts_with("info/refs") => {