smarthttp: optionally follow info/refs redirects into Endpoint · Entire
smarthttp: optionally follow info/refs redirects into Endpoint
39e0654→main·
nodo·2mo ago·2 files·+84 added/-0 removed
Adds Conn.FollowInfoRefsRedirect (off by default). When set, RequestInfoRefs rewrites Endpoint.Scheme + Endpoint.Host to the final URL after HTTP redirects, so subsequent PostRPC* calls target the redirected node instead of the originally-configured entry point.
Matches vanilla git's smart-HTTP behaviour for discovery-aware servers that 307 /info/refs to a hosting replica (the entiredb info/refs discovery design). Endpoint.Path is never modified — it still holds the repo path.
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
Changes
2
internal/gitproto
Msmarthttp.go+17
- Msmarthttp_test.go+67
44 unmodified lines
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
58 unmodified lines
119
120
121
122
123
124
125
126
127
128
129
130
131
44 unmodified lines
Transport transport.Transport
HTTP *http.Client
Auth transport.AuthMethod
// FollowInfoRefsRedirect, when true, rewrites Endpoint.Scheme and
// Endpoint.Host to the final URL returned by RequestInfoRefs after
// HTTP redirects. Subsequent PostRPC* calls then target the
// redirected host directly, matching vanilla git's smart-HTTP
// behaviour for discovery-aware servers that 307 /info/refs to a
// hosting replica. Endpoint.Path is never modified — it still
// contains the repo path. Off by default to preserve behaviour for
// callers that rely on Endpoint being stable.
FollowInfoRefsRedirect bool
}
// NewConn creates a new connection to the given endpoint.
58 unmodified lines
if err := httpError(res); err != nil {
return nil, err
}
if conn.FollowInfoRefsRedirect && res.Request != nil && res.Request.URL != nil {
final := res.Request.URL
if final.Host != conn.Endpoint.Host || final.Scheme != conn.Endpoint.Scheme {
conn.Endpoint.Scheme = final.Scheme
conn.Endpoint.Host = final.Host
}
}
// Bound the read to prevent unbounded memory allocation (issue #9).
const maxInfoRefsSize = 64 * 1024 * 1024 // 64 MiB
lr := io.LimitReader(res.Body, maxInfoRefsSize+1)
Minternal/gitproto/smarthttp.go+17
4 unmodified lines
5
6
7
8
9
10
11
12
159 unmodified lines
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
4 unmodified lines
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
159 unmodified lines
}
// TestRequestInfoRefs_FollowInfoRefsRedirect verifies that when the flag is
// set, a 307 on /info/refs rewrites Conn.Endpoint.Host so subsequent PostRPC
// calls target the redirected node. Matches vanilla git's smart-HTTP
// behaviour and lets clients use a cluster entry domain for info/refs while
// packs land on the hosting replica.
func TestRequestInfoRefs_FollowInfoRefsRedirect(t *testing.T) {
node := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
_, _ = w.Write([]byte("001e# service=git-upload-pack\n0000"))
}))
defer node.Close()
entry := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, node.URL+r.URL.Path+"?"+r.URL.RawQuery, http.StatusTemporaryRedirect)
}))
defer entry.Close()
ep, err := transport.NewEndpoint(entry.URL + "/repo.git")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
conn := NewConn(ep, "test", nil, http.DefaultTransport)
conn.FollowInfoRefsRedirect = true
if _, err := RequestInfoRefs(t.Context(), conn, transport.UploadPackService, ""); err != nil {
t.Fatalf("RequestInfoRefs: %v", err)
}
nodeURL := strings.TrimPrefix(node.URL, "http://")
if conn.Endpoint.Host != nodeURL {
t.Errorf("Endpoint.Host = %q, want %q (endpoint should follow the 307)", conn.Endpoint.Host, nodeURL)
}
}
// TestRequestInfoRefs_DoesNotFollowByDefault confirms the default behaviour
// is unchanged: Endpoint is stable even if the server 307s.
func TestRequestInfoRefs_DoesNotFollowByDefault(t *testing.T) {
node := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
_, _ = w.Write([]byte("001e# service=git-upload-pack\n0000"))
}))
defer node.Close()
ep, err := transport.NewEndpoint(entry.URL + "/repo.git")
if err != nil {
t.Fatalf("parse endpoint: %v", err)
}
entryHost := ep.Host
conn := NewConn(ep, "test", nil, http.DefaultTransport)
// FollowInfoRefsRedirect intentionally not set.
if _, err := RequestInfoRefs(t.Context(), conn, transport.UploadPackService, ""); err != nil {
t.Fatalf("RequestInfoRefs: %v", err)
}
if conn.Endpoint.Host != entryHost {
t.Errorf("Endpoint.Host = %q, want %q (endpoint should be unchanged by default)", conn.Endpoint.Host, entryHost)
}
}
func TestHTTPErrorBoundsBodyRead(t *testing.T) {
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "https://example.com/repo.git", nil)
if err != nil {
}