httpdl/cli: drop --auto-split; classify errors by type, not message text

Two Pike cleanups.

--auto-split was a got-only third connection-count policy (beside -x and -s)
that invented rather than matched aria2. Removing it also retires the now-dead
autoConns/maxAutoConns and the per-host transport cap that existed only to scale
for it; min(split, M*-x) is the sole policy again.

main's exit-code mapping fell back to substring-matching third-party error text
(strings.Contains "connection refused"/"timeout"/...), which rots when a
dependency rewords a message. The typed checks (errors.Is on the syscall errno,
*net.DNSError, net.Error.Timeout, context.DeadlineExceeded) already cover the
real stdlib errors -- verified end to end: refused -> 6, bad host -> 19. The two
cases that genuinely needed the text match are our own errors, now typed
sentinels: httpdl.ErrTimeout (idle timeout) and the bt metadata timeout wrapping
context.DeadlineExceeded.
This commit is contained in:
2026-06-20 23:55:39 +09:00
parent 270812de3e
commit 81c44d9ec7
8 changed files with 27 additions and 108 deletions

34
main.go
View File

@@ -530,7 +530,6 @@ func httpConfig(opts *cli.Options, single bool, overallDL *rate.Limiter) httpdl.
Split: opts.Int("split"),
MaxConnPerServer: opts.Int("max-connection-per-server"),
MinSplit: opts.Size("min-split-size"),
AutoSplit: opts.Bool("auto-split"),
Tries: opts.Int("max-tries"),
Timeout: time.Duration(opts.Int("timeout")) * time.Second,
FileAlloc: opts.Str("file-allocation"),
@@ -717,42 +716,29 @@ func exitCode(err error) int {
// isNameResolution reports whether err is (or wraps) a DNS lookup failure —
// "name resolution failed" (19), which usually points at the user's own
// network or resolver rather than a dead remote.
// network or resolver rather than a dead remote. The stdlib reports every such
// failure (no-such-host, server-misbehaving) as *net.DNSError.
func isNameResolution(err error) bool {
var de *net.DNSError
if errors.As(err, &de) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "no such host") || strings.Contains(msg, "server misbehaving")
return errors.As(err, &de)
}
// isNetwork reports whether err is a connection-level failure — refused, reset,
// or an unreachable host/network — a "network problem" (6).
// or an unreachable host/network — a "network problem" (6). A dial error from
// net/http wraps the syscall errno, so the typed check sees through it.
func isNetwork(err error) bool {
if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.ECONNRESET) ||
errors.Is(err, syscall.ENETUNREACH) || errors.Is(err, syscall.EHOSTUNREACH) {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "connection refused") ||
strings.Contains(msg, "connection reset") ||
strings.Contains(msg, "network is unreachable") ||
strings.Contains(msg, "no route to host")
return errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.ECONNRESET) ||
errors.Is(err, syscall.ENETUNREACH) || errors.Is(err, syscall.EHOSTUNREACH)
}
// isTimeout reports whether err is (or wraps) a timeout: a deadline exceeded, a
// net.Error that timed out, or an idle-read timeout reported as such.
// net.Error that timed out, or our own idle-read timeout (httpdl.ErrTimeout).
func isTimeout(err error) bool {
if errors.Is(err, context.DeadlineExceeded) {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, httpdl.ErrTimeout) {
return true
}
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
return true
}
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "timeout") || strings.Contains(msg, "timed out")
return errors.As(err, &ne) && ne.Timeout()
}
// saveSession writes the sources of every download that did not finish to path,