MHSanaei/3x-ui · error

%s %s: HTTP %d

Error message

%s %s: HTTP %d

What it means

Returned by Remote.do when the sub-node answers with a non-200 status and an EMPTY body, so there is nothing to quote. It carries only method, path and status code. Empty-body errors typically come from infrastructure between master and node rather than the node's own handlers, which almost always write a body.

Source

Thrown at internal/web/runtime/remote.go:266

	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("%s %s: %w", method, path, err)
	}
	defer resp.Body.Close()
	r.recordCaps(resp.Header)

	// Validate status before reading a success payload: a non-OK response's
	// body is never used beyond a short diagnostic, so don't let a node force us
	// to buffer a large body just to return an HTTP error.
	if resp.StatusCode != http.StatusOK {
		snippet, _ := io.ReadAll(io.LimitReader(resp.Body, errBodyDiagBytes))
		if msg := bytes.TrimSpace(snippet); len(msg) > 0 {
			// %q quotes/escapes the untrusted node body so control characters or
			// newlines in it can't garble or inject into the error/log output.
			return nil, fmt.Errorf("%s %s: HTTP %d: %q", method, path, resp.StatusCode, msg)
		}
		return nil, fmt.Errorf("%s %s: HTTP %d", method, path, resp.StatusCode)
	}

	// Fast-fail on an honestly-declared oversize body; the LimitReader below is
	// the real guard since Content-Length is untrusted, may be absent, or is -1
	// under transparent decompression.
	if resp.ContentLength > maxRemoteResponseBytes {
		return nil, fmt.Errorf("%s %s: %w (content-length %d, cap %d)", method, path, errRemoteResponseTooLarge, resp.ContentLength, maxRemoteResponseBytes)
	}

	raw, err := readCappedBody(resp.Body, maxRemoteResponseBytes)
	if err != nil {
		if errors.Is(err, errRemoteResponseTooLarge) {
			return nil, fmt.Errorf("%s %s: %w (cap %d bytes)", method, path, err, maxRemoteResponseBytes)
		}
		return nil, fmt.Errorf("read body: %w", err)
	}

	var env envelope

View on GitHub (pinned to ad32144c42)

Solutions

  1. Bypass the proxy: hit the node's panel port directly from the master to see whether the node itself is healthy.
  2. Check what sits in front of the node port — proxy access logs will show why it emitted the bare status.
  3. Restart or fix the node service if the proxy reports the upstream is down.
  4. If the proxy must stay, ensure it forwards /panel/api/* (and the request bodies/headers) untouched to the node.

Example fix

// before: master -> nginx -> node(down)
// err: POST panel/api/inbounds/update: HTTP 502

// after: start the node service (systemctl start x-ui) or point the master at the node's direct address
// POST panel/api/inbounds/update -> 200
Defensive patterns

Strategy: retry

Type guard

func isBareStatusError(err error) bool {
    _, hasSnippet := httpStatusOf(err) // see error 101
    return hasSnippet && !strings.Contains(err.Error(), "\"") // no quoted body
}

Try / catch

if err := r.do(ctx, method, path, body); err != nil {
    if code, ok := httpStatusOf(err); ok && code >= 502 && code <= 504 {
        return backoff.Retry(op, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 3))
    }
    return err
}

Prevention

When it happens

Trigger: A reverse proxy or load balancer in front of the node returns 502/503/504 with no body (node process dead behind the proxy); the kernel/health-checker resets with 404; a TLS-terminating proxy returns 400 for a malformed request with no explanation; the node crashed mid-response.

Common situations: nginx/caddy in front of the node while the x-ui service is stopped; proxy misrouting /panel/api/* to a static site; proxy rejecting the request body or Content-Encoding before it reaches the node; connection upgrade path broken after a proxy config change.

Related errors


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