Commit9086485
fix(git): decompress gzip'd smart-HTTP request bodies before piping to git
fix(git): decompress gzip'd smart-HTTP request bodies before piping to git git gzips the negotiation/pack body once it crosses a size threshold -- small requests (a shallow clone, a tiny push) don't trigger it, large ones (a full clone/fetch against real history, a sizeable push) do. serviceRpc() and the receive-pack ref-parser were both piping/reading the raw, still-compressed bytes straight through: git-upload-pack choked with "fatal: protocol error: bad line length character" (gzip's magic bytes aren't valid pkt-line) and exited immediately, and a gzip'd push body would have parsed as zero refs -- silently skipping push-policy checks and the post-receive hook instead of erroring. Sniff the gzip magic bytes (0x1f 0x8b) rather than trust Content-Encoding, since it's the actual bytes that matter, and decompress before handing off in both routes. Surfaced by the previous commit (f5f21f2): streaming git-upload-pack's response turned the old silent hang into a fast, loud exit-128 with a real error in the logs -- this is the actual bug that was hanging full clones/fetches of any repo with enough history to cross git's gzip threshold, this repo included.
2 files changed+28−3908648536e93e72db758b1d200e5476bea5c5ba1
2 changed files+28−3
Modifiedsrc/git/protocol.ts+14−1View fileUnifiedSplit
@@ -58,12 +58,25 @@ export async function serviceRpc(
5858 extraEnv?: Record<string, string>
5959): Promise<Response> {
6060 const repoDir = getRepoPath(owner, repo);
61 const inputBytes = body
61 const rawBytes = body
6262 ? body instanceof ArrayBuffer
6363 ? new Uint8Array(body)
6464 : new Uint8Array(await new Response(body).arrayBuffer())
6565 : new Uint8Array();
6666
67 // git gzips the negotiation body once it gets large enough (a full
68 // clone/fetch against a repo with real history routinely crosses that
69 // threshold; small requests like a shallow clone don't). We were piping
70 // the raw, still-compressed bytes straight into `git <service>`'s stdin,
71 // which choked with "fatal: protocol error: bad line length character" —
72 // gzip's magic bytes don't parse as pkt-line. Sniff and decompress
73 // rather than trust Content-Encoding, since it's the actual bytes that
74 // matter and gzip's 2-byte magic number is unambiguous.
75 const inputBytes =
76 rawBytes.length >= 2 && rawBytes[0] === 0x1f && rawBytes[1] === 0x8b
77 ? Bun.gunzipSync(rawBytes)
78 : rawBytes;
79
6780 const proc = Bun.spawn([service, "--stateless-rpc", repoDir], {
6881 stdin: "pipe",
6982 stdout: "pipe",
Modifiedsrc/routes/git.ts+14−2View fileUnifiedSplit
@@ -118,8 +118,20 @@ git.post("/:owner/:repo.git/git-receive-pack", async (c) => {
118118 }
119119
120120 // Read the body once; we parse refs from it for both pre-receive policy
121 // checks and the existing post-receive hook.
122 const bodyBuffer = await c.req.arrayBuffer();
121 // checks and the existing post-receive hook. Large pushes arrive gzip'd
122 // (same threshold behavior as fetch/clone negotiation — see protocol.ts)
123 // — decompress before ref-parsing or a big push silently skips policy
124 // checks and the post-receive hook (refs parse as empty, not an error).
125 const rawBodyBuffer = await c.req.arrayBuffer();
126 const rawBodyBytes = new Uint8Array(rawBodyBuffer);
127 let bodyBuffer: ArrayBuffer = rawBodyBuffer;
128 if (rawBodyBytes.length >= 2 && rawBodyBytes[0] === 0x1f && rawBodyBytes[1] === 0x8b) {
129 const decompressed = Bun.gunzipSync(rawBodyBytes);
130 bodyBuffer = decompressed.buffer.slice(
131 decompressed.byteOffset,
132 decompressed.byteOffset + decompressed.byteLength
133 ) as ArrayBuffer;
134 }
123135 const refs = parseReceivePackRefs(new Uint8Array(bodyBuffer));
124136
125137 // Resolve the pusher early so we can pass it to both the policy gate and
126138