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
- Ensure every custom expvar's String() returns stable, valid JSON at all times (lock internal state while serializing).
- 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.
- 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
- Make every custom expvar's String() return stable, valid JSON under concurrent mutation (lock internals).
- Test the metrics endpoint in CI with all your expvars registered.
- Never leak raw marshal errors to scrapers; log internally and return a valid JSON error body.
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
- Metrics must have a tag or listen address.
- metrics must have a tag or listen address
- unknown format of a string list:
- invalid address:
- Invalid integer range, expected either string of form "1-2"
AI-assisted analysis of XTLS/Xray-core@7d214f8b09 (2026-08-15).
Data as JSON: /api/errors/7000610f0acd9fc0.
Report an issue: GitHub.