Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package org.tron.core.services.jsonrpc;

import com.fasterxml.jackson.databind.JsonNode;
import com.googlecode.jsonrpc4j.ErrorData;
import com.googlecode.jsonrpc4j.ErrorResolver;
import com.googlecode.jsonrpc4j.JsonRpcError;
import com.googlecode.jsonrpc4j.JsonRpcErrors;
import com.googlecode.jsonrpc4j.ProxyUtil;
import com.googlecode.jsonrpc4j.ReflectionUtil;
import java.lang.reflect.Method;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.tron.core.exception.TronError;
import org.tron.core.exception.jsonrpc.JsonRpcException;

/**
* {@link ErrorResolver} that uses annotations.
*/
@Slf4j(topic = "API")
public enum JsonRpcErrorResolver implements ErrorResolver {
INSTANCE;

Expand All @@ -22,29 +26,108 @@ public enum JsonRpcErrorResolver implements ErrorResolver {
@Override
public JsonError resolveError(
Throwable thrownException, Method method, List<JsonNode> arguments) {
JsonRpcError resolver = getResolverForException(thrownException, method);
if (notFoundResolver(resolver)) {
return null;
Error fatal = findFatalCause(thrownException);
if (fatal != null) {
throw fatal;
}

JsonRpcError resolver = method == null
? null : getResolverForException(thrownException, method);
if (resolver == null) {
logUnhandledException(method, thrownException);
return new JsonError(JsonError.INTERNAL_ERROR.code, "Internal error", null);
}

String message = hasErrorMessage(resolver) ? resolver.message() : thrownException.getMessage();
if (StringUtils.isBlank(message)) {
message = defaultMessageFor(resolver.code());
}

// data priority: exception > annotation > default ErrorData
// data priority: exception > annotation
Object data = null;
if (thrownException instanceof JsonRpcException) {
JsonRpcException jsonRpcException = (JsonRpcException) thrownException;
data = jsonRpcException.getData();
}

if (data == null) {
data = hasErrorData(resolver)
? resolver.data()
: new ErrorData(resolver.exception().getName(), message);
if (data == null && hasErrorData(resolver)) {
data = resolver.data();
}

return new JsonError(resolver.code(), message, data);
}

/**
* Returns the first {@link VirtualMachineError}, {@link ThreadDeath}, {@link LinkageError} or
* {@link TronError} found in {@code throwable} or its cause chain, or {@code null} if there is
* none.
*
* <p>This is the JSON-RPC layer's propagation policy for fatal causes, not a statement that
* other {@link Error}s are safe to recover from. Callers should apply it before any logging so a
* fatal cause is never recorded as an ordinary failure. The scan has no depth cutoff, terminates
* on cyclic cause chains, and uses neither recursion nor auxiliary collections.
*/
public static Error findFatalCause(Throwable throwable) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] findFatalCause and ExitManager.findTronError both walk a cause chain but guard against cycles differently (two-pointer here, a HashSet there). Not a problem by itself. If you want one convention, a shared walker in common next to ExitManager could serve both, or, if a depth cap were acceptable, a plain bounded loop would read more simply than the two-check-per-step form.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I'd keep the current scan in this PR for two reasons:

  • No depth cap: a cap could miss a fatal cause behind many wrappers and let it enter normal error-response handling. Propagation should not depend on wrapper depth. Cycle detection ensures termination; checking both fast-pointer steps ensures that no cause is skipped.
  • No auxiliary collection: the scan runs for every exception passed to resolveError, including mapped exceptions, and may encounter an OutOfMemoryError. The two-pointer approach avoids allocating a visited set.

A shared allocation-free walker in common makes sense as a separate refactor. It should preserve each caller's matching rule: only TronError for ExitManager, and the four fatal categories for JSON-RPC. findTronError also serves ExecutorServiceManager, Manager, DposTask, and the default uncaught exception handler, so the refactor needs validation across those paths. I'll include it as an optional refactoring item in the process-level fatal-handling follow-up.

// Avoid allocations when handling memory exhaustion, without truncating deep cause chains.
// Check both fast-pointer steps so cycle detection cannot skip a fatal cause inside a cycle.
Throwable slow = throwable;
Throwable fast = throwable;
while (fast != null) {
if (isFatal(fast)) {
return (Error) fast;
}
fast = fast.getCause();
if (fast == null) {
return null;
}
if (isFatal(fast)) {
return (Error) fast;
}
fast = fast.getCause();
slow = slow.getCause();
if (fast == slow) {
return null;
}
}
return null;
}

private static boolean isFatal(Throwable cause) {
return cause instanceof VirtualMachineError
|| cause instanceof ThreadDeath
|| cause instanceof LinkageError
|| cause instanceof TronError;
}

private static void logUnhandledException(Method method, Throwable thrownException) {
logger.debug("Unhandled exception in JSON-RPC method {}", rpcMethodName(method),
thrownException);
}

private static String rpcMethodName(Method method) {
if (method == null) {
return "unknown";
}
try {
return ProxyUtil.getMethodName(method);
} catch (RuntimeException e) {
return method.getName();
}
}

private static String defaultMessageFor(int code) {
switch (code) {
case -32600:
return "Invalid Request";
case -32601:
return "Method not found";
case -32602:
return "Invalid params";
default:
return "Internal error";
}
}

private JsonRpcError getResolverForException(Throwable thrownException, Method method) {
JsonRpcErrors errors = ReflectionUtil.getAnnotation(method, JsonRpcErrors.class);
if (hasAnnotations(errors)) {
Expand All @@ -57,10 +140,6 @@ private JsonRpcError getResolverForException(Throwable thrownException, Method m
return null;
}

private boolean notFoundResolver(JsonRpcError resolver) {
return resolver == null;
}

private boolean hasErrorMessage(JsonRpcError em) {
// noinspection ConstantConditions
return em.message() != null && !em.message().trim().isEmpty();
Expand All @@ -78,4 +157,4 @@ private boolean hasAnnotations(JsonRpcErrors errors) {
private boolean isExceptionInstanceOfError(Throwable target, JsonRpcError em) {
return em.exception().isInstance(target);
}
}
}
Loading
Loading