cloudflare-native port of the tangled knot server
22

Configure Feed

Select the types of activity you want to include in your feed.

knotserver: close receive-pack stdin for no-pack updates

For HTTPS receive-pack requests where every requested ref update is a delete or points at an object already present locally, feed only the parsed command prefix to git and close stdin. This avoids smart-HTTP clients hanging while waiting for a response on updates that do not carry a packfile, and lets post-receive side effects run.

Patch category: upstreamable-core

Jer Miller (Jul 5, 2026, 11:05 AM -0600) 548fc494 0832c374

+78 -2
+21 -1
knotserver/git.go
··· 9 9 "io" 10 10 "net/http" 11 11 "os" 12 + "os/exec" 12 13 "path/filepath" 13 14 "strings" 14 15 ··· 398 399 writer: io.MultiWriter(w, pw), 399 400 } 400 401 402 + stdin := io.MultiReader(bytes.NewReader(prefix), bodyReader) 403 + if !receivePackNeedsPackfile(repo.path, parsed) { 404 + stdin = bytes.NewReader(prefix) 405 + } 406 + 401 407 cmd := service.ServiceCommand{ 402 408 GitProtocol: r.Header.Get("Git-Protocol"), 403 409 Dir: repo.path, 404 410 Stdout: capturingStdout, 405 - Stdin: io.MultiReader(bytes.NewReader(prefix), bodyReader), 411 + Stdin: stdin, 406 412 GitConfig: []string{"core.hooksPath=/dev/null"}, 407 413 Sandbox: h.sandbox, 408 414 } ··· 571 577 } 572 578 return buf.Bytes(), parsed, nil 573 579 } 580 + 581 + func receivePackNeedsPackfile(repoPath string, parsed git.ReceivePackRequest) bool { 582 + for _, line := range parsed.Lines { 583 + if line.NewSha.IsZero() { 584 + continue 585 + } 586 + cmd := exec.Command("git", "cat-file", "-e", line.NewSha.String()+"^{object}") 587 + cmd.Dir = repoPath 588 + if err := cmd.Run(); err != nil { 589 + return true 590 + } 591 + } 592 + return false 593 + }
+57 -1
knotserver/git_receive_pack_test.go
··· 16 16 "strconv" 17 17 "strings" 18 18 "testing" 19 + "time" 19 20 20 21 "github.com/go-chi/chi/v5" 21 22 "tangled.org/core/dbdrv" ··· 690 691 installFakeGit(t, receivePackReportStatus("refs/heads/main"), 0) 691 692 692 693 const packSize = 16 * 1024 * 1024 693 - prefix := receivePackRequestBody("refs/heads/main") 694 + prefix := receivePackRequestBodyFrom(strings.Repeat("1", 40), strings.Repeat("2", 40), "refs/heads/main") 694 695 body := &streamingPackReader{prefix: prefix, packBytes: packSize} 695 696 696 697 req := httptest.NewRequest(http.MethodPost, "/"+testOwnerDid+"/"+testRepoName+"/git-receive-pack", body) ··· 808 809 } 809 810 } 810 811 812 + func TestReceivePackKnownObjectUpdateDoesNotWaitForBodyEOF(t *testing.T) { 813 + // Why: smart-HTTP clients may keep the request body open while waiting for 814 + // the receive-pack response when the update needs no packfile. If the new 815 + // object already exists locally, the handler must feed only the parsed 816 + // command prefix to git and close stdin; otherwise receive-pack can update 817 + // refs but hang before post-receive side effects run. 818 + h, commits := newHTTPSPushFixtureWithCommits(t, true) 819 + installFakeGit(t, receivePackReportStatus("refs/heads/main"), 0) 820 + 821 + body := &blockingAfterReader{ 822 + data: receivePackRequestBodyFrom(commits[0], commits[1], "refs/heads/main"), 823 + } 824 + req := httptest.NewRequest(http.MethodPost, "/"+testOwnerDid+"/"+testRepoName+"/git-receive-pack", body) 825 + req.Header.Set("Content-Type", "application/x-git-receive-pack-request") 826 + req = requestWithRepoParams(req, testActorDid) 827 + 828 + rec := httptest.NewRecorder() 829 + done := make(chan struct{}) 830 + go func() { 831 + h.ReceivePack(rec, req) 832 + close(done) 833 + }() 834 + 835 + select { 836 + case <-done: 837 + case <-time.After(2 * time.Second): 838 + t.Fatal("ReceivePack blocked waiting for request body EOF after a known-object update") 839 + } 840 + 841 + if rec.Code != http.StatusOK { 842 + t.Fatalf("status = %d, want %d; body=%q", rec.Code, http.StatusOK, rec.Body.String()) 843 + } 844 + events, err := h.db.GetEvents(0, 1000) 845 + if err != nil { 846 + t.Fatalf("GetEvents: %v", err) 847 + } 848 + if len(events) != 1 { 849 + t.Fatalf("len(events) = %d, want 1 (known-object no-pack update must fire post-receive)", len(events)) 850 + } 851 + } 852 + 811 853 func TestReceivePackContextPostReceiveErrorsDoNotFailPush(t *testing.T) { 812 854 h := newHTTPSPushFixture(t, true) 813 855 installFakeGit(t, receivePackReportStatus("refs/heads/main"), 0) ··· 822 864 t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) 823 865 } 824 866 } 867 + 868 + type blockingAfterReader struct { 869 + data []byte 870 + off int 871 + } 872 + 873 + func (r *blockingAfterReader) Read(p []byte) (int, error) { 874 + if r.off < len(r.data) { 875 + n := copy(p, r.data[r.off:]) 876 + r.off += n 877 + return n, nil 878 + } 879 + select {} 880 + }