bootstrap: treat receive-pack timeouts (408/504) as batchable · Entire
bootstrap: treat receive-pack timeouts (408/504) as batchable
81390b1→main·
Soph·1mo ago·2 files·+101 added/-3 removed
GitHub kills a receive-pack POST with HTTP 408 when a slow or oversized
push outlasts its server-side deadline — the failure mode in #70, where
a large GitLab repo relayed over a slow link timed out before the single
one-shot pack finished. Only body-limit rejections (413) previously
triggered the batched-bootstrap fallback, so a 408 hard-failed with no
remediation despite --target-max-pack-bytes being the exact fix.
Classify 408 (and gateway 504) as a receive-pack deadline and route it
through the same batched retry as a size rejection: smaller packs each
finish inside the target's window. Applied both to the one-shot
auto-batch decision and the mid-batch subdivision path.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
bacf4c249e3cView transcript
[?
can you take a look at https://github.com/entireio/git-sync/issues/70Claude Code·Opus 4.8[1m]·1 step](/content/gh/entireio/git-sync/session/fddf9dc4-36f4-4c09-b698-3a0e976d3f4a#timeline-bacf4c249e3c/index.html)
Changes
2
internal/strategy/bootstrap
Mbootstrap.go+35/-3
Mbootstrap_test.go+66
499 unmodified lines
500
501
502
503
504
503
504
505
506
507
508
509
510
870 unmodified lines
1381
1382
1383
1381
1384
1385
1386
1387
53 unmodified lines
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
499 unmodified lines
_ = packReader.Close()
// Treat abortedEarly the same as a body-limit error:
// both indicate "this pack is too big for the target",
// just one is detected by the server and one by us.
sizeIssue := abortedEarly || isTargetBodyLimitError(pushErr)
// just one is detected by the server and one by us. A
// receive-pack deadline (408/504) lands here too — a
// checkpoint that times out is also too big for this
// target/link, and subdividing makes each push finish sooner.
sizeIssue := abortedEarly || isBatchableTargetPushError(pushErr)
p.log("bootstrap batch push failed",
"branch", batch.Plan.TargetRef.String(),
"batch", idx+1,
870 unmodified lines
}
func autoTargetMaxPackBytes(p Params, err error) (int64, bool) {
if p.TargetMaxPack > 0 || !isTargetBodyLimitError(err) {
if p.TargetMaxPack > 0 || !isBatchableTargetPushError(err) {
return 0, false
}
if p.SourceService == nil || !p.SourceService.SupportsBootstrapBatch() {
53 unmodified lines
strings.Contains(msg, "http 413")
}
// isTargetPushDeadlineError reports whether err indicates the target cut the
// receive-pack POST short because it ran past a server-side deadline rather
// than because the pack exceeded an announced size limit. GitHub returns 408
// (Request Timeout) when a slow or oversized push outlasts its receive-pack
// wall-clock window — common when relaying a large repo over a slow source
// link, where the upstream read rate throttles the downstream write. Gateways
// fronting other hosts surface the same condition as 504 (Gateway Timeout).
//
// Both are remedied the way a body-limit rejection is: smaller packs each
// finish inside the window, so callers route them into the same batched
// bootstrap retry. Kept distinct from isTargetBodyLimitError because the
// trigger is a timeout, not a size rejection, and there's no body limit to
// parse out of the message.
func isTargetPushDeadlineError(err error) bool {
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "http 408") || strings.Contains(msg, "http 504")
}
// isBatchableTargetPushError reports whether err is a target-side push failure
// that batched bootstrap can work around by sending smaller packs: an explicit
// body-size rejection (413 / "body exceeded size limit") or a receive-pack
// deadline (408 / 504).
func isBatchableTargetPushError(err error) bool {
return isTargetBodyLimitError(err) || isTargetPushDeadlineError(err)
}
func targetBodyLimit(err error) int64 {
if err == nil {
return 0