Skip to content
Open
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
29 changes: 26 additions & 3 deletions nexus-sdk/src/main/java/io/nexusrpc/FailureInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,17 @@ public static Builder newBuilder(FailureInfo failure) {
}

private final String message;
private final String stackTrace;
private final Map<String, String> metadata;
private final @Nullable String detailsJson;

private FailureInfo(String message, Map<String, String> metadata, @Nullable String detailsJson) {
private FailureInfo(
String message,
String stackTrace,
Map<String, String> metadata,
@Nullable String detailsJson) {
this.message = message;
this.stackTrace = stackTrace;
this.metadata = metadata;
this.detailsJson = detailsJson;
}
Expand All @@ -34,6 +40,11 @@ public String getMessage() {
return message;
}

/** Failure stack trace. */
public String getStackTrace() {

Choose a reason for hiding this comment

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

P2 Badge Annotate FailureInfo stack trace as nullable

io.nexusrpc is @NullMarked (see package-info.java), so getStackTrace() currently advertises a non-null return type, but Builder#setStackTrace accepts null and build() also leaves it unset by default (covered by FailureInfoTest.builderWithoutStackTrace). This creates FailureInfo instances whose stackTrace is null despite a non-null API contract, so callers that trust the signature can dereference it and hit runtime NPEs.

Useful? React with 👍 / 👎.

return stackTrace;
}

/** Failure metadata. */
public Map<String, String> getMetadata() {
return metadata;
Expand All @@ -50,13 +61,14 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
FailureInfo that = (FailureInfo) o;
return Objects.equals(message, that.message)
&& Objects.equals(stackTrace, that.stackTrace)
&& Objects.equals(metadata, that.metadata)
&& Objects.equals(detailsJson, that.detailsJson);
}

@Override
public int hashCode() {
return Objects.hash(message, metadata, detailsJson);
return Objects.hash(message, stackTrace, metadata, detailsJson);
}

@Override
Expand All @@ -65,6 +77,9 @@ public String toString() {
+ "message='"
+ message
+ '\''
+ ", stackTrace='"
+ stackTrace
+ '\''
+ ", metadata="
+ metadata
+ ", details="
Expand All @@ -75,6 +90,7 @@ public String toString() {
/** Builder for an operation failure. */
public static class Builder {
private @Nullable String message;
private @Nullable String stackTrace;
private final Map<String, String> metadata;
private @Nullable String detailsJson;

Expand All @@ -84,6 +100,7 @@ private Builder() {

private Builder(FailureInfo failure) {
message = failure.message;
stackTrace = failure.stackTrace;
metadata = new HashMap<>(failure.metadata);
detailsJson = failure.detailsJson;
}
Expand Down Expand Up @@ -111,11 +128,17 @@ public Builder setDetailsJson(@Nullable String detailsJson) {
return this;
}

/** Set stack trace. */
public Builder setStackTrace(@Nullable String stackTrace) {
this.stackTrace = stackTrace;
return this;
}

/** Build the operation failure. */
public FailureInfo build() {
Objects.requireNonNull(message, "Message required");
return new FailureInfo(
message, Collections.unmodifiableMap(new HashMap<>(metadata)), detailsJson);
message, stackTrace, Collections.unmodifiableMap(new HashMap<>(metadata)), detailsJson);
}
}
}
64 changes: 60 additions & 4 deletions nexus-sdk/src/main/java/io/nexusrpc/OperationException.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,76 @@
package io.nexusrpc;

import org.jspecify.annotations.Nullable;

/** An operation has failed or was canceled. */
public class OperationException extends Exception {
private final OperationState state;

private OperationException(OperationState state, Throwable cause) {
super(cause);
private OperationException(OperationState state, String message, @Nullable Throwable cause) {
super(message, cause);
this.state = state;
}

/**
* Create a failed operation exception with a message.
*
* @param message The failure message.
* @return The operation exception.
*/
public static OperationException failure(String message) {
return new OperationException(OperationState.FAILED, message, null);
}

/**
* Create a failed operation exception with a cause.
*
* @param cause The cause of the failure.
* @return The operation exception.
*/
public static OperationException failure(Throwable cause) {
return new OperationException(OperationState.FAILED, cause);
return new OperationException(OperationState.FAILED, cause.toString(), cause);
}

/**
* Create a failed operation exception with a message and cause.
*
* @param message The failure message.
* @param cause The cause of the failure.
* @return The operation exception.
*/
public static OperationException failure(String message, Throwable cause) {
return new OperationException(OperationState.FAILED, message, cause);
}

/**
* Create a canceled operation exception with a message.
*
* @param message The cancellation message.
* @return The operation exception.
*/
public static OperationException canceled(String message) {
return new OperationException(OperationState.CANCELED, message, null);
}

/**
* Create a canceled operation exception with a cause.
*
* @param cause The cause of the cancellation.
* @return The operation exception.
*/
public static OperationException canceled(Throwable cause) {
return new OperationException(OperationState.CANCELED, cause);
return new OperationException(OperationState.CANCELED, cause.toString(), cause);
}

/**
* Create a canceled operation exception with a message and cause.
*
* @param message The cancellation message.
* @param cause The cause of the cancellation.
* @return The operation exception.
*/
public static OperationException canceled(String message, Throwable cause) {
return new OperationException(OperationState.CANCELED, message, cause);
}

public OperationState getState() {
Expand Down
145 changes: 138 additions & 7 deletions nexus-sdk/src/main/java/io/nexusrpc/handler/HandlerException.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.nexusrpc.handler;

import io.nexusrpc.FailureInfo;
import java.util.Arrays;
import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -33,36 +34,160 @@ public enum RetryBehavior {
private final String rawErrorType;
private final ErrorType errorType;
private final RetryBehavior retryBehavior;
private final FailureInfo originalFailure;

public HandlerException(ErrorType errorType, String message) {
this(errorType, new RuntimeException(message), RetryBehavior.UNSPECIFIED);
/**
* Create a handler exception with the given error type and cause message.
*
* @param errorType The error type.
* @param causeMessage The cause message.
* @deprecated
*/
public HandlerException(ErrorType errorType, String causeMessage) {
this(errorType, new RuntimeException(causeMessage), RetryBehavior.UNSPECIFIED);
}

/**
* Create a handler exception with the given error type and cause message.
*
* @param errorType The error type.
* @param causeMessage The cause message.
* @param retryBehavior The retry behavior for this exception.
* @deprecated
*/
public HandlerException(ErrorType errorType, String causeMessage, RetryBehavior retryBehavior) {
this(errorType, new RuntimeException(causeMessage), retryBehavior);
}

public HandlerException(ErrorType errorType, String message, RetryBehavior retryBehavior) {
this(errorType, new RuntimeException(message), retryBehavior);
/**
* Create a handler exception with the given error type, message, and cause.
*
* @param errorType The error type.
* @param message The error message.
* @param cause The cause of this exception.
*/
public HandlerException(ErrorType errorType, String message, @Nullable Throwable cause) {
this(errorType, message, cause, RetryBehavior.UNSPECIFIED);
}

/**
* Create a handler exception with the given error type and cause.
*
* @param errorType The error type.
* @param cause The cause of this exception.
*/
public HandlerException(ErrorType errorType, @Nullable Throwable cause) {
this(errorType, cause, RetryBehavior.UNSPECIFIED);
}

/**
* Create a handler exception with the given error type, cause, and retry behavior.
*
* @param errorType The error type.
* @param cause The cause of this exception.
* @param retryBehavior The retry behavior for this exception.
*/
public HandlerException(
ErrorType errorType, @Nullable Throwable cause, RetryBehavior retryBehavior) {
super(cause == null ? "handler error" : "handler error: " + cause.getMessage(), cause);
this(
errorType,
cause == null ? "handler error" : "handler error: " + cause.getMessage(),
cause,
retryBehavior);
}

/**
* Create a handler exception with the given error type, message, cause, and retry behavior.
*
* @param errorType The error type.
* @param message The error message.
* @param cause The cause of this exception.
* @param retryBehavior The retry behavior for this exception.
*/
public HandlerException(
ErrorType errorType, String message, @Nullable Throwable cause, RetryBehavior retryBehavior) {
this(errorType, message, cause, retryBehavior, null);
}

/**
* Create a handler exception with the given error type, message, cause, retry behavior, and
* original failure.
*
* @param errorType The error type.
* @param message The error message.
* @param cause The cause of this exception.
* @param retryBehavior The retry behavior for this exception.
* @param originalFailure The original failure information if available.
*/
public HandlerException(
ErrorType errorType,
String message,
@Nullable Throwable cause,
RetryBehavior retryBehavior,
FailureInfo originalFailure) {
super(message, cause);
this.rawErrorType = errorType.name();
this.errorType = errorType;
this.errorType =
Arrays.stream(ErrorType.values()).anyMatch((t) -> t.name().equals(rawErrorType))
? ErrorType.valueOf(rawErrorType)
: ErrorType.UNKNOWN;
this.retryBehavior = retryBehavior;
this.originalFailure = originalFailure;
}

/**
* Create a handler exception with a raw error type string, cause, and retry behavior.
*
* @param rawErrorType The raw error type string.
* @param cause The cause of this exception.
* @param retryBehavior The retry behavior for this exception.
*/
public HandlerException(
String rawErrorType, @Nullable Throwable cause, RetryBehavior retryBehavior) {
super(cause == null ? "handler error" : "handler error: " + cause.getMessage(), cause);
this(
rawErrorType,
cause == null ? "handler error" : "handler error: " + cause.getMessage(),
cause,
retryBehavior);
}

/**
* Create a handler exception with a raw error type string, message, cause, and retry behavior.
*
* @param rawErrorType The raw error type string.
* @param message The error message.
* @param cause The cause of this exception.
* @param retryBehavior The retry behavior for this exception.
*/
public HandlerException(
String rawErrorType, String message, @Nullable Throwable cause, RetryBehavior retryBehavior) {
this(rawErrorType, message, cause, retryBehavior, null);
}

/**
* Create a handler exception with a raw error type string, message, cause, retry behavior, and
* original failure.
*
* @param rawErrorType The raw error type string.
* @param message The error message.
* @param cause The cause of this exception.
* @param retryBehavior The retry behavior for this exception.
* @param originalFailure The original failure information if available.
*/
public HandlerException(
String rawErrorType,
String message,
@Nullable Throwable cause,
RetryBehavior retryBehavior,
@Nullable FailureInfo originalFailure) {
super(message, cause);
this.rawErrorType = rawErrorType;
this.errorType =
Arrays.stream(ErrorType.values()).anyMatch((t) -> t.name().equals(rawErrorType))
? ErrorType.valueOf(rawErrorType)
: ErrorType.UNKNOWN;
this.retryBehavior = retryBehavior;
this.originalFailure = originalFailure;
}

/**
Expand All @@ -86,6 +211,12 @@ public RetryBehavior getRetryBehavior() {
return retryBehavior;
}

/** Original FailureInfo if available */
@Nullable
public FailureInfo getOriginalFailure() {
return originalFailure;
}

public boolean isRetryable() {
if (retryBehavior != RetryBehavior.UNSPECIFIED) {
return retryBehavior == RetryBehavior.RETRYABLE;
Expand Down
Loading
Loading