Commitf5f21f2
fix(git): stream git-upload-pack instead of buffering the whole pack
fix(git): stream git-upload-pack instead of buffering the whole pack serviceRpc() collected the entire subprocess stdout into memory via .arrayBuffer() before responding, and only started reading it after already writing+closing stdin. For a repo with any real history the generated pack can be many MB; the child starts writing it to a pipe with a small OS buffer well before anything drains it, and nothing does until the process exits. Past a certain pack size this stalls and the client sees "remote end hung up unexpectedly" mid clone/fetch. Reproduced directly against gluecron's own self-hosted repo: a shallow clone succeeded, a full clone/fetch consistently died mid-transfer. git-receive-pack (push) keeps the buffered path -- callers install a temp pre-receive hook and delete it the instant serviceRpc resolves, so push has to keep waiting for the subprocess to actually exit. git-upload-pack has no such coupling, so its response now streams straight from the subprocess stdout.
1 file changed+34−0f5f21f2a74841aa0546a1fd108f460f62732119c
1 changed file+34−0
Modifiedsrc/git/protocol.ts+34−0View fileUnifiedSplit
@@ -74,6 +74,40 @@ export async function serviceRpc(
7474 proc.stdin.write(inputBytes);
7575 proc.stdin.end();
7676
77 // git-receive-pack (push) callers install a temp pre-receive hook and
78 // delete it the instant this call resolves, so pushes must keep waiting
79 // for the subprocess to fully exit before we hand back a Response.
80 // git-upload-pack (clone/fetch) has no such coupling, and buffering its
81 // stdout via `.arrayBuffer()` was the bug: for a repo with any real
82 // history the pack can be many MB, the child starts writing it to a
83 // pipe with a small OS buffer well before we ever start reading, and
84 // nothing drains stdout until the process exits -- on large enough
85 // packs this deadlocks and the client sees the connection die mid-clone
86 // (reproduced directly against this repo: shallow clone fine, full
87 // clone/fetch "remote end hung up unexpectedly"). Stream it straight
88 // through instead so the OS pipe stays drained the whole time.
89 if (service === "git-upload-pack") {
90 proc.exited.then((code) => {
91 if (code !== 0) {
92 new Response(proc.stderr)
93 .text()
94 .then((stderr) =>
95 console.error(
96 `[git] ${service} exited ${code} for ${owner}/${repo}: ${stderr}`
97 )
98 )
99 .catch(() => {});
100 }
101 });
102
103 return new Response(proc.stdout, {
104 headers: {
105 "Content-Type": `application/x-${service}-result`,
106 "Cache-Control": "no-cache",
107 },
108 });
109 }
110
77111 const stdout = await new Response(proc.stdout).arrayBuffer();
78112 await proc.exited;
79113
80114