XTLS/Xray-core · critical
brutal
Error message
brutal
What it means
The splithttp dialer panicked with the raw congestion-controller name as the panic value while setting up a QUIC (HTTP/3) connection. The switch only accepts "reno", ""/"bbr", and "force-brutal"; any other string in the transport config's congestion field hits default: panic(quicParams.Congestion). The panic message 'brutal' appears when the configured value is literally the string 'brutal' — a near-miss of the accepted 'force-brutal'.
Source
Thrown at transport/internet/splithttp/dialer.go:271
return nil, errors.New("mask err").Base(err)
}
pktConn = newConn
}
conn, err := quic.DialEarly(ctx, pktConn, udpAddr, tlsCfg, cfg)
if err != nil {
return nil, err
}
context.AfterFunc(conn.Context(), func() { pktConn.Close() })
switch quicParams.Congestion {
case "reno":
case "", "bbr":
congestion.UseBBR(conn, bbr.Profile(quicParams.BbrProfile))
case "force-brutal":
congestion.UseBrutal(conn, quicParams.BrutalUp)
default:
panic(quicParams.Congestion)
}
return conn, nil
},
}
} else if httpVersion == "2" {
if keepAlivePeriod == 0 {
keepAlivePeriod = net.ChromeH2KeepAlivePeriod
}
if keepAlivePeriod < 0 {
keepAlivePeriod = 0
}
transport = &http2.Transport{
DialTLSContext: func(ctxInner context.Context, network string, addr string, cfg *gotls.Config) (net.Conn, error) {
return dialContext(ctxInner)
},
IdleConnTimeout: net.ConnIdleTimeout,
ReadIdleTimeout: keepAlivePeriod,View on GitHub (pinned to 7d214f8b09)
Solutions
- Change the congestion setting to an accepted value: "force-brutal" (with brutalUp bandwidth set), "bbr" or "" (default), or "reno".
- If you wanted the brutal (TCP-like fixed-rate) controller, use exactly "force-brutal" and configure its BrutalUp mbps value.
- Validate config at load time instead of panicking at dial time (see validationCode).
Example fix
// before (config.json) "congestion": "brutal" // after "congestion": "force-brutal"
Defensive patterns
Strategy: validation
Validate before calling
validCongestion := map[string]bool{"": true, "reno": true, "bbr": true, "force-brutal": true}
if !validCongestion[quicParams.Congestion] {
return fmt.Errorf("invalid congestion %q: use \"reno\", \"bbr\" or \"force-brutal\"", quicParams.Congestion)
} Try / catch
defer func() {
if r := recover(); r != nil {
return nil, fmt.Errorf("splithttp dial failed (congestion=%q): %v", quicParams.Congestion, r)
}
}() Prevention
- Validate congestion strings at config load, not at dial time.
- Remember the brutal spelling is "force-brutal", never "brutal".
- Lint shared config snippets for accepted enum values before deploying.
When it happens
Trigger: Configuring splithttp QUIC congestion to any value outside {"reno", "", "bbr", "force-brutal"} — most commonly typing "brutal" instead of "force-brutal", or "cubic"/"newreno" from other tools' vocabularies. Panics at dial time, so the outbound connection crashes the process or goroutine depending on recover() coverage.
Common situations: Copy-pasting configs from other proxies whose brutal mode is spelled 'brutal'; upgrading configs written for a build that accepted more names; typo in JSON/YAML config.
Related errors
- brutal
- force-brutal requires up
- unknown congestion control: + c.FinalMask.QuicParams.Conges
- `scMaxEachPostBytes` should be bigger than 0
- unknown bbr profile
AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15).
Data as JSON: /api/errors/666d946cce8e6b70.
Report an issue: GitHub.