MHSanaei/3x-ui · error

%s %s: HTTP %d: %q

Error message

%s %s: HTTP %d: %q

What it means

Returned by Remote.do when the sub-node answers the RPC with a non-200 status AND a non-empty body. The first %d is the HTTP status code and %q is a short (capped at errBodyDiagBytes), quoted snippet of the node's response body, escaped so untrusted node output cannot inject into logs. It means the request reached the node but the node's HTTP layer rejected it before the JSON envelope could be produced.

Source

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

	if err != nil {
		return nil, err
	}
	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)
	}

View on GitHub (pinned to ad32144c42)

Solutions

  1. Read the status code: 401/403 -> re-enter the node's API credential on the master; 404 -> upgrade the node to a build that has the endpoint; 5xx -> inspect the quoted node-side message in the node's own logs.
  2. Confirm master and node run the same release version (panel setting /node page or binary version).
  3. If a reverse proxy fronts the node, bypass it or add an exception for /panel/api/ so auth headers pass through unchanged.
  4. Re-test with curl using the same token the master uses to confirm the node accepts it.

Example fix

// before: node still on an old build
// err: GET panel/api/server/getWebCertFiles: HTTP 404: "404 page not found"

// after: upgrade node binary to the same release as master, then retry
// GET panel/api/server/getWebCertFiles -> 200 envelope
Defensive patterns

Strategy: try-catch

Validate before calling

// Check version compatibility before calling newer endpoints
if err := remote.CheckCompat(ctx); err != nil { // or compare reported versions
    return fmt.Errorf("node incompatible: %w", err)
}

Type guard

func httpStatusOf(err error) (int, bool) {
    // remote.go errors embed "HTTP <code>" for non-OK statuses
    m := regexp.MustCompile(`HTTP (\d{3})`).FindStringSubmatch(err.Error())
    if m == nil { return 0, false }
    code, _ := strconv.Atoi(m[1])
    return code, true
}

Try / catch

if err := remote.GetWebCertFiles(ctx); err != nil {
    if code, ok := httpStatusOf(err); ok && code == http.StatusNotFound {
        return nil // old node build: fall back to manual cert paths
    }
    return err
}

Prevention

When it happens

Trigger: Any Remote RPC where the node returns 401/403 (node API token mismatch or mTLS rejected), 404 (node build predates the endpoint, e.g. getWebCertFiles or descendants on an old node), 405 (method changed between versions), or 500 (panic in the node handler).

Common situations: Master and node run different 3x-ui versions so an endpoint exists only on one side; the shared bearer/secret token was regenerated on the node but not on the master; a reverse proxy in front of the node intercepts the path and returns 401/403; node handler bug returns 500 with a stack-trace body.

Related errors


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