MHSanaei/3x-ui · error
invalid outbounds JSON: %w
Error message
invalid outbounds JSON: %w
What it means
TestOutbounds parses its outboundsJSON argument with json.Unmarshal into []json.RawMessage. Any string that is not a valid JSON array (or contains invalid JSON syntax) fails at this first gate. Note the outer layer only requires an array; individual non-object elements are silently left nil and handled later, so this error strictly means 'the top-level document is not parseable JSON array'.
Source
Thrown at internal/web/service/outbound/probe_http.go:126
// through a temp xray instance (UDP-transport outbounds are always forced to
// the HTTP probe — a raw dial can't measure them).
func (s *OutboundService) TestOutbound(outboundJSON string, testURL string, allOutboundsJSON string, mode string) (*TestOutboundResult, error) {
var ob map[string]any
if err := json.Unmarshal([]byte(outboundJSON), &ob); err != nil {
return &TestOutboundResult{Mode: probeModeLabel(mode), Success: false, Error: fmt.Sprintf("Invalid outbound JSON: %v", err)}, nil
}
results := s.testOutboundsParsed([]map[string]any{ob}, testURL, allOutboundsJSON, mode)
return results[0], nil
}
// TestOutbounds probes a JSON array of outbounds and returns one result per
// input, in input order, each carrying the outbound's tag. allOutboundsJSON
// supplies the config context (sockopt.dialerProxy chains); testURL falls
// back to the default probe URL when empty.
func (s *OutboundService) TestOutbounds(outboundsJSON string, testURL string, allOutboundsJSON string, mode string) ([]*TestOutboundResult, error) {
var raw []json.RawMessage
if err := json.Unmarshal([]byte(outboundsJSON), &raw); err != nil {
return nil, fmt.Errorf("invalid outbounds JSON: %w", err)
}
if len(raw) > maxBatchItems {
return nil, fmt.Errorf("too many outbounds in one request (max %d)", maxBatchItems)
}
items := make([]map[string]any, len(raw))
for i, r := range raw {
var ob map[string]any
if err := json.Unmarshal(r, &ob); err == nil {
items[i] = ob
}
}
return s.testOutboundsParsed(items, testURL, allOutboundsJSON, mode), nil
}
// testOutboundsParsed splits items into the TCP lane (direct dials, bounded
// worker pool) and the HTTP lane (one shared temp xray instance), runs both,
// and returns results aligned with items. A nil item marks unparseable input.
func (s *OutboundService) testOutboundsParsed(items []map[string]any, testURL string, allOutboundsJSON string, mode string) []*TestOutboundResult {View on GitHub (pinned to ad32144c42)
Solutions
- Validate the payload is a JSON array before sending: json.Unmarshal into []json.RawMessage in a precheck
- If testing a single outbound, use the single-outbound API/testOutbound path that wraps it in an array
- Run the JSON through a linter/editor to catch truncation or trailing commas
Example fix
// before
svc.TestOutbounds(`{"tag":"a","protocol":"freedom"}`, ...) // object, not array
// after
svc.TestOutbounds(`[{"tag":"a","protocol":"freedom"}]`, ...) Defensive patterns
Strategy: validation
Validate before calling
if !json.Valid([]byte(outboundsJSON)) {
return errors.New("outbounds payload is not valid JSON")
}
var probe []json.RawMessage
if err := json.Unmarshal([]byte(outboundsJSON), &probe); err != nil {
return fmt.Errorf("payload must be a JSON array: %w", err)
}
results, err := svc.TestOutbounds(outboundsJSON, testURL, ctxJSON, mode) Type guard
func isJSONArrayOfOutbounds(s string) bool {
var raw []json.RawMessage
return json.Unmarshal([]byte(s), &raw) == nil
} Try / catch
results, err := svc.TestOutbounds(payload, ...)
if err != nil {
if strings.HasPrefix(err.Error(), "invalid outbounds JSON") {
// re-open editor with payload, highlight JSON error from %w chain
}
} Prevention
- Always JSON-encode outbound objects with a marshaller instead of string concatenation
- Validate with json.Valid in the frontend before enabling the test button
When it happens
Trigger: Calling TestOutbounds with a hand-typed string, a JSON object '{...}' instead of an array '[...]', YAML/base64 subscription content, or truncated output from an editor that dropped a bracket.
Common situations: Frontend sending a partially-edited outbound from the JSON editor tab; pasting a single outbound object instead of an array; smart-quote or trailing-comma corruption from copy/paste.
Related errors
- The new username and password are empty
- tg_id must be a positive integer
- token not found
- somethingWentWrong
- invalid metric
AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15).
Data as JSON: /api/errors/5f5eff3642c26a2e.
Report an issue: GitHub.