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

  1. Validate the payload is a JSON array before sending: json.Unmarshal into []json.RawMessage in a precheck
  2. If testing a single outbound, use the single-outbound API/testOutbound path that wraps it in an array
  3. 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

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


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/5f5eff3642c26a2e. Report an issue: GitHub.