diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-21 15:55:08 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-21 15:55:08 +0000 |
| commit | 68cca2e28a5edc9b8da72f483980818e4a13fb52 (patch) | |
| tree | a8ab5126d094dd1879a0926102f168434cb637f2 | |
| parent | 7da6c0c601d276340fada02d4bd45080d927a16b (diff) | |
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.
| -rw-r--r-- | src/http/mod.rs | 42 |
1 files changed, 39 insertions, 3 deletions
diff --git a/src/http/mod.rs b/src/http/mod.rs index 9172e86..ffb1562 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs | |||
| @@ -159,14 +159,23 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 159 | .and_then(|v| v.to_str().ok()) | 159 | .and_then(|v| v.to_str().ok()) |
| 160 | .map(|s| s.to_string()); | 160 | .map(|s| s.to_string()); |
| 161 | 161 | ||
| 162 | // Extract Content-Encoding header to handle gzip-compressed request bodies | ||
| 163 | // Modern git clients send gzip-compressed POST bodies for efficiency | ||
| 164 | let content_encoding = req | ||
| 165 | .headers() | ||
| 166 | .get("content-encoding") | ||
| 167 | .and_then(|v| v.to_str().ok()) | ||
| 168 | .map(|s| s.to_lowercase()); | ||
| 169 | |||
| 162 | tracing::debug!( | 170 | tracing::debug!( |
| 163 | "Git request: {} {} (npub={}, id={}, subpath={}, protocol={:?})", | 171 | "Git request: {} {} (npub={}, id={}, subpath={}, protocol={:?}, encoding={:?})", |
| 164 | method, | 172 | method, |
| 165 | path, | 173 | path, |
| 166 | npub, | 174 | npub, |
| 167 | identifier, | 175 | identifier, |
| 168 | subpath, | 176 | subpath, |
| 169 | git_protocol | 177 | git_protocol, |
| 178 | content_encoding | ||
| 170 | ); | 179 | ); |
| 171 | 180 | ||
| 172 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); | 181 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); |
| @@ -175,12 +184,39 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 175 | 184 | ||
| 176 | return Box::pin(async move { | 185 | return Box::pin(async move { |
| 177 | // Collect request body once before the match statement | 186 | // Collect request body once before the match statement |
| 178 | let body_bytes = req | 187 | let raw_body = req |
| 179 | .collect() | 188 | .collect() |
| 180 | .await | 189 | .await |
| 181 | .map(|collected| collected.to_bytes()) | 190 | .map(|collected| collected.to_bytes()) |
| 182 | .unwrap_or_else(|_| Bytes::new()); | 191 | .unwrap_or_else(|_| Bytes::new()); |
| 183 | 192 | ||
| 193 | // Decompress gzip-encoded request bodies | ||
| 194 | // Git clients send Content-Encoding: gzip for POST requests | ||
| 195 | let body_bytes = if content_encoding.as_deref() == Some("gzip") { | ||
| 196 | use flate2::read::GzDecoder; | ||
| 197 | use std::io::Read; | ||
| 198 | |||
| 199 | let mut decoder = GzDecoder::new(&raw_body[..]); | ||
| 200 | let mut decompressed = Vec::new(); | ||
| 201 | match decoder.read_to_end(&mut decompressed) { | ||
| 202 | Ok(_) => { | ||
| 203 | tracing::debug!( | ||
| 204 | "Decompressed gzip body: {} -> {} bytes", | ||
| 205 | raw_body.len(), | ||
| 206 | decompressed.len() | ||
| 207 | ); | ||
| 208 | Bytes::from(decompressed) | ||
| 209 | } | ||
| 210 | Err(e) => { | ||
| 211 | tracing::warn!("Failed to decompress gzip body: {}", e); | ||
| 212 | // Fall back to raw body (might work if not actually gzip) | ||
| 213 | raw_body | ||
| 214 | } | ||
| 215 | } | ||
| 216 | } else { | ||
| 217 | raw_body | ||
| 218 | }; | ||
| 219 | |||
| 184 | let result = match (method.as_ref(), subpath.as_str()) { | 220 | let result = match (method.as_ref(), subpath.as_str()) { |
| 185 | // GET /info/refs?service=git-upload-pack or git-receive-pack | 221 | // GET /info/refs?service=git-upload-pack or git-receive-pack |
| 186 | (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { | 222 | (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { |