# gitproto: disable HTTP keep-alives on the default transport

`b8223f7`→[main](/content/gh/entireio/git-sync/commits/main/index.html)· Soph·1mo ago·2 files·+47 added/-16 removed

Use a cloned http.Transport with DisableKeepAlives=true as the git-sync default, instead of returning the package-level http.DefaultTransport.

Two changes in one:

1. Always clone — previously, NewHTTPTransport(false) returned the shared http.DefaultTransport, so any TLS or pool settings we added would leak into other code in the same process. The library angle makes that a real footgun.
2. Keep-alives off — git-sync's HTTP workflow against a given host is coarse-grained (one info/refs GET, then one upload-pack or receive-pack POST) with real work in between (planning, source fetch, local object materialization). On the push side the gap is long enough for CDN edges and some hosted git providers to close their end of an idle TLS socket; the next POST then fails with "use of closed network connection" because the pooled connection is half-dead. Observed against Cloudflare Artifacts after a ~13s gap.

Pool reuse would save at most one TLS handshake per sync, negligible against multi-MB to multi-GB transfers, so a fresh connection per request is the right trade. Library callers needing pool reuse can pass their own RoundTripper to NewHTTPConn.

## Sessions

2b8c12630fc0View transcript

## Changes

2

- internal/gitproto  
  - Msmarthttp.go+24/-6
  - Msmarthttp_test.go+23/-10

```
200 unmodified lines

201
202
203
204
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
206
223
224
225
226
209
210
227
228
229
230
231
232
233
215
234
217
235
236
237
238

200 unmodified lines

ep.RawPath = strings.TrimRight(ep.RawPath, "/")

// NewHTTPTransport creates an http.Transport with optional TLS skip.
// NewHTTPTransport returns the default git-sync HTTP transport. It clones
// http.DefaultTransport so config changes (TLS, keep-alive policy) don't leak into other code in the same process.
//
// Keep-alives are disabled. The git smart-HTTP workflow over the same host
// is coarse-grained — info/refs, then a single upload-pack or receive-pack
// POST — with real work in between (planning, source fetch, local object
// materialization). On the push side that gap is long enough for CDN
// edges and some hosted git providers to close their end of an idle TLS
// socket; the next POST then fails with "use of closed network connection"
// because the pooled connection is half-dead. Pool reuse would save at
// most one TLS handshake per sync, which is negligible against multi-MB
// to multi-GB transfers, so we prefer a fresh connection per request and
// avoid the race entirely.
//
// Library callers that need pool reuse (e.g. embedding git-sync in a
// long-running process that hits the same host repeatedly with short
// gaps) can pass their own RoundTripper to NewHTTPConn instead.
func NewHTTPTransport(skipTLS bool) http.RoundTripper {
	if !skipTLS {
		base, ok := http.DefaultTransport.(*http.Transport)
		if !ok {
			return http.DefaultTransport
		}
		if cloned, ok := http.DefaultTransport.(*http.Transport); ok {
			tc := cloned.Clone()
			tc := base.Clone()
			tc.DisableKeepAlives = true
			if skipTLS {
				if tc.TLSClientConfig == nil {
					tc.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
				}
				tc.TLSClientConfig.InsecureSkipVerify = true
				return tc
			}
			return http.DefaultTransport
		}
	}

// RequestInfoRefs fetches /info/refs for the given service.
```

Minternal/gitproto/smarthttp.go+24/-6

```
82 unmodified lines

83
84
85
86
86
87
88
88
89
89
90
91
92
93
94
95
96
97
98
99
92
100
101
102
103
95
104
105
97
98
99
100
101
106
107
108
109
110
111
112
113
114
115
116
117

82 unmodified lines

}

func TestNewHTTPTransport(t *testing.T) {
	// Without TLS skip should return default transport.
	// Default (no TLS skip) returns a cloned transport, not the shared
	// http.DefaultTransport — config must not leak into other code.
	rt := NewHTTPTransport(false)
	if rt != http.DefaultTransport {
		t.Error("expected http.DefaultTransport when skipTLS is false")
	}
	if rt == http.DefaultTransport {
		t.Error("expected a cloned transport, got shared http.DefaultTransport")
	}
	ht, ok := rt.(*http.Transport)
	if !ok {
		t.Fatalf("expected *http.Transport, got %T", rt)
	}
	if !ht.DisableKeepAlives {
		t.Error("expected DisableKeepAlives = true on the default transport")
	}

// With TLS skip should return a transport with InsecureSkipVerify.
	// With TLS skip we still get a cloned transport with keep-alives off,
	// plus InsecureSkipVerify on the TLS config.
	rt = NewHTTPTransport(true)
	if rt == http.DefaultTransport {
		t.Error("expected a different transport when skipTLS is true")
		t.Error("expected a cloned transport when skipTLS is true")
	}
	// Verify the returned transport is an *http.Transport with skip verify.
	if ht, ok := rt.(*http.Transport); ok {
		if ht.TLSClientConfig == nil || !ht.TLSClientConfig.InsecureSkipVerify {
			t.Error("expected InsecureSkipVerify = true")
		}
		ht, ok = rt.(*http.Transport)
		if !ok {
			t.Fatalf("expected *http.Transport, got %T", rt)
		}
		if !ht.DisableKeepAlives {
			t.Error("expected DisableKeepAlives = true when skipTLS is true")
		}
		if ht.TLSClientConfig == nil || !ht.TLSClientConfig.InsecureSkipVerify {
			t.Error("expected InsecureSkipVerify = true when skipTLS is true")
		}
	}
}
```
