Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.
ccantynz-alt committed on July 19, 2026Parent: f5f21f2
2 files changed+283908648536e93e72db758b1d200e5476bea5c5ba1
2 changed files+28−3
Modifiedsrc/git/protocol.ts+14−1View fileUnifiedSplit
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
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