XTLS/Xray-core · warning

err.Error()

Error message

err.Error()

What it means

The metrics HTTP handler builds a JSON object from all registered expvar variables (plus stats and observatory blocks) and returns 500 with err.Error() if the final json.Marshal fails. Each expvar value is pre-validated with json.Valid and replaced by null when malformed, so a marshal failure here is nearly impossible in practice — it would require an invalid RawMessage to slip past validation or a map key collision producing an unencodable value. The error message surfaced to the client is the raw encoding/json error.

Source

Thrown at app/metrics/metrics.go:161

	return mux
}

func (p *MetricsHandler) handleDebugVars(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	vars := map[string]json.RawMessage{}
	expvar.Do(func(kv expvar.KeyValue) {
		value := json.RawMessage(kv.Value.String())
		if !json.Valid(value) {
			value = json.RawMessage("null")
		}
		vars[kv.Key] = value
	})
	vars["stats"] = marshalJSON(p.stats())
	vars["observatory"] = marshalJSON(p.observatoryStatus())

	payload, err := json.Marshal(vars)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Write(payload)
}

func marshalJSON(value interface{}) json.RawMessage {
	data, err := json.Marshal(value)
	if err != nil {
		return json.RawMessage("null")
	}
	return data
}

func (p *MetricsHandler) stats() map[string]map[string]map[string]int64 {
	resp := map[string]map[string]map[string]int64{
		"inbound":  {},
		"outbound": {},
		"user":     {},

View on GitHub (pinned to 7d214f8b09)

Solutions

  1. Ensure every custom expvar's String() returns stable, valid JSON at all times (lock internal state while serializing).
  2. If it fires intermittently under load, suspect a racy String(): guard the var's internals with a mutex so validation and marshal see the same bytes.
  3. Optionally marshal-then-fallback to null for the whole payload instead of a 500, so monitoring scrapes don't fail.

Example fix

// before
payload, err := json.Marshal(vars)
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}
w.Write(payload)

// after: never fail the scrape, emit a minimal valid body instead
payload, err := json.Marshal(vars)
if err != nil {
    log.Warnf("metrics marshal failed: %v", err)
    w.WriteHeader(http.StatusInternalServerError)
    w.Write([]byte(`{"error":"marshal failed"}`))
    return
}
w.Write(payload)
Defensive patterns

Strategy: fallback

Validate before calling

expvar.Do(func(kv expvar.KeyValue) {
    s := kv.Value.String()
    if !json.Valid([]byte(s)) {
        s = "null" // sanitize before it reaches the handler
    }
    vars[kv.Key] = json.RawMessage(s)
})

Try / catch

payload, err := json.Marshal(vars)
if err != nil {
    log.Warnf("metrics marshal failed: %v", err)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusInternalServerError)
    _, _ = w.Write([]byte(`{"error":"metrics marshal failed"}`))
    return
}

Prevention

When it happens

Trigger: GET on the metrics endpoint while some concurrently-updating expvar produces a .String() that races the json.Valid check and the marshal (mutation between validation and encoding), or a custom expvar publishing invalid JSON characters after validation.

Common situations: Custom expvars registered by plugins whose String() is not stable JSON; high-churn counters observed concurrently; realistically most users never see this path.

Related errors


AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15). Data as JSON: /api/errors/7000610f0acd9fc0. Report an issue: GitHub.